services: # PostgreSQL Database postgres: image: postgres:15-alpine container_name: autostock-postgres environment: TZ: Asia/Seoul POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data - ./docker/init-db.sql:/docker-entrypoint-initdb.d/init.sql ports: - "127.0.0.1:15433:5432" networks: - autostock-network healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 deploy: resources: limits: memory: 1G # Redis Cache redis: image: redis:7-alpine container_name: autostock-redis # [L-7 2026-10-02] allkeys-lru → volatile-lru: TTL 없는 상태 키(mdd:state·adaptive 이력)가 축출되면 # MDD peak/플래그가 재시작 없이 사라진다. 캐시 키·주문 락은 전부 TTL 이 있다(setex_json·_cache_set·set(ex=) 전수 확인). # 주의: TTL 키가 없는 상태로 200mb 에 닿으면 쓰기가 OOM 오류가 된다(현재 데이터 규모상 비현실적). command: redis-server --requirepass ${REDIS_PASSWORD:-autostock_redis_change_me} --maxmemory 200mb --maxmemory-policy volatile-lru environment: TZ: Asia/Seoul ports: - "127.0.0.1:6379:6379" volumes: - redis_data:/data networks: - autostock-network healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-autostock_redis_change_me}", "ping"] interval: 10s timeout: 3s retries: 5 deploy: resources: limits: memory: 256M # RabbitMQ Message Queue rabbitmq: image: rabbitmq:3-management container_name: autostock-rabbitmq environment: TZ: Asia/Seoul RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} ports: - "127.0.0.1:5672:5672" - "127.0.0.1:15672:15672" volumes: - rabbitmq_data:/var/lib/rabbitmq # - rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf networks: - autostock-network healthcheck: test: rabbitmq-diagnostics -q ping interval: 30s timeout: 10s retries: 5 deploy: resources: limits: memory: 512M # MCP Server mcp-server: build: context: ./mcp-server dockerfile: Dockerfile container_name: autostock-mcp-server env_file: - .env environment: - TZ=Asia/Seoul ports: - "127.0.0.1:8001:8001" networks: - autostock-network depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped deploy: resources: limits: memory: 256M # Backend API Server backend: build: context: ./backend dockerfile: Dockerfile container_name: autostock-backend # [H-5 2026-10-02] 기본 10초 < db_worker 종료 drain 예산(DRAIN_DEADLINE_SECONDS=20). # SIGTERM 후 10초에 SIGKILL 이 와서 drain 중인 DB 쓰기가 유실됐다. # [M2] 예산 = uvicorn graceful 5 + 백그라운드 대기 8 + drain 20 = 33초 (+여유) → 40s. backend/Dockerfile CMD 주석 참조. stop_grace_period: 40s env_file: - .env environment: - TZ=Asia/Seoul - MCP_SERVER_URL=http://mcp-server:8001 ports: - "127.0.0.1:8000:8000" volumes: - ./logs:/logs # KIS 액세스 토큰 캐시 — 컨테이너 재생성 때마다 사라지면 매 배포마다 토큰을 새로 # 발급받게 되고, KIS가 잦은 재발급을 거부한다(EGW00103 "유효하지 않은 AppKey"). - ./token_cache:/root/.cache/auto-stock networks: - autostock-network depends_on: postgres: condition: service_healthy redis: condition: service_healthy rabbitmq: condition: service_healthy mcp-server: condition: service_started # [R2 M] 프로세스 밖 생존 감시: 이벤트 루프 hang / uvicorn 무응답을 컨테이너 상태(unhealthy)로 노출. # 판정은 "10초 내 HTTP 200 응답"만 본다 — /health 의 status=degraded(DB 순간 장애·장외 stale)로 # unhealthy 가 되면 오탐이 잦다. degraded/schedulers.stale 감시는 앱 밖 cron 의 몫: # scripts/health_watch.py (평일 장중 5분 cron, 연속 이상 시 텔레그램) + .claude/scripts/prod_smoke.sh 의 /health 확인. # 위 cron 대신 compose 사이드카 health-watch 가 같은 스크립트를 돌린다(NAS cron 등록 불필요). # (plain compose 는 unhealthy 컨테이너를 재시작하지 않는다 — 경보·수동 확인용 표식) healthcheck: test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=10).status == 200 else 1)"] interval: 60s timeout: 15s retries: 3 start_period: 120s restart: unless-stopped deploy: resources: limits: memory: 768M # PostgreSQL 자동 백업 (매일 02:00 KST, 7일치 보관) postgres-backup: image: prodrigestivill/postgres-backup-local:15 container_name: autostock-postgres-backup environment: - TZ=Asia/Seoul - POSTGRES_HOST=postgres - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - SCHEDULE=0 8,17 * * * - BACKUP_KEEP_DAYS=7 - BACKUP_KEEP_WEEKS=4 - BACKUP_KEEP_MONTHS=3 - HEALTHCHECK_PORT=8080 volumes: - ./backups:/backups networks: - autostock-network depends_on: postgres: condition: service_healthy restart: unless-stopped # /health 능동 감시 사이드카 — NAS cron 등록 여부에 의존하지 않도록 compose 안에서 직접 돈다. # 평일(KST) 09~15시 5분 주기로 scripts/health_watch.py 실행(상태 전이 시 텔레그램). 장외는 stale 오탐 방지로 건너뜀. # 상태 파일은 named volume 에 유지(재기동해도 연속 이상 카운트 보존). # 마지막 실행 시각: `docker exec autostock-health-watch cat /state/last_run` (배포 스모크에서 mtime 확인용). health-watch: image: python:3.11-slim container_name: autostock-health-watch environment: - TZ=Asia/Seoul - HEALTH_URL=http://backend:8000/health - HEALTH_WATCH_STATE=/state/health_watch.json - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} - TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} volumes: - ./scripts/health_watch.py:/app/health_watch.py:ro - health_watch_state:/state command: - sh - -c - | while true; do k=$$(date -u -d '+9 hours' '+%u %H') dow=$${k% *}; hh=$${k#* }; hh=$${hh#0} if [ "$$dow" -le 5 ] && [ "$$hh" -ge 9 ] && [ "$$hh" -le 15 ]; then python /app/health_watch.py || true date -u '+%Y-%m-%dT%H:%M:%SZ' > /state/last_run fi sleep 300 done networks: - autostock-network depends_on: - backend restart: unless-stopped deploy: resources: limits: memory: 64M # Frontend frontend: build: context: ./frontend dockerfile: Dockerfile args: - VITE_API_URL=${VITE_API_URL} - VITE_WS_URL=${VITE_WS_URL} container_name: autostock-frontend environment: - TZ=Asia/Seoul ports: - "127.0.0.1:3001:80" networks: - autostock-network depends_on: - backend restart: unless-stopped deploy: resources: limits: memory: 64M volumes: postgres_data: redis_data: rabbitmq_data: backup_data: health_watch_state: networks: autostock-network: driver: bridge