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:
docker pull chorusaidlc/chorus-app:latestFor how to choose between the standalone and production forms, see the Deployment overview.
Standalone (embedded database)
Section titled “Standalone (embedded database)”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 Redisservices: 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:
docker compose -f docker-compose.local.yml up -dOpen 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-dataDocker volume, so it persists across restarts. - Generates a random session-signing secret on first start and stores it in the same volume
when
NEXTAUTH_SECRETis left empty (see Session secret). - Disables Redis (falls back to the in-memory EventBus — single-instance only).
- Runs Prisma migrations automatically on startup.
Production (external PostgreSQL + Redis)
Section titled “Production (external PostgreSQL + Redis)”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:
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .envThen run:
docker compose up -dOpen http://localhost:8637 and log in with the credentials you set in DEFAULT_USER /
DEFAULT_PASSWORD.
Run against an existing PostgreSQL
Section titled “Run against an existing PostgreSQL”If you already have PostgreSQL and Redis running, start the container directly:
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:latestThis 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.
Environment variables
Section titled “Environment variables”Database and session secret
Section titled “Database and session secret”| Variable | Description |
|---|---|
DATABASE_URL | PostgreSQL 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_SECRET | Secret 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. |
Database (alternative to DATABASE_URL)
Section titled “Database (alternative to DATABASE_URL)”If DATABASE_URL is not set, the entrypoint builds it from these individual variables:
| Variable | Description |
|---|---|
DB_HOST | PostgreSQL host |
DB_PORT | PostgreSQL port (default: 5432) |
DB_USERNAME | PostgreSQL username |
DB_PASSWORD | PostgreSQL password |
DB_NAME | Database name |
| Variable | Description |
|---|---|
REDIS_URL | Full Redis connection string. Format: redis://username:password@host:port. Takes precedence over the individual variables. |
REDIS_HOST | Redis host (used if REDIS_URL is not set) |
REDIS_PORT | Redis port (default: 6379) |
REDIS_USERNAME | Redis username (default: default) |
REDIS_PASSWORD | Redis password |
Authentication
Section titled “Authentication”| Variable | Description |
|---|---|
DEFAULT_USER | Email address for built-in login (bypasses OIDC). Auto-provisions the user and company on first login. |
DEFAULT_PASSWORD | Password for the default user (plain text, compared via bcrypt at runtime). |
NEXTAUTH_URL | Public-facing base URL of the app (default: http://localhost:8637). Set this when running behind a reverse proxy. |
COOKIE_SECURE | Set to "false" to disable secure cookies for HTTP-only deployments (default: "false" in docker-compose). Set to "true" when deploying with HTTPS in production. |
Logging
Section titled “Logging”| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | info (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_LEVEL | warn (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.
Super Admin
Section titled “Super Admin”| Variable | Description |
|---|---|
SUPER_ADMIN_EMAIL | Email for the super admin account (has access to the /admin panel). |
SUPER_ADMIN_PASSWORD_HASH | Bcrypt hash of the super admin password. Generate with: node -e "console.log(require('bcryptjs').hashSync('your-password', 10))" |
Startup behaviour
Section titled “Startup behaviour”The container’s entrypoint runs the same sequence every time it starts:
- 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/.secretand logs what it did (never the value itself). An explicit secret of your own is used as-is. - If
DATABASE_URLis not set and noDB_*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. - It runs
prisma migrate deployto apply any pending database migrations. - If the database is not ready, it retries every 10 seconds (up to 30 attempts, roughly five minutes).
- Once migrations succeed, the Next.js server starts on port
8637.
Session secret (NEXTAUTH_SECRET)
Section titled “Session secret (NEXTAUTH_SECRET)”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.
What the container does
Section titled “What the container does”- 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 mode0600. 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.
Setting and rotating the secret
Section titled “Setting and rotating the secret”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.
Image details
Section titled “Image details”- Base image:
node:22-alpine - Internal port:
8637 - Architectures:
linux/amd64,linux/arm64
Next steps
Section titled “Next steps”- 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.