Chorus는 공식 Docker 이미지 `chorusaidlc/chorus-app`을 제공합니다. 이 가이드에서는 두 가지 Compose 구성(내장 데이터베이스를 쓰는 스탠드얼론, 외부 PostgreSQL과 Redis를 쓰는 프로덕션), 기존 데이터베이스에 연결하는 `docker run`, 전체 환경 변수 참고 정보, 그리고 컨테이너가 시작할 때 하는 일을 설명합니다.

먼저 이미지를 가져옵니다(pull).

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

스탠드얼론 구성과 프로덕션 구성 중 어느 것을 고를지는 [배포 개요](/ko/guides/deployment-overview/)를 참고하세요.

## 스탠드얼론(내장 데이터베이스)

외부 데이터베이스가 필요 없습니다. 이미지에 [PGlite](https://pglite.dev)(내장 PostgreSQL)가 포함되어 있으며 모든 구성 요소를 자동으로 시작합니다. 데이터는 Docker 볼륨에 저장되어 컨테이너를 재시작해도 유지됩니다.

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

그런 다음 실행합니다.

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

`http://localhost:8637`을 열고 `admin@example.com` / `changeme`로 로그인합니다(`DEFAULT_USER` / `DEFAULT_PASSWORD` 환경 변수로 바꿀 수 있습니다).

스탠드얼론 모드에서 컨테이너는 다음을 수행합니다.

- PGlite를 외부에 노출되지 않는 내부 포트(`5433`)에서 시작합니다.
- 데이터를 `chorus-local-data` Docker 볼륨에 저장하므로 재시작해도 유지됩니다.
- `NEXTAUTH_SECRET`을 비워 두면 첫 시작 시 무작위 세션 서명 비밀 키를 생성해 같은 볼륨에 저장합니다([세션 비밀 키](#세션-비밀-키nextauth_secret) 참고).
- Redis를 비활성화합니다(메모리 내 이벤트 전달로 대체되므로 단일 인스턴스 전용입니다).
- 시작 시 Prisma 마이그레이션을 자동으로 실행합니다.

## 프로덕션(외부 PostgreSQL + Redis)

프로덕션 환경, 특히 여러 레플리카를 둘 때는 앱을 외부 PostgreSQL과 Redis에 연결하세요. `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:
```

세션 비밀 키를 직접 지정하려면 시작하기 전에 한 번 생성해 compose 파일과 같은 위치의 `.env` 파일에 적어 둡니다.

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

그런 다음 실행합니다.

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

`http://localhost:8637`을 열고 `DEFAULT_USER` / `DEFAULT_PASSWORD`에 설정한 인증 정보로 로그인합니다.

:::caution[HTTPS와 보안 쿠키]
위의 Compose 파일은 HTTP 전용 배포(예: 내부 네트워크 테스트)를 지원하기 위해 `COOKIE_SECURE=false`를 설정합니다. 이 설정에서는 쿠키가 암호화되지 않은 HTTP로 전송되므로, 신뢰할 수 있는 내부 네트워크에서만 사용하세요. 프로덕션에서 HTTPS 뒤에 배포할 때는 보안 쿠키를 켜기 위해 `COOKIE_SECURE=true`를 설정하세요.
:::

## 기존 PostgreSQL에 연결해 실행하기

이미 PostgreSQL과 Redis가 실행 중이라면 컨테이너를 직접 시작합니다.

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

이 예제에서는 생성된 비밀 키가 `chorus-app-data`에 보관되므로 컨테이너를 다시 만들어도 유지됩니다.
직접 관리하려면 고정된 `NEXTAUTH_SECRET`을 주입하고 시작할 때마다 같은 값을 사용하세요.

## 환경 변수

### 데이터베이스와 세션 비밀 키

<div class="table-scroll" role="region" aria-label="필수 환경 변수" tabindex="0">

  | 변수 | 설명 |
  | --- | --- |
  | `DATABASE_URL` | PostgreSQL 연결 문자열. 형식: `postgresql://user:password@host:port/dbname`. 대신 개별 `DB_*` 변수를 설정할 수도 있습니다(아래 참고). **생략하면** 엔트리포인트가 내장 PGlite 인스턴스를 자동으로 시작합니다. |
  | `NEXTAUTH_SECRET` | 세션 토큰에 서명하기 위한 비밀 키. 무작위 문자열을 사용하세요(예: `openssl rand -base64 32`). **Docker에서는 비워 둘 수 있습니다.** 이 경우 컨테이너가 첫 시작 시 생성해 `/app/data/.secret`에 보관합니다. 앱 컨테이너를 여러 개 실행할 때는 모든 레플리카에 같은 값을 명시적으로 설정해야 합니다. [세션 비밀 키](#세션-비밀-키nextauth_secret)를 참고하세요. |

</div>

### 데이터베이스(`DATABASE_URL`의 대안)

`DATABASE_URL`이 설정되지 않은 경우, 엔트리포인트는 다음 개별 변수로 그것을 조립합니다.

<div class="table-scroll" role="region" aria-label="데이터베이스 환경 변수" tabindex="0">

  | 변수 | 설명 |
  | --- | --- |
  | `DB_HOST` | PostgreSQL 호스트 |
  | `DB_PORT` | PostgreSQL 포트(기본값: `5432`) |
  | `DB_USERNAME` | PostgreSQL 사용자 이름 |
  | `DB_PASSWORD` | PostgreSQL 비밀번호 |
  | `DB_NAME` | 데이터베이스 이름 |

</div>

### Redis

<div class="table-scroll" role="region" aria-label="Redis 환경 변수" tabindex="0">

  | 변수 | 설명 |
  | --- | --- |
  | `REDIS_URL` | 전체 Redis 연결 문자열. 형식: `redis://username:password@host:port`. 개별 변수보다 우선합니다. |
  | `REDIS_HOST` | Redis 호스트(`REDIS_URL`이 설정되지 않은 경우 사용) |
  | `REDIS_PORT` | Redis 포트(기본값: `6379`) |
  | `REDIS_USERNAME` | Redis 사용자 이름(기본값: `default`) |
  | `REDIS_PASSWORD` | Redis 비밀번호 |

</div>

### 인증

<div class="table-scroll" role="region" aria-label="인증 환경 변수" tabindex="0">

  | 변수 | 설명 |
  | --- | --- |
  | `DEFAULT_USER` | 내장 로그인용 이메일 주소(OIDC를 우회합니다). 첫 로그인 시 사용자와 해당 워크스페이스를 자동으로 생성합니다. |
  | `DEFAULT_PASSWORD` | 기본 사용자의 비밀번호(평문으로 지정하며, 런타임에 bcrypt로 비교합니다). |
  | `NEXTAUTH_URL` | 앱의 외부 공개 기본 URL(기본값: `http://localhost:8637`). 리버스 프록시 뒤에서 실행할 때 설정합니다. |
  | `COOKIE_SECURE` | HTTP 전용 배포에서 보안 쿠키를 끄려면 `"false"`로 설정합니다(docker-compose에서는 기본값 `"false"`). 프로덕션에서 HTTPS로 배포할 때는 `"true"`로 설정합니다. |

</div>

### 로그

<div class="table-scroll" role="region" aria-label="로그 환경 변수" tabindex="0">

  | 변수 | 기본값 | 설명 |
  | --- | --- | --- |
  | `LOG_LEVEL` | `info`(프로덕션) / `debug`(개발) | 서버 최소 로그 레벨. 허용 값: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`. Prisma 쿼리 로그를 억제하려면 `info`로 설정합니다. |
  | `NEXT_PUBLIC_LOG_LEVEL` | `warn`(프로덕션) / `debug`(개발) | 브라우저 최소 로그 레벨. 허용 값: `debug`, `info`, `warn`, `error`. |

</div>

프로덕션 Docker 이미지는 항상 JSON을 stdout으로 출력합니다(CloudWatch / ELK에 바로 연결 가능). 색상이 입혀진 보기 좋은 출력은 로컬 개발에서만 사용할 수 있습니다.

### 슈퍼 관리자

<div class="table-scroll" role="region" aria-label="슈퍼 관리자 환경 변수" tabindex="0">

  | 변수 | 설명 |
  | --- | --- |
  | `SUPER_ADMIN_EMAIL` | 슈퍼 관리자 계정의 이메일(`/admin` 패널에 접근할 수 있습니다). |
  | `SUPER_ADMIN_PASSWORD_HASH` | 슈퍼 관리자 비밀번호의 bcrypt 해시. 생성 방법: `node -e "console.log(require('bcryptjs').hashSync('your-password', 10))"` |

</div>

## 시작 시 동작

컨테이너의 엔트리포인트는 시작할 때마다 같은 순서를 실행합니다.

1. `NEXTAUTH_SECRET`을 확인합니다. 값이 비어 있거나 예전에 배포된 예제에 들어 있던 자리 표시자 값 중 하나이면, 컨테이너는 `/app/data/.secret`에서 비밀 키를 읽거나 새로 생성하고 무엇을 했는지 로그에 남깁니다(값 자체는 절대 출력하지 않습니다). 직접 명시적으로 설정한 비밀 키는 그대로 사용됩니다.
2. `DATABASE_URL`이 설정되지 않고 `DB_*` 변수도 하나도 제공되지 않으면, 엔트리포인트는 내부 포트(`5433`)에서 내장 PGlite 인스턴스를 시작합니다. 외부 데이터베이스가 **구성되어 있으면** PGlite는 시작하지 않습니다.
3. `prisma migrate deploy`를 실행해 보류 중인 데이터베이스 마이그레이션을 적용합니다.
4. 데이터베이스가 아직 준비되지 않았으면 10초마다 재시도합니다(최대 30회, 약 5분).
5. 마이그레이션이 성공하면 Next.js 서버가 `8637` 포트에서 시작합니다.

:::note[마이그레이션은 자동으로 실행됩니다]
마이그레이션을 직접 실행할 필요가 없습니다. 엔트리포인트가 시작할 때 보류 중인 마이그레이션을 적용하므로, 업그레이드는 같은 데이터베이스에 연결해 새 이미지를 시작하기만 하면 됩니다. 새 이미지는 첫 부팅 때 스키마를 마이그레이션할 수 있으므로 업그레이드 전에 데이터베이스나 데이터 볼륨을 백업하세요. [셀프 호스팅 Chorus 운영하기](/ko/guides/deploy-operations/)를 참고하세요.
:::

## 세션 비밀 키(`NEXTAUTH_SECRET`)

`NEXTAUTH_SECRET`은 Chorus가 직접 발급하는 로그인 세션(내장 이메일/비밀번호 로그인과 슈퍼 관리자 세션)에
서명합니다. OIDC 토큰은 ID 제공자가 서명하며, 에이전트 API 키는 별도로 검증됩니다.
이 비밀 키를 교체하면 Chorus가 서명한 세션만 무효화됩니다. API 키나 ID 제공자의 OIDC 토큰은 바뀌지 않습니다.

:::caution[0.18.0 이하에서 업그레이드]
배포에서 공개 자리 표시자 비밀 키를 사용했다면 0.18.1은 그 값을 무시하고 비공개 키를 재사용하거나 생성하므로,
해당 Chorus 서명 세션의 사용자는 다시 로그인해야 합니다. 명시적으로 지정한 안전한 키는 그대로
유지됩니다. 그 값을 계속 사용하면 사용자가 다시 로그인할 필요가 없습니다. CDK 재배포에서는
`nextAuthSecret`을 명시적으로 유지하세요. [CDK 비밀 키 설정](/ko/guides/deploy-production/)을 참고하세요.

오래된 Compose 파일에 `/app/data` 볼륨이 없으면 컨테이너를 재생성할 때마다 생성된 키가 사라집니다.
위의 볼륨을 추가하거나 영구적인 `NEXTAUTH_SECRET`을 설정하세요.
:::

### 컨테이너의 동작

- **명시적으로 설정한, 자리 표시자가 아닌 값**: 파일을 읽거나 쓰지 않고 그대로 사용합니다.
- **미설정, 빈 값, 알려진 공개 자리 표시자**: `/app/data/.secret`을 재사용합니다. 파일이 없거나 비어 있으면
  무작위 32바이트를 64자리 16진수로 생성해 `0600` 권한으로 저장합니다. 환경 변수의 자리 표시자에는 경고도 남깁니다.
- **사용할 수 없는 저장된 키**: 일반 파일이 아니거나 읽을 수 없는 경우, 공개 자리 표시자가 저장되어 있는 경우,
  또는 생성·쓰기에 실패한 경우에는 마이그레이션 전에 시작을 중단합니다. 저장된 잘못된 자리 표시자를 조용히 받아들이거나 교체하지 않습니다.

키 값은 로그에 출력하지 않습니다. 자동 생성된 키를 유지하려면 `/app/data`를 영구 볼륨에 두세요. 여러 레플리카에는 **같은** 키를
명시적으로 주입해야 합니다. [AWS CDK 스택](/ko/guides/deploy-production/)은 이미 Secrets Manager를 통해 키를 공유합니다.

### 비밀 키 설정과 교체

`openssl rand -base64 32`로 한 번 생성해 배포 환경의 비밀 저장소나 보호된 `.env`에 보관하고 이후 시작할 때 재사용하세요.
실제로 사용되는 키가 바뀌면 내장 이메일/비밀번호 로그인 사용자와 슈퍼 관리자가 다시 로그인해야 합니다. `.secret`을 삭제하면 자동 생성된
키만 교체되며, 명시적으로 설정한 키가 있으면 아무 영향이 없습니다.

이 자동 생성은 Docker 엔트리포인트와 npm `chorus` 런처가 수행합니다. `next start`나 `node server.js`로
직접 실행한다면 안전한 값을 직접 설정해야 합니다. 앱은 알려진 자리 표시자를 오류 로그에 기록하지만
교체하지는 않습니다. 키가 없으면 Chorus는 자체 로그인 세션을 발급할 수 없습니다.

## 이미지 세부 정보

- **기본 이미지:** `node:22-alpine`
- **내부 포트:** `8637`
- **아키텍처:** `linux/amd64`, `linux/arm64`

## 다음 단계

- **[프로덕션 배포](/ko/guides/deploy-production/)**: 글로벌 npm 패키지에서 실행하기와
  전체 AWS CDK 안내.
- **[셀프 호스팅 Chorus 운영하기](/ko/guides/deploy-operations/)**: 첫 로그인 부트스트랩, 배포 측 인증, 그리고
  업그레이드 전 백업.
