# PI Continuum — 에어갭(폐쇄망) 배포용 Compose. # # 빌드 단계가 없다. 모든 이미지는 번들의 images/*.tar 를 먼저 적재해 쓴다. # # ./bin/load-images.sh # tar → 로컬 이미지 저장소 # cp .env.example .env # 값 확인·수정 # ./bin/up-compose.sh # docker compose (podman: up-podman-compose.sh) # ./bin/verify.sh # 헬스체크 + alpha-processing 연동 확인 # # 시작 순서: postgres/redis/minio → minio-init → bootstrap(migrate + seeding, 1회) # → web/worker/scheduler/alpha-processing → nginx # # 네트워크는 compose 가 만드는 기본 네트워크 하나다. 서비스끼리는 서비스 이름으로 찾는다. # 호스트로 여는 포트는 nginx 뿐이다. PostgreSQL·Redis·MinIO 를 호스트에 열려면 .env 의 # EXPOSE_*_PORT 를 채운다 — up-*.sh 가 compose.ports.yml 을 만들어 0.0.0.0 으로 붙인다. # 로그는 볼륨이 아니라 호스트 디렉터리(LOG_DIR, 기본 ./logs)에 쌓인다. # logs/app/ web·worker·scheduler·bootstrap (컨테이너 uid 10001) # logs/processing/ alpha-processing (컨테이너 uid 10001) name: ${COMPOSE_PROJECT_NAME:-pi-continuum} x-app: &app image: ${APP_IMAGE:-pi-continuum:76d67d4} pull_policy: never init: true env_file: .env environment: &app-env POSTGRESQL_IP: postgres POSTGRESQL_PORT: "5432" REDIS_CACHE_IP: redis REDIS_MAIN_IP: redis REDIS_MAIN_PORT: "6379" MINIO_ENDPOINT: minio:9000 MINIO_USE_SSL: "False" GRPC_PROCESSING_IP: alpha-processing GRPC_PROCESSING_PORT: "50051" # 브라우저가 접속하는 공개 주소. 백엔드가 실행 시점에 읽어 절대 URL·CSRF 신뢰 출처· # 로그인 쿠키 Secure 여부를 정한다. 이미지에 굽지 않으므로 바꿔도 재빌드하지 않는다. VITE_API_SERVER_URL: ${PUBLIC_URL:-http://localhost} volumes: - ${LOG_DIR:-./logs}/app:/app/__log x-infra-ready: &infra-ready postgres: condition: service_healthy redis: condition: service_healthy minio-init: condition: service_completed_successfully services: # 선택형 OpenAI 호환 게이트웨이. up-*.sh 가 LITELLM_ENABLED로 profile을 선택한다. litellm: profiles: ["litellm"] image: ${LITELLM_IMAGE:-ghcr.io/berriai/litellm:v1.101.0} pull_policy: never restart: unless-stopped command: ["--config", "/app/config.yaml", "--host", "0.0.0.0", "--port", "4000"] # 앱의 DB·스토리지 비밀값 대신 공급자 설정만 전달한다. 별도 DB는 사용하지 않는다. environment: LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:-} LITELLM_LOCAL_MODEL_COST_MAP: "True" LITELLM_TELEMETRY: "False" WATSONX_URL: ${WATSONX_URL:-} WATSONX_APIKEY: ${WATSONX_APIKEY:-} WATSONX_TOKEN: ${WATSONX_TOKEN:-} WATSONX_PROJECT_ID: ${WATSONX_PROJECT_ID:-} WATSONX_DEPLOYMENT_SPACE_ID: ${WATSONX_DEPLOYMENT_SPACE_ID:-} WATSONX_ZENAPIKEY: ${WATSONX_ZENAPIKEY:-} AWS_REGION_NAME: ${AWS_REGION_NAME:-us-east-1} AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-} AWS_SESSION_TOKEN: ${AWS_SESSION_TOKEN:-} AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK:-} # 사설 Bedrock runtime(VPC endpoint·PrivateLink 등). 비우면 공개 regional endpoint 를 쓴다. # SigV4 서명 region 은 AWS_REGION_NAME 을 그대로 쓰므로 실제 region 을 유지한다. AWS_BEDROCK_RUNTIME_ENDPOINT: ${AWS_BEDROCK_RUNTIME_ENDPOINT:-} # LiteLLM 전체 바깥 호출의 TLS 인증서 검증 여부. 비우면 true 로 본다. SSL_VERIFY: ${SSL_VERIFY:-true} volumes: - ./conf/litellm/config.yaml:/app/config.yaml:ro healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4000/health/liveliness', timeout=3).close()"] interval: 10s timeout: 5s retries: 20 start_period: 30s postgres: image: pgvector/pgvector:pg16 pull_policy: never restart: unless-stopped environment: POSTGRES_DB: ${POSTGRESQL_DB:?required} POSTGRES_USER: ${POSTGRESQL_ID:?required} POSTGRES_PASSWORD: ${POSTGRESQL_PWD:?required} command: ["postgres", "-c", "max_connections=300"] volumes: - postgres-data:/var/lib/postgresql/data # 데이터 볼륨이 처음 만들어질 때만 실행된다 — migration 은 pgvector 확장을 만들지 않는다. - ./conf/postgres/init.sql:/docker-entrypoint-initdb.d/10-init.sql:ro healthcheck: # 복구 중 대상 DB를 drop/recreate 해도 엔진의 주기 healthcheck가 그 DB에 새로 # 접속하지 않도록 항상 유지되는 maintenance DB를 확인한다. test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d postgres"] interval: 5s timeout: 5s retries: 20 redis: image: redis:8-alpine pull_policy: never restart: unless-stopped # AOF 는 보조 수단이다 — 잡의 정본은 DB manifest 이고 유실 시 scheduler 가 재배분한다. command: ["redis-server", "--appendonly", "yes"] volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 20 minio: image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z.hotfix.7aa24e772 pull_policy: never restart: unless-stopped command: ["server", "/data", "--console-address", ":9001"] environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?required} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?required} MINIO_REGION_NAME: ${MINIO_REGION:-us-east-1} volumes: - minio-data:/data healthcheck: test: ["CMD", "curl", "-f", "http://127.0.0.1:9000/minio/health/live"] interval: 5s timeout: 5s retries: 30 start_period: 10s # 버킷 생성 (1회성) minio-init: image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z pull_policy: never restart: "no" depends_on: minio: condition: service_healthy environment: MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?required} MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?required} entrypoint: ["/bin/sh", "-c"] command: - | set -eu mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" mc mb --ignore-existing local/pi-continuum # migrate + 기본 seeding (1회성). 앱 컨테이너는 이게 끝나야 뜬다. 모두 멱등이다. bootstrap: <<: *app restart: "no" command: ["bootstrap"] environment: <<: *app-env SKIP_MIGRATE: ${SKIP_MIGRATE:-false} SKIP_SEED: ${SKIP_SEED:-false} depends_on: *infra-ready web: <<: *app restart: unless-stopped scale: ${WEB_REPLICAS:-4} command: ["web"] depends_on: &app-ready <<: *infra-ready bootstrap: condition: service_completed_successfully volumes: - static-data:/app/static - media-data:/app/media - ${LOG_DIR:-./logs}/app:/app/__log # 현장에서 이미지의 특정 파일만 바꿔야 하면 여기에 파일 단위로 덮어쓴다. # 프롬프트는 worker·scheduler 도 읽으므로 같은 줄을 그쪽에도 넣는다. patches/README.md 참고. #- ./patches/prompts/interview_system.md:/app/prompts/interview_system.md:ro healthcheck: test: ["CMD", "python", "-c", "import http.client as h; c=h.HTTPConnection('127.0.0.1',8000,timeout=3); c.request('GET','/'); raise SystemExit(c.getresponse().status >= 400)"] interval: 10s timeout: 5s retries: 20 start_period: 30s # 문서 분석·합성 잡은 수십 분 걸린다. SIGTERM 을 받은 RQ 워커는 현재 잡을 끝낸 뒤 # 내려가므로(warm shutdown) 종료 유예를 길게 둔다. 당장 내리려면 `kill worker-default` # — 중단된 run 은 scheduler 가 30분 무응답 뒤 복구한다. worker-default: <<: *app restart: unless-stopped scale: ${RQ_DEFAULT_WORKERS:-1} stop_grace_period: 30m command: ["worker", "default", "--max-jobs", "100"] depends_on: *app-ready volumes: - media-data:/app/media - ${LOG_DIR:-./logs}/app:/app/__log worker-high: <<: *app restart: unless-stopped scale: ${RQ_HIGH_WORKERS:-1} stop_grace_period: 30m command: ["worker", "high", "--max-jobs", "100"] depends_on: *app-ready volumes: - media-data:/app/media - ${LOG_DIR:-./logs}/app:/app/__log worker-log: <<: *app restart: unless-stopped scale: ${RQ_LOG_WORKERS:-1} command: ["worker", "log", "--max-jobs", "500"] depends_on: *app-ready # 업무 문서 분석 reconciler — DB manifest 를 보고 RQ 에 배분·복구한다. # 정확히 1개만 떠야 한다. `--scale scheduler=N` 을 쓰지 말 것. scheduler: <<: *app restart: unless-stopped command: ["scheduler"] depends_on: *app-ready # 문서·URL 텍스트 추출 (Redis blob → gRPC). DB 를 쓰지 않으므로 POSTGRESQL_* 을 주지 않는다. # entrypoint 가 REDIS_MAIN_IP·REDIS_MAIN_PORT·VITE_OP_TYPE 셋을 강제하고, 나머지는 기본값이 있다. alpha-processing: image: ${PROCESSING_IMAGE:-alpha-processing:b11f26b} pull_policy: never restart: unless-stopped environment: REDIS_MAIN_IP: redis REDIS_MAIN_PORT: "6379" VITE_OP_TYPE: PRD GRPC_PORT: "50051" GRPC_MAX_WORKERS: "4" # 임베딩 ONNX 파일 선택 — 바꾸면 결과가 달라진다. EMBEDDING_ONNX_VARIANT: model EMBEDDING_ONNX_INTRA_THREADS: "4" # 폐쇄망에서 HuggingFace 로 나가지 않게 못 박는다 (이미지에도 같은 값이 있다). HF_HUB_OFFLINE: "1" TRANSFORMERS_OFFLINE: "1" shm_size: 1gb volumes: - ${LOG_DIR:-./logs}/processing:/www/alpha-processing/__log depends_on: redis: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import socket; socket.create_connection(('127.0.0.1', 50051), 3).close()"] interval: 10s timeout: 5s retries: 12 start_period: 60s # 정적 파일 + web 리버스 프록시. NGINX_CONF=https 면 :443 에서 TLS 를 종단한다. nginx: image: nginx:1.30-alpine pull_policy: never restart: unless-stopped depends_on: web: condition: service_healthy ports: - "${HTTP_PORT:-80}:80" - "${HTTPS_PORT:-443}:443" volumes: - ./conf/nginx/${NGINX_CONF:-http}.conf:/etc/nginx/conf.d/default.conf:ro - ./conf/nginx/app.conf:/etc/nginx/pi-continuum/app.conf:ro # docker 는 내장 DNS 로 web 을 계속 재해석하고, podman 은 기동 시 1회 해석한다. - ./conf/nginx/upstream/${NGINX_UPSTREAM:-docker}.conf:/etc/nginx/pi-continuum/upstream.conf:ro - ${NGINX_CERT_DIR:-./conf/nginx/certs}:/etc/nginx/certs:ro - static-data:/srv/static:ro volumes: postgres-data: redis-data: minio-data: static-data: media-data: