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.
cloacinactlbinary installed- PostgreSQL 16+ running and accessible
- A database created for Cloacina (migrations run automatically on startup)
If you do not have a PostgreSQL instance, use the project’s Docker Compose file:
docker compose -f .angreal/docker-compose.yaml up -d
This 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.
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/cloacina
The 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:8080
On 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).
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-key
Store this key securely. It is the only way to authenticate until you create additional keys.
The bootstrap key is created only when no API keys exist in the database. There are three ways to control it:
The server generates a random key and writes it to ~/.cloacina/bootstrap-key. No flags needed.
Provide a specific key on first startup:
cloacinactl server start \
--database-url postgresql://... \
--bootstrap-key "my-secret-admin-key-here"
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.
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"}' | jq
Response:
{
"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.
The server exposes two unauthenticated health endpoints:
Returns 200 if the process is alive. Does not check database connectivity.
curl -s http://localhost:8080/health | jq
{"status": "ok"}
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.
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"
For production, bind to all interfaces on a specific port:
cloacinactl server start --bind 0.0.0.0:8080
For local-only access (behind a reverse proxy on the same host):
cloacinactl server start --bind 127.0.0.1:8080
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.
The API server does not handle TLS directly. Place a reverse proxy in front of it for HTTPS.
api.example.com {
reverse_proxy localhost:8080
}
Caddy handles automatic certificate provisioning and renewal.
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;
}
}
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 -d
Retrieve 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-key
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.
curl -s http://localhost:8080/v1/auth/keys \
-H "Authorization: Bearer $API_KEY" | jq
curl -s -X DELETE http://localhost:8080/v1/auth/keys/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer $API_KEY" | jq
Revoked 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.