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= - 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:
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. - 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 - 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:
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 \ -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:latestEnvironment variables
Section titled “Environment variables”Required
Section titled “Required”| 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). |
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:
- 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.
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.