6dcbb45eb9
브라우저가 프론트와 API 를 서로 다른 도메인으로 호출하던 구조를 같은 도메인의 /api 경로로 통합한다. 운영은 Nginx Proxy Manager 의 Proxy Host(/ → frontend:8080)와 Custom location(/api → backend:3000)이, 개발은 vite dev 프록시가 같은 규칙으로 백엔드에 넘긴다. - 같은 출처 호출이라 CORS 설정이 필요 없고 CSP connect-src 'self' 가 그대로 동작한다 (기존 예시대로 별도 도메인에 배포하면 CSP 에 막혀 API 호출이 실패했다) - 프론트 번들의 baseURL 이 /api 상대경로라 도메인이 바뀌어도 다시 빌드할 필요가 없다 - NPM 설정 절차와 주의점(Cache Assets 끄기, proxy 네트워크 생성)을 README "운영 배포"에 정리했다 - 프록시 설정이 빠지면 frontend nginx 가 /api 를 404 로 막는다 — index.html(200)로 조용히 실패하지 않게 함께 고친 CORS 문제: - CORS_ORIGIN 에 콤마로 여러 오리진을 적으면 문자열 전체를 하나로 비교해 모두 막히던 버그 - 운영에서도 localhost:5173·localhost:3000 이 credentials 포함으로 항상 허용되던 문제 - 형식이 잘못된 오리진(경로 포함 등)은 기동 시점에 실패하도록 검증 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
186 lines
6.6 KiB
YAML
186 lines
6.6 KiB
YAML
# ==========================================
|
|
# 운영 라우팅 — 프론트와 API 는 같은 도메인을 쓴다
|
|
#
|
|
# https://<도메인>/ → frontend:8080 (Nginx Proxy Manager Proxy Host 의 기본 대상)
|
|
# https://<도메인>/api/... → backend:${BACKEND_PORT} (같은 Proxy Host 의 Custom location /api)
|
|
#
|
|
# 브라우저는 /api 상대경로로만 호출하므로 CORS 가 필요 없다.
|
|
# NPM 은 아래 proxy 외부 네트워크로 컨테이너 이름(${PROJECT_NAME}-frontend / -backend)에 접근한다.
|
|
# 설정 절차는 README "운영 배포" 참조.
|
|
# ==========================================
|
|
services:
|
|
# ----------------------------------------
|
|
# 1. Frontend (Nginx 정적 호스팅) — NPM Proxy Host 의 기본 대상(/)
|
|
# ----------------------------------------
|
|
frontend:
|
|
container_name: ${PROJECT_NAME}-frontend
|
|
build:
|
|
context: ./${FRONTEND_DIR}
|
|
dockerfile: Dockerfile
|
|
args:
|
|
- BUILD_MODE=${BUILD_MODE}
|
|
# 같은 도메인 구성이면 /api 그대로 — 도메인이 바뀌어도 이미지를 다시 빌드할 필요가 없다
|
|
- VITE_API_BASE_URL=${BACKEND_API_URL:-/api}
|
|
expose:
|
|
- "8080"
|
|
environment:
|
|
- NODE_ENV=${APP_ENV}
|
|
restart: unless-stopped
|
|
networks:
|
|
- app-network
|
|
- proxy
|
|
depends_on:
|
|
- backend
|
|
healthcheck:
|
|
# localhost 는 컨테이너 안에서 ::1 로 먼저 해석되는데 nginx 는 IPv4 만 바인딩하므로 127.0.0.1 을 명시한다
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/ || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 512m
|
|
|
|
# ----------------------------------------
|
|
# 2. Backend (HTTP API & WebSocket 통합) — NPM Custom location /api 의 대상
|
|
# ----------------------------------------
|
|
backend:
|
|
container_name: ${PROJECT_NAME}-backend
|
|
build:
|
|
context: ./${BACKEND_DIR}
|
|
dockerfile: Dockerfile
|
|
expose:
|
|
- "${BACKEND_PORT}"
|
|
- "${WEBSOCKET_PORT}"
|
|
# PID 1 을 tini 로 두어 SIGTERM 이 애플리케이션까지 확실히 전달되게 한다 (graceful shutdown)
|
|
init: true
|
|
# 처리 중인 요청을 마칠 시간 — 초과하면 SIGKILL (기본 10초)
|
|
stop_grace_period: 30s
|
|
env_file:
|
|
- ./${BACKEND_DIR}/.env.${APP_ENV}
|
|
environment:
|
|
- NODE_ENV=${APP_ENV}
|
|
- PORT=${BACKEND_PORT}
|
|
- WEBSOCKET_PORT=${WEBSOCKET_PORT}
|
|
# 같은 도메인 구성에서는 비어 있다 — 다른 오리진에서 호출할 때만 콤마로 나열
|
|
- CORS_ORIGIN=${CORS_ORIGIN:-}
|
|
# 앱이 직접 쓰지는 않는다. API 를 별도 도메인으로 분리했을 때 로컬 주소가 남은 채
|
|
# 운영에 배포되는 것을 기동 시점에 잡기 위해 검증용으로 주입한다 (shared/config/env.validation.ts)
|
|
- BACKEND_API_URL=${BACKEND_API_URL:-/api}
|
|
# 위 공개 주소 검사를 건너뛰는 탈출구 — 로컬에서 운영 이미지를 검증할 때만 true
|
|
- ALLOW_LOCALHOST_URL=${ALLOW_LOCALHOST_URL:-}
|
|
- THROTTLE_TTL_MS=${THROTTLE_TTL_MS:-60000}
|
|
- THROTTLE_LIMIT=${THROTTLE_LIMIT:-100}
|
|
- DB_HOST=db
|
|
- DB_PORT=5432
|
|
- DB_USER=${DB_USER}
|
|
- DB_PASSWORD=${DB_PASSWORD}
|
|
- DB_NAME=${DB_NAME}
|
|
- DB_SYNCHRONIZE=${DB_SYNCHRONIZE:-false}
|
|
- UPLOAD_DIR=/app/uploads
|
|
- UPLOAD_MAX_SIZE_MB=${UPLOAD_MAX_SIZE_MB:-5}
|
|
- REDIS_HOST=redis
|
|
- REDIS_PORT=6379
|
|
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
# 업로드 파일은 컨테이너 재생성과 무관하게 보존되어야 한다
|
|
volumes:
|
|
- ${UPLOAD_DATA_DIR:-./data/uploads}:/app/uploads
|
|
restart: unless-stopped
|
|
networks:
|
|
- app-network
|
|
- proxy
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
# redis 는 profile 로 비활성화될 수 있으므로 선택적 의존으로 둔다
|
|
redis:
|
|
condition: service_healthy
|
|
required: false
|
|
healthcheck:
|
|
# localhost 는 컨테이너 안에서 ::1 로 먼저 해석되는데 Node 는 0.0.0.0(IPv4) 바인딩이므로 127.0.0.1 을 명시한다
|
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:${BACKEND_PORT}/ || exit 1"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 30s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '1.0'
|
|
memory: 1g
|
|
|
|
# ----------------------------------------
|
|
# 3. Database (PostgreSQL) — 내부 네트워크 전용
|
|
# ----------------------------------------
|
|
db:
|
|
container_name: ${PROJECT_NAME}-db
|
|
image: ${DB_IMAGE}
|
|
expose:
|
|
- "5432"
|
|
environment:
|
|
POSTGRES_USER: ${DB_USER}
|
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
POSTGRES_DB: ${DB_NAME}
|
|
volumes:
|
|
- ${DB_DATA_DIR}:/var/lib/postgresql/data
|
|
restart: always
|
|
networks:
|
|
- app-network
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 10s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.5'
|
|
memory: 512m
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
|
|
# ----------------------------------------
|
|
# 4. Redis — 내부 네트워크 전용 · 기본 비활성
|
|
#
|
|
# 기본적으로 기동하지 않는다. 캐시/세션 공유가 실제로 필요해질 때 켠다.
|
|
# 루트 .env.{APP_ENV} 에서 COMPOSE_PROFILES=redis
|
|
# 또는 일회성으로 docker compose --profile redis up -d
|
|
# ----------------------------------------
|
|
redis:
|
|
profiles: ["redis"]
|
|
container_name: ${PROJECT_NAME}-redis
|
|
image: ${REDIS_IMAGE}
|
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
|
expose:
|
|
- "6379"
|
|
volumes:
|
|
- ${REDIS_DATA_DIR}:/data
|
|
restart: always
|
|
networks:
|
|
- app-network
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep PONG"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 5s
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '0.25'
|
|
memory: 256m
|
|
labels:
|
|
- "com.centurylinklabs.watchtower.enable=true"
|
|
|
|
networks:
|
|
app-network:
|
|
driver: bridge
|
|
# 리버스 프록시(Nginx Proxy Manager)와 공유하는 외부 네트워크 — frontend·backend 만 붙는다 (db·redis 는 내부 전용)
|
|
# compose 가 만들지 않으므로 서버에서 한 번 만들어 두어야 기동한다: docker network create proxy
|
|
# NPM 은 컨테이너 이름으로 찾아오므로 같은 서버의 프로젝트끼리 PROJECT_NAME 이 겹치면 안 된다.
|
|
proxy:
|
|
external: true |