Skip to content

Deploy with Docker

Chorus ships an official Docker image, chorusaidlc/chorus-app. This guide shows the two Compose shapes — standalone (embedded database) and production (external PostgreSQL + Redis) — a plain docker run against an existing database, the full environment-variable reference, and what the container does at startup.

Pull the image first:

Terminal window
docker pull chorusaidlc/chorus-app:latest

For how to choose between the standalone and production forms, see the Deployment overview.

No external database needed. The image bundles PGlite (embedded PostgreSQL) and starts everything automatically. Data persists in a Docker volume across container restarts.

Create a docker-compose.local.yml:

# Standalone Chorus — embedded PGlite, no external PostgreSQL or Redis
services:
app:
image: chorusaidlc/chorus-app:latest
ports:
- "8637:8637"
environment:
# No DATABASE_URL — entrypoint auto-starts embedded PGlite
- REDIS_URL=
# Leave empty: the container generates a random session secret on first
# start and keeps it in the chorus-local-data volume. Set it explicitly
# (openssl rand -base64 32) if you prefer to manage it yourself.
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-}
- COOKIE_SECURE=false
- DEFAULT_USER=${DEFAULT_USER:-admin@example.com}
- DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-changeme}
volumes:
- chorus-local-data:/app/data
volumes:
chorus-local-data:

Then run:

Terminal window
docker compose -f docker-compose.local.yml up -d

Open http://localhost:8637 and log in with admin@example.com / changeme (or override via the DEFAULT_USER / DEFAULT_PASSWORD environment variables).

In standalone mode the container:

  • Starts PGlite on an internal port (5433), not exposed externally.
  • Stores data in the chorus-local-data Docker volume, so it persists across restarts.
  • Generates a random session-signing secret on first start and stores it in the same volume when NEXTAUTH_SECRET is left empty (see Session secret).
  • Disables Redis (falls back to the in-memory EventBus — single-instance only).
  • Runs Prisma migrations automatically on startup.

For production, especially with multiple replicas, wire the app to an external PostgreSQL and Redis. Create a docker-compose.yml:

services:
app:
image: chorusaidlc/chorus-app:latest
ports:
- "8637:8637"
environment:
- DATABASE_URL=postgresql://chorus:chorus@db:5432/chorus
- REDIS_URL=redis://default:chorus-redis@redis:6379
# Session-signing secret. Leave empty and the container generates one on
# first start and persists it in the chorus-app-data volume (single
# replica only). For production, or more than one app container, set it
# explicitly in .env or the environment: openssl rand -base64 32
# Changing the secret invalidates Default Auth and Super Admin sessions.
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-}
- COOKIE_SECURE=${COOKIE_SECURE:-false}
- DEFAULT_USER=admin@example.com
- DEFAULT_PASSWORD=your-password
volumes:
- chorus-app-data:/app/data
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:7-alpine
command: redis-server --requirepass chorus-redis
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "chorus-redis", "ping"]
interval: 5s
timeout: 3s
retries: 5
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: chorus
POSTGRES_PASSWORD: chorus
POSTGRES_DB: chorus
volumes:
- chorus-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U chorus -d chorus"]
interval: 5s
timeout: 5s
retries: 5
volumes:
chorus-app-data:
chorus-data:
redis-data:

To supply your own session secret, generate it once before starting and put it in a .env file next to the compose file:

Terminal window
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env

Then run:

Terminal window
docker compose up -d

Open http://localhost:8637 and log in with the credentials you set in DEFAULT_USER / DEFAULT_PASSWORD.

If you already have PostgreSQL and Redis running, start the container directly:

Terminal window
docker run -d \
-p 8637:8637 \
-e DATABASE_URL=postgresql://user:pass@your-db-host:5432/chorus \
-e REDIS_URL=redis://default:password@your-redis-host:6379 \
-v chorus-app-data:/app/data \
-e COOKIE_SECURE=false \
-e DEFAULT_USER=admin@example.com \
-e DEFAULT_PASSWORD=your-password \
chorusaidlc/chorus-app:latest

This example keeps the generated secret in chorus-app-data across container recreates. To manage it yourself, inject one persistent NEXTAUTH_SECRET and reuse it on every start.

VariableDescription
DATABASE_URLPostgreSQL connection string. Format: postgresql://user:password@host:port/dbname. Alternatively, set individual DB_* variables (see below). If omitted, the entrypoint starts an embedded PGlite instance automatically.
NEXTAUTH_SECRETSecret key for signing session tokens. Use a random string (for example, openssl rand -base64 32). In Docker it may be left empty: the container then generates one on first start and keeps it in /app/data/.secret. Must be set explicitly, to the same value, on every replica when running more than one app container. See Session secret.

If DATABASE_URL is not set, the entrypoint builds it from these individual variables:

VariableDescription
DB_HOSTPostgreSQL host
DB_PORTPostgreSQL port (default: 5432)
DB_USERNAMEPostgreSQL username
DB_PASSWORDPostgreSQL password
DB_NAMEDatabase name
VariableDescription
REDIS_URLFull Redis connection string. Format: redis://username:password@host:port. Takes precedence over the individual variables.
REDIS_HOSTRedis host (used if REDIS_URL is not set)
REDIS_PORTRedis port (default: 6379)
REDIS_USERNAMERedis username (default: default)
REDIS_PASSWORDRedis password
VariableDescription
DEFAULT_USEREmail address for built-in login (bypasses OIDC). Auto-provisions the user and company on first login.
DEFAULT_PASSWORDPassword for the default user (plain text, compared via bcrypt at runtime).
NEXTAUTH_URLPublic-facing base URL of the app (default: http://localhost:8637). Set this when running behind a reverse proxy.
COOKIE_SECURESet to "false" to disable secure cookies for HTTP-only deployments (default: "false" in docker-compose). Set to "true" when deploying with HTTPS in production.
VariableDefaultDescription
LOG_LEVELinfo (production) / debug (dev)Minimum server log level. Accepts: trace, debug, info, warn, error, fatal, silent. Set to info to suppress Prisma query logs.
NEXT_PUBLIC_LOG_LEVELwarn (production) / debug (dev)Minimum browser log level. Accepts: debug, info, warn, error.

Production Docker images always output JSON to stdout (ready for CloudWatch / ELK). Colorized pretty output is only available in local development.

VariableDescription
SUPER_ADMIN_EMAILEmail for the super admin account (has access to the /admin panel).
SUPER_ADMIN_PASSWORD_HASHBcrypt hash of the super admin password. Generate with: node -e "console.log(require('bcryptjs').hashSync('your-password', 10))"

The container’s entrypoint runs the same sequence every time it starts:

  1. It checks NEXTAUTH_SECRET. If it is empty, or set to one of the placeholder values that used to appear in the shipped examples, the container reads or generates the secret in /app/data/.secret and logs what it did (never the value itself). An explicit secret of your own is used as-is.
  2. If DATABASE_URL is not set and no DB_* variables are provided, the entrypoint starts an embedded PGlite instance on an internal port (5433). When an external database is configured, PGlite is not started.
  3. It runs prisma migrate deploy to apply any pending database migrations.
  4. If the database is not ready, it retries every 10 seconds (up to 30 attempts, roughly five minutes).
  5. Once migrations succeed, the Next.js server starts on port 8637.

NEXTAUTH_SECRET signs Chorus’s own user_session (Default Auth) and admin_session (Super Admin) JWTs. OIDC tokens are signed by the identity provider, and agent API keys are validated separately. Rotating this secret invalidates Chorus-signed sessions; it does not rotate API keys or invalidate the provider’s OIDC tokens.

  • Explicit non-placeholder value: uses it without reading or writing a secret file.
  • Unset, empty, or a known public placeholder: reuses /app/data/.secret; if missing or empty, generates 32 random bytes encoded as 64 hexadecimal characters and saves them with mode 0600. A placeholder in the environment also produces a warning.
  • Unusable persisted secret: a non-regular or unreadable file, a persisted public placeholder, or a generation/write failure stops startup before migrations. An invalid persisted placeholder is never silently accepted or replaced.

The secret value is never logged. To retain an auto-generated secret, keep /app/data on a persistent volume. For multiple replicas, explicitly inject the same secret into every instance; the AWS CDK stack already shares it through Secrets Manager.

Generate a value once with openssl rand -base64 32, store it in your deployment’s secret store or protected .env, and reuse it on subsequent starts. Changing the effective value requires Default Auth users and the Super Admin to sign in again. Deleting .secret only rotates an auto-generated secret; it has no effect when an explicit secret is configured.

The Docker entrypoint and npm chorus launcher perform this bootstrap. Starting the app directly with next start or node server.js does not: provide a secure secret yourself. The app logs an error for a known placeholder, but that warning does not replace it; an unset secret prevents Chorus-signed session issuance.

  • Base image: node:22-alpine
  • Internal port: 8637
  • Architectures: linux/amd64, linux/arm64
  • Production deployment — running from the global npm package and the full AWS CDK walkthrough.
  • Operations — first-login bootstrap, deployment-side authentication, and backing up before you upgrade.