Skip to content

Docker

The tracked Compose stack starts Chorus, SurrealDB, and Garage. It binds public-facing ports to loopback, pins images, runs read-only containers with bounded resources, and refuses to render until every credential has been supplied explicitly.

Create a gitignored .env next to docker-compose.yml with permissions limited to your user. Each deployment must use fresh values:

Terminal window
umask 077
mkdir -p .secrets
openssl rand -hex 32 > .secrets/garage_rpc_secret
openssl rand -hex 32 > .secrets/garage_admin_token
openssl rand 48 > .secrets/chorus_rate_limit_hmac_key
openssl genpkey -algorithm ED25519 \
-out .secrets/chorus_approval_execution_signing_key.pem
{
echo "SURREAL_PASS=$(openssl rand -hex 32)"
echo "GARAGE_RPC_SECRET_FILE=.secrets/garage_rpc_secret"
echo "GARAGE_ADMIN_TOKEN_FILE=.secrets/garage_admin_token"
echo "CHORUS_RATE_LIMIT_HMAC_KEY_FILE=.secrets/chorus_rate_limit_hmac_key"
echo "CHORUS_APPROVAL_EXECUTION_SIGNING_KEY_FILE=.secrets/chorus_approval_execution_signing_key.pem"
} > .env

Run the tracked preflight before creating containers. It resolves Compose, rejects missing, short, placeholder, or Garage-incompatible credentials, validates Garage’s exact key shapes, and requires all four mandatory mounted secret files to be regular, nonempty, and exactly mode 0400 or 0600. The rate-limit HMAC key must contain 32–4096 opaque bytes after terminal line endings are removed. The approval-execution key must be one unencrypted Ed25519 private key in PKCS8 PEM form; preflight validates its envelope, private-key structure, and public algorithm without printing or copying its private bytes. It also validates the runtime shutdown bounds and requires the whole-second CHORUS_STOP_GRACE_PERIOD to exceed the graceful plus force-abort budgets. Preflight reports names and reasons, never secret contents.

Supply S3 credentials from the generated owner-only file described below before invoking preflight. A fresh deployment uses temporary shape-valid values only long enough to render and start Garage; those values are never imported.

The operator scripts use the Docker CLI’s Compose v2 plugin by default. Set COMPOSE_BIN=/absolute/path/to/docker-compose to select a reviewed standalone Compose v2 binary; the scripts invoke that path directly and still use docker for core Engine operations.

Garage needs a one-time layout, bucket, and key before Chorus can use artifact storage. Garage v1.0.1 accepts an imported secret only as a positional CLI argument, so Chorus never automates key import. Normal bootstrap is strictly verify-only: it reads and checks the exact single-node layout, bucket, configured key/secret, and RWO permission, but never assigns or applies a layout, creates a bucket or key, deletes a key, or grants or revokes permission. It fails closed when any persisted state is absent or different.

For a fresh Garage volume, let Garage generate the key. The explicit generation path creates a uniquely named key and captures its output in an owner-only recovery file. It durably stages the canonical shell credentials, grants only that invocation’s new key, re-reads and verifies the exact key, secret, and RWO permission, and only then atomically publishes the no-clobber output file. Any failure before publication deletes only the freshly created key by ID.

Terminal window
umask 077
export S3_ACCESS_KEY="GK$(openssl rand -hex 12)"
export S3_SECRET_KEY="$(openssl rand -hex 32)"
export GARAGE_CREDENTIALS_FILE="$PWD/.secrets/garage_s3.env"
test ! -e "$GARAGE_CREDENTIALS_FILE"
./scripts/deployment-preflight.sh
docker compose up -d surrealdb garage
GARAGE_BOOTSTRAP_GENERATE_CREDENTIALS_FILE="$GARAGE_CREDENTIALS_FILE" \
./scripts/garage-bootstrap.sh
. "$GARAGE_CREDENTIALS_FILE"
./scripts/deployment-preflight.sh
docker compose up -d chorus
docker compose ps

The first S3 pair exists only to satisfy the fail-closed Compose model before Garage is running; the generation command never imports it. Sourcing the generated file replaces both variables with Garage’s actual key. The selected output path must be absolute and absent; its existing parent directory must be owned by the current user and not group- or world-writable. Bootstrap creates the file as mode 0600, fsyncs its file and directory state, and never overwrites it.

If key deletion cannot be confirmed after an interrupted or failed generation, bootstrap keeps $GARAGE_CREDENTIALS_FILE.recovery (canonical shell form) or $GARAGE_CREDENTIALS_FILE.recovery.raw (the durably recorded unique name plus Garage CLI output), prints the fresh key ID or unique name, and refuses to retry. Do not delete these files blindly. Inspect the reported key in Garage, delete that freshly generated key with garage key delete <key-id> --yes if it still exists, and only after confirming Garage state remove the recovery sibling and rerun bootstrap. Neither recovery file is committed configuration; both remain owner-only and gitignored.

Keep that gitignored file as the canonical credential source. In every later shell, source it before Compose. Running bootstrap without generation is optional and idempotently verifies the existing layout, key, secret, bucket, and permission without changing Garage:

Terminal window
. "$PWD/.secrets/garage_s3.env"
./scripts/deployment-preflight.sh
./scripts/garage-bootstrap.sh
docker compose up -d

Chorus is reachable at http://127.0.0.1:3000; SurrealDB is reachable locally at 127.0.0.1:8000. Garage’s S3 and administration ports are internal-only. To expose Chorus beyond the host, place an authenticated TLS reverse proxy in front of it and set SIWE_PUBLIC_URL, embedded-MCP authority allowlists, and trusted proxy policy explicitly. The pinned SurrealDB image receives its initial root user and password through SURREAL_USER and SURREAL_PASS environment entries; the password is never placed in container command arguments, and the disposable drills attest both the resolved model and the running container.

Compose mounts the private chorus-home named volume at /home/bun/.chorus. Zero-config therefore writes the first admin profile inside the Chorus container, not to the host account’s ~/.chorus/config.yaml. Verify it in place without printing the API key:

Terminal window
docker compose exec -T chorus bun run packages/cli/src/index.ts whoami

To use the source CLI on the host, run from the repository root, install the frozen dependencies, and copy the profile into a dedicated gitignored home. The target must not already exist:

Terminal window
bun install --frozen-lockfile
umask 077
export CHORUS_CLI_HOME="$PWD/.secrets/chorus-cli-home"
export CHORUS_HOST_CONFIG="$CHORUS_CLI_HOME/.chorus/config.yaml"
mkdir -p "$CHORUS_CLI_HOME/.chorus"
test ! -e "$CHORUS_HOST_CONFIG"
docker compose cp chorus:/home/bun/.chorus/config.yaml "$CHORUS_HOST_CONFIG"
chmod 600 "$CHORUS_HOST_CONFIG"
HOME="$PWD/.secrets/chorus-cli-home" bun run packages/cli/src/index.ts whoami

Keep using that command-scoped HOME for later host CLI calls. Do not copy over an existing CLI home, echo the file, or place its API key in a process argument.

Federation remains disabled in the base stack. To enable it, create a separate key without adding its contents to the image or .env:

Terminal window
umask 077
openssl rand 48 > .secrets/chorus_federation_key
chmod 600 .secrets/chorus_federation_key
cat >> .env <<'ENV'
FEDERATION_ENABLED=true
CHORUS_FEDERATION_PUBLIC_ORIGIN=https://chorus.example
CHORUS_FEDERATION_KEY_ENCRYPTION_KEY_HOST_FILE=.secrets/chorus_federation_key
ENV
./scripts/deployment-preflight.sh
docker compose up -d chorus

The base Compose model mounts that host file at the fixed in-container path. While federation is disabled it uses an inert /dev/null source; when enabled, preflight resolves the same project .env as Compose and refuses to proceed unless the host file, key length, and public origin are valid. Startup reads the mounted file before connecting to SurrealDB. Do not reuse the rate-limit HMAC key.

The optional local reranker is profile-gated:

Terminal window
docker compose --profile reranker up -d reranker

SurrealDB readiness uses its bundled /surreal isready command (the distroless image does not put that binary on the docker exec PATH). Chorus probes GET /health/ready with Bun’s built-in fetch; neither healthcheck assumes curl exists in a slim image. The readiness route is 200 only after startup owns the listener and all resources, and becomes 503 as soon as drain starts. SurrealDB receives a two-minute start grace because a new RocksDB volume can take more than one minute to open on the supported Docker Desktop path without being stuck. GET /health is liveness and does not mean the process accepts work. Compose waits for both SurrealDB and Garage health before starting Chorus. Chorus has a ten-minute health start grace because measured first-ledger adoption and production-sized migrations can make valid progress for more than five minutes before the listener opens; after that grace, twelve failed ten-second probes mark it unhealthy.

Terminal window
docker compose ps
curl --fail http://127.0.0.1:3000/health/ready
docker compose down

The first SIGTERM or SIGINT closes admission, stops schedulers, waits for accepted HTTP/MCP work and streams, closes resources in reverse acquisition order, and disconnects SurrealDB. The default total deadline is 30 seconds (CHORUS_SHUTDOWN_TIMEOUT_MS); a second signal or the deadline starts a bounded force-abort pass (CHORUS_FORCE_ABORT_TIMEOUT_MS, one second by default), logs unfinished component diagnostics, and exits nonzero. Compose grants Chorus 45 seconds by default. Set CHORUS_STOP_GRACE_PERIOD (for example 311s for the maximum runtime budgets) when increasing either timeout; deployment preflight rejects an equal or shorter grace.

Named volumes retain database, object, and local Chorus bootstrap state. Use the tracked compose-backup.sh and restore runbook before an upgrade. docker compose down -v destroys that state and is not an ordinary shutdown command.

Every service uses bounded local JSON logs (three 10 MiB files). The normal restart policy is unless-stopped; the disposable restore drill overrides it to no, so an abruptly abandoned drill is not resurrected by a later Docker daemon restart.

The pinned SurrealDB image runs as uid 65532, so its named volume is mounted at the image-owned /home/nonroot directory. Garage and Chorus retain only CAP_DAC_READ_SEARCH after dropping all capabilities; this is the minimum read-only exception needed for owner-only file-backed Compose secrets whose host ownership is preserved by bind mounts.