NAS 정식 스택과 배포 파이프라인 도구를 고정
This commit is contained in:
parent
f1b80676c1
commit
d6d9dc5f61
8 changed files with 812 additions and 1 deletions
308
infra/docker-compose.nas.yml
Normal file
308
infra/docker-compose.nas.yml
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
name: vignette-prod
|
||||
|
||||
# NAS 운영 전용 완결 스택이다. infra/docker-compose.yml의 로컬 개발 스택과 합치지 않는다.
|
||||
# 외부 볼륨은 백업/격리 복구/휴먼 게이트를 통과한 뒤 운영자가 미리 생성해야 한다.
|
||||
|
||||
x-api-image: &api-image
|
||||
image: ${VIGNETTE_NAS_API_IMAGE:?Immutable NAS API image reference is required}
|
||||
|
||||
x-web-image: &web-image
|
||||
image: ${VIGNETTE_NAS_WEB_IMAGE:?Immutable NAS web image reference is required}
|
||||
|
||||
x-production-labels: &production-labels
|
||||
com.vignette.owner: vignette
|
||||
com.vignette.runtime-class: production
|
||||
com.vignette.ssot-host: nas
|
||||
com.vignette.stack: vignette-prod
|
||||
|
||||
services:
|
||||
db:
|
||||
image: ${VIGNETTE_NAS_POSTGRES_IMAGE:?Immutable NAS PostgreSQL image ID is required}
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-vignette_owner}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?NAS PostgreSQL owner password is required}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-vignette}
|
||||
APP_DB_USER: ${APP_DB_USER:-vignette_app}
|
||||
APP_DB_PASSWORD: ${APP_DB_PASSWORD:?NAS application DB password is required}
|
||||
volumes:
|
||||
- type: volume
|
||||
source: pgdata
|
||||
target: /var/lib/postgresql/data
|
||||
- type: bind
|
||||
source: ./db/init
|
||||
target: /docker-entrypoint-initdb.d
|
||||
read_only: true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- >-
|
||||
PGPASSWORD=$${APP_DB_PASSWORD} psql -U $${APP_DB_USER} -d $${POSTGRES_DB}
|
||||
-tAc "SELECT to_regclass('app.ci_regression_dag_node') IS NOT NULL"
|
||||
| grep -qx t
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 5m
|
||||
labels:
|
||||
<<: *production-labels
|
||||
com.vignette.data-role: database-ssot
|
||||
networks: [backend]
|
||||
|
||||
engine:
|
||||
<<: *api-image
|
||||
command:
|
||||
- python
|
||||
- -m
|
||||
- uvicorn
|
||||
- engine_gateway.gateway:app
|
||||
- --host
|
||||
- 0.0.0.0
|
||||
- --port
|
||||
- "9099"
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 30s
|
||||
environment:
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:?OpenAI key is required for the NAS engine}
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
||||
OPENAI_ENGINE_MODEL: ${OPENAI_ENGINE_MODEL:?An approved OpenAI engine model is required}
|
||||
OPENAI_ENGINE_MODELS: ${OPENAI_ENGINE_MODELS:-gpt-5.6-terra,gpt-5.6-luna,gpt-4.1}
|
||||
ENGINE_GATEWAY_SHARED_SECRET: ${ENGINE_GATEWAY_SHARED_SECRET:?Gateway shared secret is required}
|
||||
ENGINE_CAPABILITY_CACHE_TTL_SECONDS: ${ENGINE_CAPABILITY_CACHE_TTL_SECONDS:-1800}
|
||||
ENGINE_READY_TTL_SECONDS: ${ENGINE_READY_TTL_SECONDS:-1800}
|
||||
ENGINE_READY_FAILURE_TTL_SECONDS: ${ENGINE_READY_FAILURE_TTL_SECONDS:-30}
|
||||
ENGINE_READY_TIMEOUT_SECONDS: ${ENGINE_READY_TIMEOUT_SECONDS:-45}
|
||||
ENGINE_GENERATE_TIMEOUT_SECONDS: ${ENGINE_GENERATE_TIMEOUT_SECONDS:-300}
|
||||
expose:
|
||||
- "9099"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=128m
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- python
|
||||
- -c
|
||||
- >-
|
||||
import json, urllib.request;
|
||||
payload=json.load(urllib.request.urlopen('http://localhost:9099/health'));
|
||||
assert payload.get('ok') is True
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 8
|
||||
start_period: 30s
|
||||
labels:
|
||||
<<: *production-labels
|
||||
com.vignette.data-role: engine-ssot
|
||||
networks: [backend, app-egress]
|
||||
|
||||
api:
|
||||
<<: *api-image
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
# NAS 호스트 터널만 접근할 수 있도록 loopback에만 고정 바인딩한다.
|
||||
ports:
|
||||
- "127.0.0.1:18080:8000"
|
||||
depends_on:
|
||||
db: { condition: service_healthy }
|
||||
engine: { condition: service_healthy }
|
||||
environment:
|
||||
ENVIRONMENT: prod
|
||||
DATABASE_URL: postgresql://${APP_DB_USER:-vignette_app}:${APP_DB_PASSWORD:?NAS application DB password is required}@db:5432/${POSTGRES_DB:-vignette}
|
||||
ENGINE_URL: http://engine:9099
|
||||
ENGINE_MODE: openai
|
||||
VIGNETTE_LIVE_CLIENT_PROVIDER: openai
|
||||
ENGINE_GATEWAY_SHARED_SECRET: ${ENGINE_GATEWAY_SHARED_SECRET:?Gateway shared secret is required}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:?OpenAI key is required for production voice and engine paths}
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
||||
VIGNETTE_VOICE_STT_PROVIDER: openai
|
||||
VIGNETTE_VOICE_TTS_PROVIDER: openai
|
||||
SESSION_SECRET: ${SESSION_SECRET:?Production session secret is required}
|
||||
VIGNETTE_RUPTURE_INTERNAL_TOKEN: ${VIGNETTE_RUPTURE_INTERNAL_TOKEN:?G3 token is required}
|
||||
VIGNETTE_PRACTICE_INTERNAL_TOKEN: ${VIGNETTE_PRACTICE_INTERNAL_TOKEN:?G4 token is required}
|
||||
VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN: ${VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN:?G5 token is required}
|
||||
VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN: ${VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN:?G6 token is required}
|
||||
VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN: ${VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN:?G7 token is required}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN:?G8 token is required}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED:-false}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_INTERVAL_SECONDS: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_INTERVAL_SECONDS:-3600}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_STARTUP_DELAY_SECONDS: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_STARTUP_DELAY_SECONDS:-30}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_RETRY_DELAY_SECONDS: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_RETRY_DELAY_SECONDS:-300}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_LEASE_TIMEOUT_SECONDS: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_LEASE_TIMEOUT_SECONDS:-1800}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENGINE_TIMEOUT_SECONDS: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENGINE_TIMEOUT_SECONDS:-300}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_BATCH_SIZE: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_BATCH_SIZE:-1}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED: ${VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED:-false}
|
||||
VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_ENABLED: "false"
|
||||
OAUTH_GOOGLE_CLIENT_ID: ${OAUTH_GOOGLE_CLIENT_ID:?Google OAuth client id is required}
|
||||
OAUTH_GOOGLE_CLIENT_SECRET: ${OAUTH_GOOGLE_CLIENT_SECRET:?Google OAuth client secret is required}
|
||||
OAUTH_REDIRECT_URI: ${OAUTH_REDIRECT_URI:-https://api-vignette.chanpaca.net/auth/callback}
|
||||
AUTH_ALLOWED_EMAIL_DOMAINS: '${AUTH_ALLOWED_EMAIL_DOMAINS:-["hs.ac.kr","twentyoz.kr"]}'
|
||||
AUTH_TEACHER_EMAILS: '${AUTH_TEACHER_EMAILS:-[]}'
|
||||
AUTH_ADMIN_EMAILS: '${AUTH_ADMIN_EMAILS:-[]}'
|
||||
AUTH_SUPER_ADMIN_EMAILS: '${AUTH_SUPER_ADMIN_EMAILS:?Super admin allowlist is required}'
|
||||
AUTH_APPROVED_EMAILS: '${AUTH_APPROVED_EMAILS:-[]}'
|
||||
AUTH_NEW_USER_DEFAULT_STATUS: pending
|
||||
AUTH_DEV_LOGIN_ENABLED: "false"
|
||||
AUTH_DEV_LOGIN_EXTRA_ORIGINS: '[]'
|
||||
DEFAULT_AFFILIATION: ${DEFAULT_AFFILIATION:-}
|
||||
AUTO_SEED_PERSONAS: "false"
|
||||
ALLOW_SEED_PERSONA_FALLBACK: "false"
|
||||
EVALUATOR_GOLDEN_FEWSHOT_ENABLED: ${EVALUATOR_GOLDEN_FEWSHOT_ENABLED:-false}
|
||||
FRONTEND_BASE_URL: https://vignette.chanpaca.net
|
||||
FRONTEND_ORIGIN_MAP: '${FRONTEND_ORIGIN_MAP:?Set FRONTEND_ORIGIN_MAP in the NAS secret environment}'
|
||||
CORS_ORIGINS: '${CORS_ORIGINS:-["https://vignette.chanpaca.net","https://vignette-b1q.pages.dev"]}'
|
||||
USER_UPLOAD_DIR: /app/uploads
|
||||
NOTIFICATION_EMAIL_PROVIDER: ${NOTIFICATION_EMAIL_PROVIDER:-disabled}
|
||||
NOTIFICATION_EMAIL_MAX_ATTEMPTS: ${NOTIFICATION_EMAIL_MAX_ATTEMPTS:-3}
|
||||
NOTIFICATION_EMAIL_RETRY_SECONDS: ${NOTIFICATION_EMAIL_RETRY_SECONDS:-900}
|
||||
SMTP_HOST: ${SMTP_HOST:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
|
||||
SMTP_FROM_NAME: ${SMTP_FROM_NAME:-Vignette}
|
||||
SMTP_STARTTLS: ${SMTP_STARTTLS:-true}
|
||||
SMTP_SSL: ${SMTP_SSL:-false}
|
||||
VIGNETTE_RUNTIME_CLASS: production
|
||||
VIGNETTE_SSOT_HOST: nas
|
||||
VIGNETTE_NAS_PGDATA_VOLUME: ${VIGNETTE_NAS_PGDATA_VOLUME:?Explicit NAS PostgreSQL volume name is required}
|
||||
VIGNETTE_NAS_APIUPLOADS_VOLUME: ${VIGNETTE_NAS_APIUPLOADS_VOLUME:?Explicit NAS uploads volume name is required}
|
||||
volumes:
|
||||
- type: volume
|
||||
source: apiuploads
|
||||
target: /app/uploads
|
||||
volume:
|
||||
nocopy: true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=128m
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- python
|
||||
- -c
|
||||
- >-
|
||||
import json, urllib.request;
|
||||
payload=json.load(urllib.request.urlopen('http://localhost:8000/health'));
|
||||
assert payload.get('status') == 'ok' and payload.get('db') is True and payload.get('engine') is True
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 8
|
||||
start_period: 2m
|
||||
labels:
|
||||
<<: *production-labels
|
||||
com.vignette.data-role: api-ssot
|
||||
networks: [backend, app-egress]
|
||||
|
||||
web:
|
||||
<<: *web-image
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
api: { condition: service_healthy }
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=16m
|
||||
# nginx가 client/proxy/fastcgi 임시 파일과 PID를 기록해야 한다.
|
||||
# 루트 FS는 계속 read-only로 유지한다.
|
||||
- /var/cache/nginx:rw,noexec,nosuid,size=16m
|
||||
- /var/run:rw,noexec,nosuid,size=1m
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
labels:
|
||||
<<: *production-labels
|
||||
com.vignette.data-role: static-web-replica
|
||||
networks: [backend]
|
||||
|
||||
private-proxy:
|
||||
image: caddy@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
web: { condition: service_started }
|
||||
api: { condition: service_healthy }
|
||||
ports:
|
||||
- "${VIGNETTE_NAS_PRIVATE_BIND_ADDRESS:?Explicit NAS Tailnet or private bind address is required}:${VIGNETTE_NAS_PRIVATE_HTTP_PORT:-18088}:80"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./Caddyfile.nas
|
||||
target: /etc/caddy/Caddyfile
|
||||
read_only: true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /data:rw,noexec,nosuid,size=16m
|
||||
- /config:rw,noexec,nosuid,size=16m
|
||||
- /tmp:rw,noexec,nosuid,size=16m
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
labels:
|
||||
<<: *production-labels
|
||||
com.vignette.data-role: tailnet-private-validation
|
||||
networks: [backend]
|
||||
|
||||
cloudflared:
|
||||
image: cloudflare/cloudflared@sha256:d9ff527dc19799e24d3689058161fb01e752a24be1823577bae54c9c2489275f
|
||||
profiles: [public-cutover-gate]
|
||||
command:
|
||||
- tunnel
|
||||
- --no-autoupdate
|
||||
- --config
|
||||
- /etc/cloudflared/config.yml
|
||||
- run
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
api: { condition: service_healthy }
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${VIGNETTE_TUNNEL_CONFIG_PATH:?Rendered cloudflared config path is required}
|
||||
target: /etc/cloudflared/config.yml
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ${VIGNETTE_TUNNEL_CREDENTIAL_PATH:?Tunnel credential JSON path is required}
|
||||
target: /run/secrets/vignette-tunnel.json
|
||||
read_only: true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp:rw,noexec,nosuid,size=32m
|
||||
cap_drop: [ALL]
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
labels:
|
||||
<<: *production-labels
|
||||
com.vignette.data-role: gated-public-edge
|
||||
# API는 NAS loopback에만 열리고, 터널은 호스트 네트워크로 이를 호출한다.
|
||||
# Docker 재시작 뒤에도 restart 정책으로 공개 연결을 자동 복구한다.
|
||||
network_mode: host
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
external: true
|
||||
name: ${VIGNETTE_NAS_PGDATA_VOLUME:?Explicit NAS PostgreSQL volume name is required}
|
||||
apiuploads:
|
||||
external: true
|
||||
name: ${VIGNETTE_NAS_APIUPLOADS_VOLUME:?Explicit NAS uploads volume name is required}
|
||||
|
||||
networks:
|
||||
backend:
|
||||
name: vignette-prod-backend
|
||||
driver: bridge
|
||||
internal: true
|
||||
labels:
|
||||
<<: *production-labels
|
||||
app-egress:
|
||||
name: vignette-prod-app-egress
|
||||
driver: bridge
|
||||
labels:
|
||||
<<: *production-labels
|
||||
tunnel-egress:
|
||||
name: vignette-prod-tunnel-egress
|
||||
driver: bridge
|
||||
labels:
|
||||
<<: *production-labels
|
||||
Loading…
Add table
Add a link
Reference in a new issue