generated from ttipo/project-base
chore(docker): 운영 배포 점검 반영 — 헬스체크·업로드 권한·빌드 컨텍스트·e2e 직렬 실행
운영 스택을 따로 띄워 확인하다 드러난 것들을 막는다. - 업로드 디렉터리가 root 소유로 생기면 사진 업로드가 전부 500 인데 헬스체크(/)는 healthy 였다 → 운영 헬스체크를 /api/health(DB) + 업로드 쓰기 권한으로, 이미지에 node 소유 디렉터리를 만들어 둔다 - 빌드 컨텍스트에 호스트(Windows) node_modules 와 backend/.env.* 가 들어가고 있었다 → .dockerignore - CI e2e 는 스위트마다 빈 DB 에 마이그레이션을 동시에 돌려 충돌하고 재연결 타이밍으로만 통과했다 → --runInBand - dev 헬스체크는 쿼리 로그가 쌓이지 않게 / 로 둔다 - 첫 배포 준비(env 두 파일·proxy 네트워크·업로드 디렉터리·백업·셸 APP_ENV 주의)를 docker 스킬에 적는다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,21 @@ sudo docker compose --env-file .env.production up -d --build
|
||||
sudo docker compose --env-file .env.production logs -f backend
|
||||
```
|
||||
|
||||
첫 배포 전에 서버에서 한 번 준비한다.
|
||||
|
||||
- env 두 파일 — 루트 `.env.production`, `backend/.env.production` (`JWT_SECRET` 32자 이상, 소셜 로그인 값). placeholder 가 남으면 기동 실패
|
||||
- 외부 네트워크 `proxy` — 리버스 프록시(TLS 종료)와 backend·frontend 가 이 네트워크로 만난다. 없으면 `docker network create proxy`
|
||||
- 업로드 디렉터리 — backend 는 uid 1000 으로 돈다. `sudo install -d -o 1000 -g 1000 ./data/uploads` (`UPLOAD_DATA_DIR`).
|
||||
root 소유로 생기면 업로드가 전부 실패하고 backend 가 unhealthy 가 된다
|
||||
- 마이그레이션은 backend 가 기동할 때 자동 적용된다 (`migrationsRun`). 별도 명령이 없다
|
||||
- 백업 — `DB_DATA_DIR` 와 `UPLOAD_DATA_DIR` 를 **함께** 백업한다 (DB 가 파일을 id 로 가리킨다)
|
||||
- 셸에 `APP_ENV` 같은 변수를 export 하지 않는다 — compose 는 셸 변수를 `--env-file` 보다 우선한다.
|
||||
`APP_ENV=development` 가 남아 있으면 운영 명령이 development 설정(개발용 로그인 열림)으로 뜬다.
|
||||
`sudo docker compose --env-file .env.production config | grep NODE_ENV` 로 `production` 인지 확인한다
|
||||
|
||||
배포 후 `sudo docker compose --env-file .env.production ps` 에서 backend 가 `healthy` 인지 확인한다
|
||||
(헬스체크는 `/api/health` — DB 연결과 업로드 디렉터리 쓰기 권한을 함께 본다).
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드는 호스트에서 (개발 환경)
|
||||
|
||||
@@ -76,6 +76,11 @@ COMPOSE_PROFILES=
|
||||
|
||||
# ==========================================
|
||||
# 파일 업로드 (로컬 디스크)
|
||||
#
|
||||
# UPLOAD_DATA_DIR : 업로드 사진이 저장되는 호스트 경로 (컨테이너의 /app/uploads 에 연결)
|
||||
# Linux 호스트라면 백엔드(비-root, uid 1000)가 쓸 수 있게 미리 만든다:
|
||||
# install -d -o 1000 -g 1000 ./data/uploads
|
||||
# (Docker Desktop 의 Windows·macOS 경로는 소유권과 무관하게 쓸 수 있다)
|
||||
# ==========================================
|
||||
UPLOAD_DATA_DIR=./data/uploads
|
||||
UPLOAD_MAX_SIZE_MB=5
|
||||
|
||||
@@ -77,6 +77,12 @@ COMPOSE_PROFILES=
|
||||
|
||||
# ==========================================
|
||||
# 파일 업로드 (로컬 디스크)
|
||||
#
|
||||
# UPLOAD_DATA_DIR : 업로드 사진이 저장되는 호스트 경로 (컨테이너의 /app/uploads 에 연결)
|
||||
# - 백엔드는 비-root(node, uid 1000)로 돈다. 디렉터리가 없으면 docker 가 root 소유로 만들어
|
||||
# 업로드가 전부 실패하므로(헬스체크 unhealthy) **첫 배포 전에 한 번** 만들어 둔다.
|
||||
# sudo install -d -o 1000 -g 1000 ./data/uploads
|
||||
# - DB 는 파일을 id 로 가리킨다. 백업은 DB_DATA_DIR 와 이 디렉터리를 **함께** 한다.
|
||||
# ==========================================
|
||||
UPLOAD_DATA_DIR=./data/uploads
|
||||
UPLOAD_MAX_SIZE_MB=5
|
||||
|
||||
@@ -57,8 +57,11 @@ jobs:
|
||||
run: npm run test
|
||||
|
||||
# 표준 응답 래퍼·에러 코드·traceId 계약 검증 (.claude/rules/response-format.md)
|
||||
# --runInBand: 스위트들이 같은 DB 를 쓰고, NODE_ENV=test 라 스위트마다 기동할 때 마이그레이션을 돌린다.
|
||||
# 병렬로 돌면 빈 DB 에서 여러 스위트가 같은 테이블을 동시에 만들다 충돌한다(duplicate key ... pg_class).
|
||||
# TypeORM 재연결(3초 뒤)로 대개 통과하지만 타이밍에 기대는 것이라 순서대로 돌린다.
|
||||
- name: E2E test
|
||||
run: npm run test:e2e
|
||||
run: npm run test:e2e -- --runInBand
|
||||
env:
|
||||
NODE_ENV: test
|
||||
# 서비스 컨테이너의 네트워크 별칭 (localhost 는 잡 컨테이너 자신을 가리킨다)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 빌드 컨텍스트에서 제외한다 — 이미지 안에서 새로 만들거나, 이미지에 들어가면 안 되는 것
|
||||
#
|
||||
# node_modules : 호스트(Windows)에서 설치한 네이티브 바이너리(@swc/core-win32 등)가
|
||||
# 이미지의 npm ci 결과를 덮어써 Linux 빌드가 깨진다
|
||||
# .env* : 시크릿 — 값은 compose 의 env_file/environment 로만 주입한다 (.claude/rules/security.md)
|
||||
# uploads : 사용자 파일 — 운영에서는 compose 볼륨(UPLOAD_DATA_DIR)에 있다
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
uploads
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
@@ -29,3 +29,5 @@ NAVER_CLIENT_ID=
|
||||
GOOGLE_CLIENT_IDS=
|
||||
# Apple 번들 ID·서비스 ID (콤마 구분) — id_token 의 aud
|
||||
APPLE_CLIENT_IDS=
|
||||
|
||||
# 개발용 로그인(AUTH_DEV_LOGIN)은 운영에서 값과 무관하게 코드로 닫힌다 — 여기에 두지 않는다.
|
||||
|
||||
@@ -31,6 +31,11 @@ RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# 업로드 저장 위치(compose 의 UPLOAD_DIR) — node 가 쓸 수 있게 소유권을 넘겨 둔다.
|
||||
# named volume 은 이 소유권을 이어받는다. 호스트 경로(bind mount)는 호스트 디렉터리 소유권을 따르므로
|
||||
# 운영 서버에서 미리 uid 1000 소유로 만들어 둔다 (.env.production.example 의 UPLOAD_DATA_DIR 참조)
|
||||
RUN mkdir -p /app/uploads && chown node:node /app/uploads
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
# 비-root 실행 (보안 규칙)
|
||||
|
||||
@@ -49,6 +49,10 @@ services:
|
||||
ports:
|
||||
- "${BACKEND_PORT}:${BACKEND_PORT}"
|
||||
- "${WEBSOCKET_PORT}:${WEBSOCKET_PORT}"
|
||||
# dev 는 TypeORM 쿼리 로그가 켜져 있어 /api/health(DB ping)를 부르면 30초마다 SELECT 1 이 로그에 쌓인다
|
||||
# → 프로세스가 떠 있는지만 본다 (운영은 base 의 /api/health)
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "test -w /app/uploads && wget -qO- http://127.0.0.1:${BACKEND_PORT}/ || exit 1"]
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
- WATCHPACK_POLLING=true
|
||||
|
||||
+3
-1
@@ -88,8 +88,10 @@ services:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
healthcheck:
|
||||
# /api/health 는 DB 까지 확인한다 (루트 '/' 는 프로세스가 떠 있는지만 본다)
|
||||
# 업로드 디렉터리에 쓸 수 없으면(호스트 경로 소유권 문제) 사진 업로드가 전부 500 이 되므로 unhealthy 로 드러낸다
|
||||
# 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"]
|
||||
test: ["CMD-SHELL", "test -w /app/uploads && wget -qO- http://127.0.0.1:${BACKEND_PORT}/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 빌드 컨텍스트에서 제외한다 — 이미지 안에서 npm ci · vite build 로 새로 만든다
|
||||
# (호스트에서 설치한 node_modules 의 네이티브 바이너리가 Linux 빌드를 깨뜨리지 않게)
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
Reference in New Issue
Block a user