e7c00e6f65
security.md 가 금지한다고 적어둔 것을 실제 설정이 어기고 있었다. - 프론트엔드 운영 컨테이너가 root 로 nginx 를 실행하고 있었다. nginx-unprivileged 로 전환하고 포트를 8080 으로 맞춤(비-root 는 1024 미만 바인드 불가). - dev 오버라이드가 사용하는 빌드 스테이지에도 USER node 적용. 익명 볼륨이 node 소유로 생성되도록 chown 을 USER 전환 앞에 둔다 (그렇지 않으면 vite 가 node_modules/.vite 캐시를 쓰지 못한다). - dev 의 DB/Redis 포트가 0.0.0.0 에 바인딩되어 같은 LAN 에 노출돼 있었다 → 127.0.0.1 로 한정. - CSP 헤더 추가. 대신 현대 브라우저에서 권장되지 않는 X-XSS-Protection 은 제거. - CORS 차단 시 Error 를 던져 500(SYS_001)이 되던 것을 헤더 미부여 방식으로 변경. - 헬스체크가 localhost 를 ::1 로 해석해 항상 실패하던 문제 수정(127.0.0.1 명시). - graceful shutdown 을 위해 init(tini)과 stop_grace_period 적용. - *.example 의 실제같은 비밀번호를 placeholder 로 교체하고, 비어 있던 서비스 env 예시에 안내를 채움. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
153 lines
4.1 KiB
YAML
153 lines
4.1 KiB
YAML
services:
|
|
# ----------------------------------------
|
|
# 1. Frontend (Nginx 정적 호스팅)
|
|
# ----------------------------------------
|
|
frontend:
|
|
container_name: ${PROJECT_NAME}-frontend
|
|
build:
|
|
context: ./${FRONTEND_DIR}
|
|
dockerfile: Dockerfile
|
|
args:
|
|
- BUILD_MODE=${BUILD_MODE}
|
|
- VITE_API_BASE_URL=${BACKEND_API_URL}
|
|
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 통합)
|
|
# ----------------------------------------
|
|
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}
|
|
- THROTTLE_TTL_MS=${THROTTLE_TTL_MS}
|
|
- THROTTLE_LIMIT=${THROTTLE_LIMIT}
|
|
- DB_HOST=db
|
|
- DB_PORT=5432
|
|
- DB_USER=${DB_USER}
|
|
- DB_PASSWORD=${DB_PASSWORD}
|
|
- DB_NAME=${DB_NAME}
|
|
- REDIS_HOST=redis
|
|
- REDIS_PORT=6379
|
|
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
restart: unless-stopped
|
|
networks:
|
|
- app-network
|
|
- proxy
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
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 — 내부 네트워크 전용
|
|
# ----------------------------------------
|
|
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
|
|
proxy:
|
|
external: true |