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=
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-chorus-local-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.
  • 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
- NEXTAUTH_SECRET=change-me-to-a-random-secret
- DEFAULT_USER=admin@example.com
- DEFAULT_PASSWORD=your-password
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-data:
redis-data:

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 \
-e NEXTAUTH_SECRET=change-me-to-a-random-secret \
-e COOKIE_SECURE=false \
-e DEFAULT_USER=admin@example.com \
-e DEFAULT_PASSWORD=your-password \
chorusaidlc/chorus-app:latest

| 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 JWT session tokens. Use a random string (for example, openssl rand -base64 32). |

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 |

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

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

| 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))" |

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

  1. 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.
  2. It runs prisma migrate deploy to apply any pending database migrations.
  3. If the database is not ready, it retries every 10 seconds (up to 30 attempts, roughly five minutes).
  4. Once migrations succeed, the Next.js server starts on port 8637.
  • 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.