Deploying the API Server
This guide covers starting the Cloacina API server, configuring authentication, and deploying to production. The API server provides a multi-tenant HTTP interface backed by PostgreSQL.
Prerequisites
Section titled “Prerequisites”cloacinactlbinary installed- PostgreSQL 16+ running and accessible
- A database created for Cloacina (migrations run automatically on startup)
Starting the Server
Section titled “Starting the Server”Step 1: Start PostgreSQL
Section titled “Step 1: Start PostgreSQL”If you do not have a PostgreSQL instance, use the project’s Docker Compose file:
docker compose -f .angreal/docker-compose.yaml up -dThis starts PostgreSQL 16 published on host port 15432 (deliberately not 5432, to avoid colliding with other local Postgres instances) with credentials cloacina:cloacina and database cloacina.
Step 2: Start the API Server
Section titled “Step 2: Start the API Server”cloacinactl server start execs the cloacina-server binary from your PATH — both binaries must be installed.
cloacinactl server start --database-url postgresql://cloacina:cloacina@localhost:15432/cloacinaThe server binds to 127.0.0.1:8080 (loopback) by default. To accept remote connections — e.g. in a container or behind a proxy — set the bind address explicitly (0.0.0.0:8080 for all interfaces):
cloacinactl server start \ --database-url postgresql://cloacina:cloacina@localhost:15432/cloacina \ --bind 0.0.0.0:8080On startup, the server connects to PostgreSQL, applies any pending migrations, and starts serving. The unauthenticated operational endpoints (/health, /ready, /metrics, /openapi.json) live at the root; everything else is under the /v1 prefix (e.g. POST /v1/auth/keys).
Step 3: Retrieve the Bootstrap Key
Section titled “Step 3: Retrieve the Bootstrap Key”On first startup, the server auto-generates an admin API key and writes it to ~/.cloacina/bootstrap-key with 0600 permissions. Read it once:
cat ~/.cloacina/bootstrap-keyStore this key securely. It is the only way to authenticate until you create additional keys.
Bootstrap Key Options
Section titled “Bootstrap Key Options”The bootstrap key is created only when no API keys exist in the database. There are three ways to control it:
Auto-Generated (Default)
Section titled “Auto-Generated (Default)”The server generates a random key and writes it to ~/.cloacina/bootstrap-key. No flags needed.
Explicit Key via Flag
Section titled “Explicit Key via Flag”Provide a specific key on first startup:
cloacinactl server start \ --database-url postgresql://... \ --bootstrap-key "my-secret-admin-key-here"Explicit Key via Environment Variable
Section titled “Explicit Key via Environment Variable”export CLOACINA_BOOTSTRAP_KEY="my-secret-admin-key-here"cloacinactl server start --database-url postgresql://...In all cases, the plaintext key is written to ~/.cloacina/bootstrap-key (mode 0600). On subsequent startups, the bootstrap step is skipped because keys already exist.
First-Time Setup: Create an API Key
Section titled “First-Time Setup: Create an API Key”Use the bootstrap key to create a named API key for regular use:
curl -s -X POST http://localhost:8080/v1/auth/keys \ -H "Authorization: Bearer $(cat ~/.cloacina/bootstrap-key)" \ -H "Content-Type: application/json" \ -d '{"name": "ci-deploy", "role": "write"}' | jqResponse:
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "ci-deploy", "key": "clk_abc123...", "permissions": "write", "tenant_id": "public", "is_admin": false, "created_at": "2026-04-02T12:00:00+00:00"}The key field is returned exactly once. Store it in your secrets manager. All authenticated endpoints require an Authorization: Bearer <key> header.
Keys minted on this global endpoint are scoped to the built-in public tenant. To create keys for a named tenant, use POST /v1/tenants/{tenant_id}/keys — see Manage API Keys.
Health Checks
Section titled “Health Checks”The server exposes two unauthenticated health endpoints:
Liveness: GET /health
Section titled “Liveness: GET /health”Returns 200 if the process is alive. Does not check database connectivity.
curl -s http://localhost:8080/health | jq{"status": "ok"}Readiness: GET /ready
Section titled “Readiness: GET /ready”Returns 200 if the server can acquire a database connection from the pool. Returns 503 with a reason field (database unreachable) otherwise.
Readiness is scoped to platform health only. Tenant workload health — for example a crashed computation graph from a bad package — deliberately does not fail /ready: with multiple replicas behind a load balancer, a single crashed graph would otherwise eject every replica from the pool simultaneously. Crashed graphs remain visible via GET /v1/health/graphs (and the cloacina_component_health metric).
curl -s http://localhost:8080/ready | jq{"status": "ready"}Use /health for container liveness probes and /ready for load balancer readiness checks.
Production Configuration
Section titled “Production Configuration”Database URL
Section titled “Database URL”The database URL can be provided through three sources (highest priority first):
--database-urlCLI flagDATABASE_URLenvironment variabledatabase_urlin~/.cloacina/config.toml
Set it in the config file for convenience:
cloacinactl config set database_url "postgresql://cloacina:secret@db.example.com:5432/cloacina"Bind Address
Section titled “Bind Address”For production, bind to all interfaces on a specific port:
cloacinactl server start --bind 0.0.0.0:8080For local-only access (behind a reverse proxy on the same host):
cloacinactl server start --bind 127.0.0.1:8080Logging
Section titled “Logging”The server writes logs to both stderr and ~/.cloacina/logs/cloacina-server.log (daily rotation, JSON format). Control verbosity with RUST_LOG:
RUST_LOG=info cloacinactl server start --database-url postgresql://...Use --verbose for debug-level output during troubleshooting.
TLS Termination
Section titled “TLS Termination”The API server does not handle TLS directly. Place a reverse proxy in front of it for HTTPS.
Caddy Example
Section titled “Caddy Example”api.example.com { reverse_proxy localhost:8080}Caddy handles automatic certificate provisioning and renewal.
Nginx Example
Section titled “Nginx Example”server { listen 443 ssl; server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}Docker Deployment
Section titled “Docker Deployment”docker-compose.yaml
Section titled “docker-compose.yaml”The repository ships a canonical production-shape compose file at
deploy/docker-compose/cloacina.yml: one Postgres, one
cloacina-server (HTTP API + reconciler), and one cloacina-compiler
(build worker — required for Rust package uploads to ever leave
pending). The server and compiler coordinate only through the shared
database.
services: postgres: image: postgres:16 environment: POSTGRES_USER: cloacina POSTGRES_PASSWORD: cloacina POSTGRES_DB: cloacina volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U cloacina"] interval: 10s timeout: 5s retries: 5
cloacina-server: image: ghcr.io/colliery-io/cloacina-server:latest command: - "--bind" - "0.0.0.0:8080" - "--database-url" - "postgres://cloacina:cloacina@postgres:5432/cloacina" depends_on: postgres: condition: service_healthy ports: - "8080:8080" volumes: - server-home:/var/lib/cloacina
cloacina-compiler: image: ghcr.io/colliery-io/cloacina-compiler:latest command: - "--bind" - "0.0.0.0:9000" - "--database-url" - "postgres://cloacina:cloacina@postgres:5432/cloacina" depends_on: postgres: condition: service_healthy ports: - "9000:9000" volumes: - compiler-home:/var/lib/cloacina
volumes: pgdata: server-home: compiler-home:Pin the image tags to a concrete version in real deployments. Start with:
docker compose -f deploy/docker-compose/cloacina.yml up -dRetrieve the bootstrap key from the server container (the server’s home
directory defaults to $HOME/.cloacina, so as root in the container the
key lands at /root/.cloacina/bootstrap-key; pass --home to relocate
it onto the mounted volume if you want it to survive container
recreation):
docker compose -f deploy/docker-compose/cloacina.yml \ exec cloacina-server cat /root/.cloacina/bootstrap-keyGraceful Shutdown
Section titled “Graceful Shutdown”The server handles SIGINT (Ctrl+C) and SIGTERM for graceful shutdown, draining in-flight HTTP requests before exiting. Container orchestrators like Kubernetes send SIGTERM by default, which the server handles correctly.
API Key Management
Section titled “API Key Management”List Keys
Section titled “List Keys”curl -s http://localhost:8080/v1/auth/keys \ -H "Authorization: Bearer $API_KEY" | jqRevoke a Key
Section titled “Revoke a Key”curl -s -X DELETE http://localhost:8080/v1/auth/keys/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer $API_KEY" | jqRevoked keys are rejected immediately (the server clears its LRU auth cache on revocation).
For the full list of API endpoints including tenant management, workflow upload, and execution, see the HTTP API Reference.