diff --git a/.claude/skills/docker/SKILL.md b/.claude/skills/docker/SKILL.md index d6c0256..eae2a1d 100644 --- a/.claude/skills/docker/SKILL.md +++ b/.claude/skills/docker/SKILL.md @@ -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 연결과 업로드 디렉터리 쓰기 권한을 함께 본다). + --- ## 프론트엔드는 호스트에서 (개발 환경) diff --git a/.env.development.example b/.env.development.example index 0fcef22..b5308db 100644 --- a/.env.development.example +++ b/.env.development.example @@ -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 diff --git a/.env.production.example b/.env.production.example index 7aae564..a2dca74 100644 --- a/.env.production.example +++ b/.env.production.example @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbd8045..c3f176e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 는 잡 컨테이너 자신을 가리킨다) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..5bc8470 --- /dev/null +++ b/backend/.dockerignore @@ -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 diff --git a/backend/.env.production.example b/backend/.env.production.example index fb460d4..0f50dc9 100644 --- a/backend/.env.production.example +++ b/backend/.env.production.example @@ -29,3 +29,5 @@ NAVER_CLIENT_ID= GOOGLE_CLIENT_IDS= # Apple 번들 ID·서비스 ID (콤마 구분) — id_token 의 aud APPLE_CLIENT_IDS= + +# 개발용 로그인(AUTH_DEV_LOGIN)은 운영에서 값과 무관하게 코드로 닫힌다 — 여기에 두지 않는다. diff --git a/backend/Dockerfile b/backend/Dockerfile index 3773d5c..9337ac1 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -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 실행 (보안 규칙) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 35bd0c5..9c2864f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 7ccb969..9f61f2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..710cfb1 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +# 빌드 컨텍스트에서 제외한다 — 이미지 안에서 npm ci · vite build 로 새로 만든다 +# (호스트에서 설치한 node_modules 의 네이티브 바이너리가 Linux 빌드를 깨뜨리지 않게) +node_modules +dist +coverage +*.log +*.tsbuildinfo