How to Deploy Cloacina in Production
This guide covers deploying cloacinactl server start behind a TLS-terminating reverse proxy.
Prerequisites
Section titled “Prerequisites”- PostgreSQL 14+ accessible from the server
- A reverse proxy (nginx, Caddy, or Envoy)
- TLS certificate (Let’s Encrypt, or self-signed for internal use)
Why a Reverse Proxy?
Section titled “Why a Reverse Proxy?”cloacinactl server start binds plain HTTP. All traffic — including API keys, tenant credentials, and workflow package uploads — is transmitted in cleartext without a reverse proxy providing TLS termination.
A reverse proxy also provides:
- TLS termination with automatic certificate renewal (Caddy)
- Connection draining and load balancing
- Request logging and access control
- Static file serving for documentation
Caddy (Recommended)
Section titled “Caddy (Recommended)”Caddy provides automatic HTTPS with Let’s Encrypt.
# Caddyfilecloacina.example.com { reverse_proxy localhost:8080}Start Caddy:
caddy run --config Caddyfileserver { listen 443 ssl http2; server_name cloacina.example.com;
ssl_certificate /etc/ssl/certs/cloacina.pem; ssl_certificate_key /etc/ssl/private/cloacina.key;
# WebSocket support location /v1/ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
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;
# Body size limit (match server's 100MB limit) client_max_body_size 100m; }}Starting the Server
Section titled “Starting the Server”# Basic startcloacinactl server start \ --database-url "postgres://user:pass@localhost:5432/cloacina" \ --bind 127.0.0.1:8080
# With explicit bootstrap keycloacinactl server start \ --database-url "postgres://user:pass@localhost:5432/cloacina" \ --bind 127.0.0.1:8080 \ --bootstrap-key "clk_your_bootstrap_key_here"The server binds to 127.0.0.1 (localhost only) when behind a reverse proxy — do not bind to 0.0.0.0 without TLS.
Health Checks
Section titled “Health Checks”Configure your load balancer to check:
- Liveness:
GET /health— returns 200 if the process is alive - Readiness:
GET /ready— returns 200 when the database connection pool is reachable and no loaded computation graph has crashed; otherwise 503 with areasonfield (database unreachableorcrashed computation graphs)
Docker Compose Example
Section titled “Docker Compose Example”services: postgres: image: postgres:16 environment: POSTGRES_USER: cloacina POSTGRES_PASSWORD: cloacina POSTGRES_DB: cloacina volumes: - pgdata:/var/lib/postgresql/data
cloacina: build: . # cloacina-server takes its flags directly (no subcommand). Binding # 0.0.0.0 is safe here because the port is NOT published to the host — # only Caddy (443) is reachable from outside the compose network. command: ["--database-url", "postgres://cloacina:cloacina@postgres:5432/cloacina", "--bind", "0.0.0.0:8080"] depends_on: - postgres expose: - "8080"
caddy: image: caddy:2 ports: - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile
volumes: pgdata:Choosing a deployment shape first
Section titled “Choosing a deployment shape first”Before productionizing, confirm you’re deploying the right shape — embedded library, local daemon, single server, or server + compiler + agent fleet. The When to Use Cloacina decision table maps each goal to a mode. This guide assumes the server shape; for the horizontally-scaled fleet see Deploy an Execution Agent Fleet.
Production readiness checklist
Section titled “Production readiness checklist”Work through this before exposing a server to real traffic. Each item links to the guide that covers it in depth.
Network & transport
- TLS terminated at a reverse proxy (Caddy/nginx above); the server itself binds plain HTTP.
- Server bound to
127.0.0.1(or a private interface) — never0.0.0.0reachable from the internet without the proxy in front. Don’t publish the raw8080port. - Reverse-proxy
client_max_body_sizematches the server’s 100 MB package-upload limit. - WebSocket upgrade headers proxied (needed for live execution streams and event ingestion).
Authentication & access
- Bootstrap admin key captured into a secret manager on first start, then the
~/.cloacina/bootstrap-keyfile removed or locked down. It is shown only once. - Application clients use tenant-scoped keys (
cloacinactl key create … --role …), never the admin key. See 01 - Deploy a Server. - CORS configured only if a browser UI calls the server cross-origin — via the
CLOACINA_CORS_ALLOWED_ORIGINSenv var (works withcloacinactl server start) or the--cors-allowed-originsflag on thecloacina-serverbinary. The value is the origin users load the UI from. See Deploy the Web UI.
Data & isolation
- PostgreSQL (not SQLite) for any multi-tenant or multi-replica deployment. See Database Backends.
- Multi-tenant isolation reviewed — in particular that executions actually run against the tenant’s own schema (a misconfigured runner can execute against the wrong schema and break isolation). See Configure a Multi-Tenant Deployment.
- Database backups and a restore drill in place. See Back Up and Restore.
Supply chain
- In low-trust / multi-tenant deployments, package signing required so unsigned packages are refused — otherwise a compromised or untrusted uploader could get arbitrary code compiled and run in your build sandbox. See Require Signed Packages.
Operations
- Load-balancer health checks wired to
GET /health(liveness) andGET /ready(readiness — confirms the DB pool is reachable and no computation graph has crashed). - Metrics scraped from
/metricsand tracing wired up. See Observe Execution State. - Runner sizing tuned for your load — database connection pool size, task concurrency, and execution timeouts. See Performance Tuning for the server knobs and recommended values.
Upgrading the server
Section titled “Upgrading the server”For Kubernetes/Helm deployments, follow the rolling-update path in
Deploying to Kubernetes.
For a directly-managed server (cloacinactl server start or the container image),
upgrade like this:
- Back up the database first. Run a fresh backup (Back Up and Restore) so you can roll back if a migration misbehaves.
- Pin the new version. Deploy a specific image tag (e.g.
ghcr.io/colliery-io/cloacina-server:0.12.0), never:latest, so the rollout is reproducible. - Drain, then swap. Stop sending new traffic at the reverse proxy, let in-flight requests finish, stop the old process, and start the new one against the same
DATABASE_URL. The server runs any pending schema migrations automatically on startup (embedded, transaction-safe), so no manual migration step is needed. - Gate traffic on readiness. Wait for
GET /readyto return 200 before re-enabling proxy traffic — it confirms the DB pool is reachable and no loaded graph crashed on boot.
Downtime note: a single-replica server has a brief gap between stop and ready. For zero-downtime, run multiple replicas against one PostgreSQL database (see Horizontal Scaling) and roll them one at a time behind the proxy.
Rollback: if the new version fails readiness, restart the previous pinned tag against the same database. If a migration changed the schema incompatibly, restore the pre-upgrade backup from step 1.