---
title: Deployment with Docker Compose
description: The reference docker-compose.yml — service definitions, startup order, volumes, first-run procedure, and local vs production considerations.
pageType: config
---

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

For local users, the stack is intentionally operated through one small surface: `caracal up`, `caracal status`, and `caracal down`. The service split below is an internal trust-boundary design, not a requirement for users to manage each process by hand.

## Services and startup order

The stack defines the core application containers, infrastructure containers, and one-shot initialization 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.

### `postgres`

```yaml
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.

### `redis`

```yaml
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`.

### `init`

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.

### `sts`

```yaml
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`

### `api`

```yaml
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`

### `gateway`

```yaml
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. Use TLS at the Gateway (`TLS_CERT_FILE`, `TLS_KEY_FILE`) or at a trusted fronting ingress. Internal compose traffic to STS may use the private service network.

**Required:** `DATABASE_URL`, `REDIS_URL`, `STS_URL`, `STREAMS_HMAC_KEY`, `AUDIT_HMAC_KEY`, `GATEWAY_STS_HMAC_KEY`

### `audit`

```yaml
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`

### `coordinator`

```yaml
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`

### `control` profile

```yaml
profiles: [control]
ports: 127.0.0.1:8087:8087
environment:
  CARACAL_CONTROL_ENABLED: "${CARACAL_ENGINE_CONTROL_ENABLED:-false}"
  CONTROL_GATE_FILE: /run/caracal-control/enabled
  CONTROL_PORT: "8087"
  CONTROL_HOST: "0.0.0.0"
volumes:
  - "${CARACAL_CONTROL_STATE_DIR:-./control}:/run/caracal-control:ro"
```

The control service is unmounted by default and managed through the engine. Use the authenticated interactive CLI Control flow or the TUI Control menu instead of operating the Compose profile, thin scripts, or Node entrypoints directly.

The TUI exposes the same flow under **Control**. Use `mount` to load Control with the endpoint gate closed, `enable` and `disable` for fast gate-only access control, and `unmount` to remove the optional runtime for long-term idle state. The engine persists lifecycle state and owns the local gate file mounted into the Control container.

**Required inside the managed service:** `CARACAL_CONTROL_ENABLED=true`, `CONTROL_GATE_FILE`, `STS_JWKS_URL`, `STS_ISSUER_URL`, `CONTROL_AUDIENCE`, `CARACAL_API_URL`, `CONTROL_API_TOKEN`.
`CARACAL_CONTROL_ENABLED` and `CARACAL_CONTROL_STATE_DIR` are injected by the engine lifecycle path; do not set them directly or start the profile manually.

When `CARACAL_MODE=rc` or `CARACAL_MODE=stable`, the control service additionally requires `CONTROL_REDIS_URL` (single-replica memory replay is rejected) and `AUDIT_HMAC_KEY` (audit rows must be signed). The image is built from `apps/control/Dockerfile`; the container runs the TypeScript service that wraps the shared `@caracalai/engine` dispatch.

---

## First-run procedure

**1. Bootstrap credentials and start the stack:**

```bash
pnpm caracal up
```

The first `caracal up` generates per-secret files in `infra/secrets/files/` (gitignored, `0444`) that the compose stack mounts as Docker secrets. Services consume secrets exclusively through the `*_FILE` convention; no secret values are written into any `.env`. `infra/docker/dev.env` is committed and rendered from `packages/engine/src/envSchema.ts` — do not edit it by hand.

To override defaults, copy `infra/docker/local.env.example` to `infra/docker/local.env` (gitignored) and edit it. Compose loads `dev.env` first then `local.env`, so local entries win.

To provision credentials without starting the stack:

```bash
pnpm secrets:init
```

**2. Verify health:**

```bash
pnpm caracal status
```

The five application services plus PostgreSQL and Redis should show `ok`. One-shot initialization containers show `Exited (0)`.

**3. Provision a zone and application:**

The stack starts with an empty database. Use the control-plane admin API (via the CLI) to create the initial zone, register an application, and capture its client secret:

```bash
caracal zone create --name local
caracal app create --zone <zone-id> --name local-app
```

Save the printed client secret into `caracal.toml` alongside `zone_url`, `zone_id`, and `application_id`.

---

## Volumes

| Volume | Mount inside container | Contents |
| --- | --- | --- |
| `postgresData` | `/var/lib/postgresql` | All Postgres data files |
| `redisData` | `/data` | Redis AOF log and RDB snapshot |
| `stsReplay` | `/var/lib/caracal` | STS audit replay buffer |

All three 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.

---

## Port summary

| Service | Local binding | Purpose |
| --- | --- | --- |
| Postgres | `127.0.0.1:5432` | DB access from CLI tools |
| Redis | `127.0.0.1:6379` | Redis CLI debugging |
| STS | `127.0.0.1:8080` | Token exchange; JWKS endpoint |
| API | `127.0.0.1:3000` | Control-plane REST; CLI target |
| Gateway | `127.0.0.1:8081` | Upstream proxy (public-facing in prod) |
| Coordinator | `127.0.0.1:4000` | Agent lifecycle; delegation graph |
| Audit | `127.0.0.1:9090` | Audit query and export status |
| Control | `127.0.0.1:8087` | Optional engine-managed endpoint |

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.

---

## Production adaptation checklist

- 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 `CARACAL_MODE=stable`.
- Provide `TLS_CERT_FILE` and `TLS_KEY_FILE` for the Gateway, or terminate TLS at a trusted ingress before traffic reaches it.
- Set `STREAMS_HMAC_KEY`, `AUDIT_HMAC_KEY`, and `GATEWAY_STS_HMAC_KEY` on services that require them.
- 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).
