generated from ttipo/project-base
chore(docker): 메일 엔진·네트워크 분리·같은 주소 배포와 Node 24 고정
웹메일 1단계를 개발 환경에서 띄우고 운영에 올리기 위한 인프라 구성이다. - 메일 엔진 Stalwart(v0.16.27)와 개발용 Mailpit 을 compose profile `mail` 로 추가한다. 초기 설정은 mail/scripts/setup.mjs 가 맡는다 — 여러 번 실행해도 안전하고, 운영에서는 사전 검사가 실패하면 아무것도 바꾸지 않고 멈춘다 - 화면과 API 를 한 주소로 배포한다: 프론트 nginx 가 /api 를 백엔드로 넘기고, 본문 한도·시간 제한·헤더 전달을 env 와 맞춘다. nginx 가 직접 내는 오류도 표준 실패 형식이다 - 인터넷을 향한 nginx 는 edge 네트워크로 백엔드에만 닿고, DB·메일 엔진은 app-network 에 둔다 (메일 엔진의 신뢰 대역도 app-network 뿐) - Node 24.22.1·npm 11.6.2·nginx 1.30.6 을 Dockerfile 과 CI 에 한 값으로 고정한다 - 개발 호스트 포트는 127.0.0.1 로 한정하고, data/ 는 클론 때부터 사용자 소유가 되게 .gitkeep 을 추적한다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+76
-10
@@ -21,8 +21,9 @@ services:
|
||||
target: build-stage
|
||||
# 비-root 구동이라 1024 미만 포트를 쓸 수 없다 → vite 기본 포트 사용
|
||||
command: npm run dev -- --host 0.0.0.0 --port 5173
|
||||
# 호스트의 브라우저만 붙으면 된다 — 127.0.0.1 로 한정해 LAN 에 노출하지 않는다 (보안 규칙, 호스트 vite 와 같은 범위)
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:5173"
|
||||
- "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"]
|
||||
@@ -36,30 +37,58 @@ services:
|
||||
volumes:
|
||||
- ./${FRONTEND_DIR}:/app
|
||||
- /app/node_modules
|
||||
# dev에서는 리버스 프록시 미사용 — base의 networks 목록을 덮어씀
|
||||
# dev에서는 리버스 프록시 미사용 — base의 networks 목록을 덮어씀.
|
||||
# 운영과 같이 edge 로만 백엔드에 닿는다 (app-network = 메일 엔진의 신뢰 대역에는 붙이지 않는다)
|
||||
networks: !override
|
||||
- app-network
|
||||
- edge
|
||||
|
||||
backend:
|
||||
# dev에서는 Docker Desktop 시작 시 자동 실행 방지 (수동 up 때만 실행)
|
||||
restart: "no"
|
||||
build:
|
||||
target: builder
|
||||
command: npm run start:dev
|
||||
# /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:
|
||||
- "${BACKEND_PORT}:${BACKEND_PORT}"
|
||||
- "${WEBSOCKET_PORT}:${WEBSOCKET_PORT}"
|
||||
- "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
|
||||
# base 의 업로드 볼륨을 소스 바인드 마운트 위에 다시 얹는다
|
||||
# 빌드 산출물은 호스트와 나눈다 — 호스트에서 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
|
||||
# dev에서는 리버스 프록시 미사용 — base의 networks 목록을 덮어씀
|
||||
networks: !override
|
||||
- app-network
|
||||
# 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 를 덮어씀)
|
||||
@@ -75,6 +104,43 @@ services:
|
||||
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"
|
||||
|
||||
# ----------------------------------------
|
||||
# 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"
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user