Files
arasystem/docker-compose.dev.yml
T
ttipo 73babeaa2c feat(docker): 13단계 운영 기반 — 백업 · 감시 · 관리자 단단히 · 휴지통 30일 · 메일 프로그램 자동 설정
전 직원 전환 전에 서버가 스스로 지키고 되살릴 수 있게 했다.
- 관리자 단단히(setup): 관리자 역할에서 impersonate 를 빼고 관리자 허용 IP 를 앱 네트워크로 — 관리자 비밀번호로 바깥 993 · JMAP 에서 직원 메일함에 대리 로그인되던 길을 막았다.
  운영에서 처음 걸 때는 되돌릴 복구 관리자가 있을 때만 건다(관리자 잠김 방지).
- 휴지통 · 스팸 30일 정리를 KST 03:00 으로 명시. imapsync 로 옮긴 메일도 옮긴 때부터 센다(임시 엔진 실측).
- restic 백업(호스트 systemd 03:30 — 메일 볼륨 warm → 엔진 멈춤 9초 안팎 → final, pg_dump, 업로드, env) · 복원(새 서버 차례를 빈 클론에서 실측) · 15분 점검 · 부팅 되살리기, Healthchecks 하트비트.
- 메일 프로그램 자동 설정: frontend nginx 의 autoconfig · autodiscover 고정 응답(엔진 것은 POP3 를 알리고 입력을 되돌려 쓴다).
- 모든 서비스 docker 로그 보존, DB 이미지 판 · digest 고정.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:44:44 +09:00

183 lines
10 KiB
YAML

# 로그 보존 — docker-compose.yml 의 logging-default 와 같은 값 (앵커는 파일을 건너가지 않아 이 파일에만 있는 서비스 — mailpit — 에 다시 적는다)
x-logging: &logging-default
driver: json-file
options:
max-size: "10m"
max-file: "5"
compress: "true"
services:
# ----------------------------------------
# frontend — dev 에서는 기본 기동하지 않는다 (profile 로 숨김)
#
# vite 를 컨테이너에서 돌리면 소스가 Windows 바인드 마운트(./frontend:/app) 너머에 있어
# 수천 개 파일을 매 요청마다 가상 파일시스템으로 읽는다. 여기에 폴링 감시까지 겹쳐
# 첫 서빙까지 수 분이 걸린다. 호스트에서 직접 띄우면 같은 작업이 1~2초다.
#
# cd frontend && npm install && npm run dev # → http://localhost:5173
#
# vite dev 프록시가 /api 를 localhost:${BACKEND_PORT} 로 넘기므로 백엔드는 컨테이너 그대로 쓴다.
# 그래도 컨테이너로 띄워야 한다면 (호스트에 Node 를 깔 수 없는 환경 등):
# docker compose ... --profile frontend up -d
# 또는 루트 .env.development 에 COMPOSE_PROFILES=frontend
# ----------------------------------------
frontend:
profiles: ["frontend"]
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행)
restart: "no"
build:
target: build-stage
# 비-root 구동이라 1024 미만 포트를 쓸 수 없다 → vite 기본 포트 사용
command: npm run dev -- --host 0.0.0.0 --port 5173
# 호스트의 브라우저만 붙으면 된다 — 127.0.0.1 로 한정해 LAN 에 노출하지 않는다 (보안 규칙, 호스트 vite 와 같은 범위)
ports:
- "127.0.0.1:${FRONTEND_PORT}:5173"
# dev 는 nginx(8080)가 아니라 vite(5173)를 띄우므로 base 의 헬스체크를 덮어쓴다
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5173/ || exit 1"]
environment:
# 바인드 마운트 너머의 변경은 inotify 가 잡지 못하므로 컨테이너에서만 폴링을 켠다
# (vite.config.ts 가 이 값을 보고 watch.usePolling 을 결정한다 — 호스트 실행 시엔 꺼진다)
- CHOKIDAR_USEPOLLING=true
- WATCHPACK_POLLING=true
# vite dev 서버가 /api 요청을 백엔드 컨테이너로 프록시한다 (vite.config.ts)
- VITE_DEV_API_PROXY=http://backend:${BACKEND_PORT}
volumes:
- ./${FRONTEND_DIR}:/app
- /app/node_modules
# dev에서는 리버스 프록시 미사용 — base의 networks 목록을 덮어씀.
# 운영과 같이 edge 로만 백엔드에 닿는다 (app-network = 메일 엔진의 신뢰 대역에는 붙이지 않는다)
networks: !override
- edge
backend:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행)
restart: "no"
build:
target: builder
# /app/dist 는 아래 익명 볼륨이다. 그런데 nest-cli.json 의 deleteOutDir 는 출력 폴더를 통째로 지우려다
# 마운트 지점이라 EBUSY 로 기동에 실패한다 (검증함). 그래서 기동할 때 볼륨 "내용만" 비우고
# (= deleteOutDir 와 같은 효과 — 지운 소스의 옛 entity·migration 산출물이 TypeORM glob 에 잡히지 않게),
# deleteOutDir 만 끈 nest-cli.json 사본으로 같은 start:dev 를 돌린다. 원본 nest-cli.json 은 고치지 않는다.
#
# 마이그레이션은 컨테이너를 기동할 때 한 번만 적용한다 (dc down -v 등으로 DB 를 새로 만든 경우 대비).
# watch 재시작은 이 스크립트를 다시 돌지 않으므로, 생성만 하고 검토 전인 마이그레이션이 저장할 때마다 적용되지는 않는다.
# ⚠️ 다만 컨테이너를 다시 띄우면 그때까지 생긴 마이그레이션이 적용된다 — 검토를 마친 뒤 재기동한다.
# 실패하면 기동을 멈춘다 (마이그레이션이 덜 된 DB 로 앱을 띄우지 않는다)
command:
- sh
- -c
- |
set -e
find dist -mindepth 1 -delete
npm run migration:run
node -e "const fs = require('fs'); const c = require('./nest-cli.json'); c.compilerOptions = { ...c.compilerOptions, deleteOutDir: false }; fs.mkdirSync('node_modules/.cache', { recursive: true }); fs.writeFileSync('node_modules/.cache/nest-cli.dev.json', JSON.stringify(c))"
exec npm run start:dev -- --config node_modules/.cache/nest-cli.dev.json
# 호스트의 vite 개발 서버·DB 툴만 붙으면 된다 — 127.0.0.1 로 한정해 LAN 에 노출하지 않는다 (보안 규칙)
# (Android 에뮬레이터의 10.0.2.2 는 호스트의 127.0.0.1 로 연결되므로 그대로 된다. 실기기는 닿지 않는다)
ports:
- "127.0.0.1:${BACKEND_PORT}:${BACKEND_PORT}"
- "127.0.0.1:${WEBSOCKET_PORT}:${WEBSOCKET_PORT}"
environment:
- CHOKIDAR_USEPOLLING=true
- WATCHPACK_POLLING=true
volumes:
- ./${BACKEND_DIR}:/app
- /app/node_modules
# 빌드 산출물은 호스트와 나눈다 — 호스트에서 npm run build 를 하면 바인드 마운트된 backend/dist 가
# 지워졌다 다시 생기면서 컨테이너의 Nest watch 가 끊긴다 (앱이 옛 코드로 계속 돈다)
- /app/dist
# 업로드는 개발에서만 호스트 디렉터리로 — 파일을 바로 열어 볼 수 있게 (base 의 named volume 을 같은 경로로 덮어쓴다).
# 소스 바인드 마운트(/app) 위에 다시 얹는다. Windows·macOS 의 Docker Desktop 은 소유자 문제가 없고,
# 리눅스 호스트는 도커가 없는 디렉터리를 root 소유로 만들므로 처음 한 번 mkdir -p data/uploads 를 해 둔다
- ${UPLOAD_DATA_DIR:-./data/uploads}:/app/uploads
# 대용량 첨부 파일(11단계)은 개발도 base 그대로 named volume <PROJECT_NAME>_files(/app/files)다 — 호스트 디렉터리로 바꾸지 않는다:
# 2GB 까지의 파일을 Windows 바인드 마운트 너머로 쓰고 읽으면 느리고, 호스트 백신(Windows Defender)이 시험 파일(EICAR 등)을
# 지워 악성 검사 시험이 어긋난다. 새 볼륨은 builder 단계의 /app/files(node 소유)를 물려받는다 (backend/Dockerfile).
# 디스크 여유 검사(FILE_STORAGE_MIN_FREE_GB)는 Docker Desktop 가상 디스크의 여유를 본다
# dev 는 watch 컴파일(swc)과 타입 검사(tsc, nest-cli.json 의 typeCheck)가 앱과 함께 돌아
# base 의 1g 로는 OOM 으로 두 번 죽었다 → dev 에서만 2g. 운영 한도(1g)는 그대로다
deploy:
resources:
limits:
memory: 2g
db:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 always 를 덮어씀)
restart: "no"
# 로컬 DB 툴 접속용. 127.0.0.1 로 한정해 외부 네트워크에 노출되지 않게 한다 (보안 규칙)
ports:
- "127.0.0.1:${DB_PORT}:5432"
redis:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 always 를 덮어씀)
restart: "no"
# 로컬 Redis CLI 접속용. 127.0.0.1 로 한정 (보안 규칙)
ports:
- "127.0.0.1:${REDIS_PORT}:6379"
mail:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 unless-stopped 를 덮어씀)
restart: "no"
# 운영용 공개 포트(25·465·993·443)를 모두 지운다 — 개발 환경은 외부에서 메일을 받지 않는다.
# 관리 화면·setup 스크립트용 HTTP(8080)만 127.0.0.1 로 연다 (보안 규칙)
ports: !override
- "127.0.0.1:${MAIL_ADMIN_PORT}:8080"
mail-resolver:
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 unless-stopped 를 덮어씀).
# 개발도 같은 리졸버를 띄운다 — 엔진은 도커 DNS 를 거쳐(System) 바깥 이름을 이 리졸버에 묻는다(Mailpit 같은 도커 내부 이름은 도커 DNS 가 푼다)
restart: "no"
# ----------------------------------------
# 악성 검사 (ClamAV) — 개발은 기본으로 띄우지 않는다 (profile `clamav`, 운영은 늘 뜬다)
#
# 서명을 읽은 검사기가 메모리를 1GB 안팎 쓰고, 처음 기동은 서명(110MB 안팎)을 받는다. 끈 동안 백엔드(CLAMAV_HOST 비움)는
# 다 올린 대용량 파일을 바로 "검사하지 못함"으로 둔다 (계약 4절 "악성 검사"). 켜려면 루트 .env.development 에 둘 다:
# COMPOSE_PROFILES=mail,clamav
# CLAMAV_HOST=clamav
# 그다음 docker compose ... up -d (backend 가 새 값으로 다시 만들어진다). 서명은 볼륨 <PROJECT_NAME>_clamav-db 에 남아 다음에는 바로 뜬다
# ----------------------------------------
clamav:
profiles: ["clamav"]
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (base 의 unless-stopped 를 덮어씀)
restart: "no"
clamav-updater:
profiles: ["clamav"]
restart: "no"
# ----------------------------------------
# mailpit — 개발 전용 메일 수집기 (profile `mail`, 운영 compose 에는 없다)
#
# setup 스크립트가 Stalwart 의 외부 발송 경로를 이리로 돌린다(Relay → mailpit:1025).
# 사내 도메인 밖으로 보낸 메일은 인터넷으로 나가지 않고 여기에만 쌓인다.
# UI: http://localhost:${MAILPIT_UI_PORT}
# ⚠️ 경로는 setup 스크립트가 설정·리로드한 뒤에야 적용된다. 그 전에 외부 주소로 보내면 실제로 발송된다.
# ----------------------------------------
mailpit:
profiles: ["mail"]
container_name: ${PROJECT_NAME}-mailpit
image: ${MAILPIT_IMAGE}
# 이미지 기본은 root 실행이다 → nobody 로 내린다 (보안 규칙: dev 포함 비-root).
# 메일은 /tmp 의 임시 DB 에 저장되므로 쓰기 권한 문제가 없다 (컨테이너를 재시작하면 비워진다)
user: "65534:65534"
restart: "no"
logging: *logging-default
# SMTP 는 내부망(mail 컨테이너)에서만 받는다
expose:
- "1025"
ports:
- "127.0.0.1:${MAILPIT_UI_PORT}:8025"
networks:
- app-network
deploy:
resources:
limits:
cpus: '0.25'
memory: 128m
# base의 proxy 외부 네트워크 의존을 dev에서 제거
networks:
proxy: !reset null