Chorus ships an official Docker image, **`chorusaidlc/chorus-app`**. This guide covers two
Compose setups (standalone with an embedded database, and production with external PostgreSQL
and 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:

```bash
docker pull chorusaidlc/chorus-app:latest
```

For help choosing between the standalone and production setups, see the
[Deployment overview](/guides/deployment-overview/).

## Standalone (embedded database)

No external database needed. The image bundles [PGlite](https://pglite.dev) (embedded
PostgreSQL) and starts everything automatically. Data persists in a Docker volume across
container restarts.

Create a `docker-compose.local.yml`:

```yaml
# 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:

```bash
docker compose -f docker-compose.local.yml up -d
```

Open `http://localhost:8637` and sign 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](#session-secret-nextauth_secret)).
- Disables Redis (falls back to in-memory event delivery — single-instance only).
- Runs Prisma migrations automatically on startup.

## Production (external PostgreSQL + Redis)

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

```yaml
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:

```bash
echo "NEXTAUTH_SECRET=$(openssl rand -base64 32)" >> .env
```

Then run:

```bash
docker compose up -d
```

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

:::caution[HTTPS and secure cookies]
The Compose files above set `COOKIE_SECURE=false` to support HTTP-only deployments (for
example, internal-network testing). Only use `COOKIE_SECURE=false` on a trusted internal
network — cookies are then sent over unencrypted HTTP. When you deploy behind HTTPS in
production, set `COOKIE_SECURE=true` to enable secure cookies.
:::

## Run against an existing PostgreSQL

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

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

## Environment variables

### 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](#session-secret-nextauth_secret). |

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

### Redis

| 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

| Variable | Description |
| --- | --- |
| `DEFAULT_USER` | Email address for built-in login (bypasses OIDC). Automatically creates the user and their workspace on first sign-in. |
| `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

| 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

| 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

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

:::note[Migrations run automatically]
You do not run migrations yourself. Applying pending migrations at startup is part of the
entrypoint, so upgrading is just starting a newer image against the same database. Because a
new image can migrate your schema on first boot, back up the database or data volume before
upgrading — see [Operate a self-hosted Chorus](/guides/deploy-operations/).
:::

## Session secret (`NEXTAUTH_SECRET`)

`NEXTAUTH_SECRET` signs the sign-in sessions Chorus issues itself: built-in email/password
logins and Super Admin sessions. OIDC tokens are signed by the identity provider, and agent
API keys are validated separately. Rotating this secret signs out Chorus-issued sessions; it
does not rotate API keys or invalidate the provider's OIDC tokens.

:::caution[Upgrading from 0.18.0 or earlier]
If your deployment used a public placeholder secret, 0.18.1 ignores that value and reuses
or generates a private secret, so users of the affected Chorus-signed sessions must sign in
again. An explicitly supplied secure secret is preserved; keep that value and users will not
have to sign in again. For CDK redeployments, explicitly preserve
`nextAuthSecret`; see [CDK secret configuration](/guides/deploy-production/).

An old Compose file without an `/app/data` volume loses the generated secret whenever the
container is recreated. Add the volume shown above or supply a persistent `NEXTAUTH_SECRET`.
:::

### 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 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](/guides/deploy-production/) already shares it through
Secrets Manager.

### 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
signs out users of the built-in email/password login and the Super Admin; they must 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 does not replace it; with no secret set,
Chorus cannot issue its own sign-in sessions.

## Image details

- **Base image:** `node:22-alpine`
- **Internal port:** `8637`
- **Architectures:** `linux/amd64`, `linux/arm64`

## Next steps

- **[Deploy in production](/guides/deploy-production/)** — running from the global npm package
  and the full AWS CDK walkthrough.
- **[Operate a self-hosted Chorus](/guides/deploy-operations/)** — first-login bootstrap, deployment-side
  authentication, and backing up before you upgrade.
