Skip to content

Deployment with Docker Compose

The Docker Compose stack at infra/docker/docker-compose.yml is the reference deployment for all five Caracal services plus their infrastructure dependencies. It is the canonical local development environment and the blueprint for adapting to Kubernetes, ECS, or other orchestrators.

The stack defines eight containers. Health-based dependencies enforce this startup sequence:

postgres ─────┐
redis ─────┤── init ── sts ── api ── gateway
└── coordinator
└── init ── audit

The init container must complete before STS, API, audit, or coordinator start. STS must be healthy before API and coordinator start. API must be healthy before the gateway starts.

image: caracal/postgres:dev # postgres:18-alpine
ports: 127.0.0.1:5432:5432
volumes: postgresData:/var/lib/postgresql
healthcheck: pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}
interval: 5s timeout: 3s retries: 20

The custom image adds no logic on top of the base Postgres image. Database migrations run from the API service on startup, not from this container.

image: caracal/redis:dev # redis:8-alpine with custom redis.conf
ports: 127.0.0.1:6379:6379
volumes: redisData:/data
command: --requirepass ${REDIS_PASSWORD}
healthcheck: redis-cli -a ${REDIS_PASSWORD} --no-auth-warning PING | grep PONG
interval: 5s timeout: 3s retries: 20

Starts with redis-server /etc/caracal/redis.conf, which enables appendonly yes, appendfsync everysec, and maxmemory-policy noeviction.

A one-shot container (restart: "no") that runs infra/redis/provision-streams.sh after both Postgres and Redis are healthy. It creates all Redis streams and consumer groups idempotently using XGROUP CREATE ... MKSTREAM. Safe to re-run at any time.

All other services depend (directly or transitively) on init completing with exit code 0.

image: caracal/sts:dev
ports: 127.0.0.1:8080:8080
depends_on:
init: { condition: service_completed_successfully }
healthcheck: wget -qO- http://127.0.0.1:8080/ready || exit 1

Go binary. Does not run migrations. Healthy once Postgres and Redis are reachable and the audit replay buffer is ready.

Required: DATABASE_URL, REDIS_URL, ZONE_KEK, AUDIT_HMAC_KEY, ISSUER_URL

image: caracal/api:dev
ports: 127.0.0.1:3000:3000
depends_on:
sts: { condition: service_healthy }
healthcheck: wget -qO- http://127.0.0.1:3000/ready || exit 1

TypeScript/Node.js service. Runs Postgres migrations (0001 through current) on every startup — idempotent. Seeds the bootstrap admin token if CARACAL_ADMIN_TOKEN is set and not yet present. Starts the transactional outbox dispatcher as a background worker.

Required: DATABASE_URL, REDIS_URL, STS_URL, ZONE_KEK, CARACAL_ADMIN_TOKEN

image: caracal/gateway:dev
ports: 127.0.0.1:8081:8081
depends_on:
sts: { condition: service_healthy }
api: { condition: service_healthy }
healthcheck: wget -qO- http://127.0.0.1:8081/ready || exit 1

Go binary. The only service exposed externally in production. Requires TLS in production (TLS_CERT_FILE, TLS_KEY_FILE). In development, set INSECURE_HTTP=true and INSECURE_STS=true.

Required: DATABASE_URL, REDIS_URL, STS_URL, STREAMS_HMAC_KEY

image: caracal/audit:dev
ports: 127.0.0.1:9090:9090
depends_on:
init: { condition: service_completed_successfully }
healthcheck: wget -qO- http://127.0.0.1:9090/ready || exit 1

Go binary. Consumes caracal.audit.events stream, persists events to the partitioned audit_events table, runs the chain HMAC tamper sweeper, and drives Parquet export to S3 if configured.

Required: DATABASE_URL, REDIS_URL, AUDIT_HMAC_KEY

image: caracal/coordinator:dev
ports: 127.0.0.1:4000:4000
depends_on:
sts: { condition: service_healthy }
healthcheck: wget -qO- http://127.0.0.1:4000/ready || exit 1

Runs two processes via start.sh: the Go relay binary (background) and the Node.js coordinator (foreground). The relay forwards lifecycle events to Redis streams. The coordinator manages agent sessions, delegation edges, and invocations.

Required: DATABASE_URL, REDIS_URL, STS_URL, ISSUER_URL, AGENT_COORDINATOR_SCOPE


1. Copy and populate the env file:

Terminal window
cp infra/docker/.env.example infra/docker/.env

Set these values in infra/docker/.env:

Terminal window
POSTGRES_PASSWORD=<strong-password>
REDIS_PASSWORD=<strong-password>
CARACAL_ADMIN_TOKEN=<strong-token>
ZONE_KEK=$(openssl rand -hex 32)
AUDIT_HMAC_KEY=$(openssl rand -hex 32)
STREAMS_HMAC_KEY=$(openssl rand -hex 32)
# Development only — do not use in production
INSECURE_STS=true
INSECURE_HTTP=true
CARACAL_LOCAL_BOOTSTRAP_ENABLED=true

2. Build images:

Terminal window
cd infra/docker && docker compose build

3. Start the stack:

Terminal window
docker compose up -d

4. Verify health:

Terminal window
docker compose ps

All services should show healthy. The init container shows Exited (0).

5. Bootstrap (first run only):

With CARACAL_LOCAL_BOOTSTRAP_ENABLED=true, the API seeds initial zone and application data automatically on first startup. To trigger bootstrap explicitly:

Terminal window
caracal stack bootstrap

VolumeMount inside containerContents
postgresData/var/lib/postgresqlAll Postgres data files
redisData/dataRedis AOF log and RDB snapshot

Both are Docker-managed named volumes. They survive docker compose down but are deleted with docker compose down -v.

In production, replace these with external volumes backed by persistent block storage (EBS, GCP PD, Azure Disk). Do not use ephemeral container storage for either.


ServiceLocal bindingPurpose
Postgres127.0.0.1:5432DB access from CLI tools
Redis127.0.0.1:6379Redis CLI debugging
STS127.0.0.1:8080Token exchange; JWKS endpoint
API127.0.0.1:3000Control-plane REST; CLI target
Gateway127.0.0.1:8081Upstream proxy (public-facing in prod)
Coordinator127.0.0.1:4000Agent lifecycle; delegation graph
Audit127.0.0.1:9090Audit query and export status

All bindings use 127.0.0.1 only. In production, services communicate over a private internal network and only the Gateway is exposed to external traffic.


  • Replace named volumes with persistent external volumes.
  • Run multiple replicas of stateless services (STS, API, Gateway, Coordinator) behind a load balancer.
  • Supply secrets from a vault or secrets manager; do not use .env files.
  • Set INSECURE_HTTP=false and INSECURE_STS=false; provide TLS_CERT_FILE and TLS_KEY_FILE for the Gateway.
  • Set CARACAL_ENV=production on the Gateway.
  • Remove CARACAL_LOCAL_BOOTSTRAP_ENABLED=true from the API env.
  • Set STREAMS_HMAC_KEY on all services that publish or consume from Redis streams.
  • Scale the Audit service to a single replica with leader election active (the service uses advisory locks to elect an export leader and a retention leader).