템플릿 점검에서 나온 항목을 한 번에 반영한다. 항목별 이유:
- Node 20 → 24 LTS (Dockerfile·CI): Node 20 은 2026-04-30 에 지원이 끝나 보안 패치가 없다
- backend·frontend .dockerignore 추가: `COPY . .` 가 호스트 node_modules(Windows 바이너리)·dist·
.env*(시크릿)를 이미지에 넣고 있었다
- 업로드 디렉터리 쓰기 검사(fail-fast): Linux 에서 Docker 가 root 소유로 만든 바인드 마운트에
비-root 앱이 쓰지 못해 첫 업로드에서야 SYS_001 로 터졌다. 기동 시 해결 명령을 안내한다
- DB 커넥션 풀 100 → 10 (DB_POOL_MAX): PostgreSQL 기본 max_connections(100)를 앱 하나가 다 잡았다
- Swagger 운영 기본 비활성 (SWAGGER_ENABLED): API 구조 전체가 공개 도메인에 노출됐다
- 헬스체크 운영 응답에서 오류 상세 제거: 드라이버 오류 문구에 내부 주소·DB 계정명이 담긴다.
원인은 error 로그로 남긴다
- nginx: location 의 add_header 때문에 정적 자원에서 보안 헤더가 빠지던 문제와,
index.html 이 캐시돼 배포 직후 빈 화면이 날 수 있던 문제를 고친다
- vite allowedHosts: true 제거 (DEV_ALLOWED_HOSTS): DNS rebinding 보호가 꺼져 있었다
- 전역 설정을 configureApp() 으로 분리해 main.ts 와 e2e 가 공유: e2e 가 설정을 복사해 재현해
실제 설정과 어긋나도 통과할 수 있었다. PORT 도 빈 값을 방어하는 파서를 거친다
- 쓰이지 않던 WEBSOCKET_PORT 제거: 별도 포트는 같은 도메인 라우팅과 맞지 않는다
- 예제 useUser 가 메시지 대신 에러 코드를 보관하도록 수정, testing.md 표 깨짐 수정,
settings.json 허용 목록에서 npm install 제거 (Windows lock 파일 규칙과 충돌)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
브라우저가 프론트와 API 를 서로 다른 도메인으로 호출하던 구조를 같은 도메인의 /api 경로로 통합한다.
운영은 Nginx Proxy Manager 의 Proxy Host(/ → frontend:8080)와 Custom location(/api → backend:3000)이,
개발은 vite dev 프록시가 같은 규칙으로 백엔드에 넘긴다.
- 같은 출처 호출이라 CORS 설정이 필요 없고 CSP connect-src 'self' 가 그대로 동작한다
(기존 예시대로 별도 도메인에 배포하면 CSP 에 막혀 API 호출이 실패했다)
- 프론트 번들의 baseURL 이 /api 상대경로라 도메인이 바뀌어도 다시 빌드할 필요가 없다
- NPM 설정 절차와 주의점(Cache Assets 끄기, proxy 네트워크 생성)을 README "운영 배포"에 정리했다
- 프록시 설정이 빠지면 frontend nginx 가 /api 를 404 로 막는다 — index.html(200)로 조용히 실패하지 않게
함께 고친 CORS 문제:
- CORS_ORIGIN 에 콤마로 여러 오리진을 적으면 문자열 전체를 하나로 비교해 모두 막히던 버그
- 운영에서도 localhost:5173·localhost:3000 이 credentials 포함으로 항상 허용되던 문제
- 형식이 잘못된 오리진(경로 포함 등)은 기동 시점에 실패하도록 검증 추가
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.env.production.example 이 개발용 값(localhost)을 그대로 담고 있어, 복사해 배포하면
서버는 정상 기동하지만 CORS 화이트리스트가 실제 도메인을 막고 프론트 번들은
localhost 를 호출한다 — 브라우저에서만 조용히 실패해 원인을 찾기 어렵다.
- CORS_ORIGIN·BACKEND_API_URL 에 로컬 주소나 예시 도메인이 있으면 운영에서 기동 실패
- 예제 값을 도메인 형태로 교체 (그대로 두면 이 검사에 걸린다)
- BACKEND_API_URL 은 앱이 쓰지 않지만 검사를 위해 compose 가 backend 에 주입한다
- 로컬에서 운영 이미지를 검증할 때만 ALLOW_LOCALHOST_URL=true 로 건너뛴다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Windows 바인드 마운트(./frontend:/app) 너머의 소스를 vite 가 모듈마다 읽는 데다
폴링 감시까지 항상 켜져 있어 첫 서빙에 수 분이 걸렸다. 호스트에서 띄우면 2초다.
- dev 오버라이드의 frontend 를 compose profile `frontend` 뒤로 숨긴다 (운영은 그대로)
- 폴링 감시는 CHOKIDAR_USEPOLLING 으로 컨테이너에서만 켠다
- dev 서버 포트·프록시 대상을 루트 .env.{mode} 에서 읽는다 (포트 SSOT 유지)
포트 충돌 시 조용히 옮기지 않도록 strictPort — CORS_ORIGIN·포트 매핑이 묶여 있다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI 워크플로는 저장소에 있지만 실행 주체(러너)가 없어 동작하지 않는 상태였다.
러너가 없으면 워크플로는 "Waiting for a runner" 로 대기만 한다.
- ci/docker-compose.runner.yml — act_runner 를 별도 스택으로 분리.
앱 스택은 개발할 때만 띄우지만 러너는 항상 켜져 있어야 하므로 생명주기가 다르다.
- ci/.env.example — 등록 토큰은 ci/.env 에만 두며 커밋되지 않는다.
- ci/README.md — 설치 절차, 증상별 문제 해결, 잡별 리소스 특성.
- 라벨 기본값이 워크플로의 runs-on(ubuntu-latest)과 일치하는지 교차 확인했다.
불일치는 잡이 영원히 대기하는 가장 흔한 원인이다.
러너가 도커 소켓에 접근해 사실상 호스트 관리자 권한을 갖는다는 점도 문서에 명시했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Redis 컨테이너가 항상 떠 있었지만 애플리케이션은 사용하지 않았다.
쓰지 않는 컨테이너는 메모리를 차지하고 "이게 왜 떠 있지" 하는 혼란을 준다.
- redis 서비스를 profiles: ["redis"] 로 숨겨 기본 기동에서 제외
- backend 의 redis 의존성을 required: false 로 두어 없어도 정상 기동
- 루트 .env 의 COMPOSE_PROFILES 한 줄로 켤 수 있게 함
(데이터 볼륨과 REDIS_* 는 유지되어 켜면 이전 상태가 그대로 이어진다)
profile 미지정 시 서비스 목록에서 제외되는 것, redis 없이 앱이 정상 동작하는 것,
COMPOSE_PROFILES=redis 로 다시 기동되는 것을 모두 확인했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
- 슬래시 커맨드(nestjs/vue3/flutter)를 정식 스킬(.claude/skills)로 전환하고
docker 스킬을 신설(루트 command.md 이관)
- 공통 규칙을 .claude/rules 로 모듈화(coding-common·response-format·error-codes·
git-convention·security·env-structure·testing·checklist)하고 CLAUDE.md 에서 @import
- 파일·식별자 네이밍 규약, 테스트 규칙 추가
- .claude/settings.json(권한·환경) 추가
- CLAUDE.md·README.md 참조 갱신, 단독 실행(non-Docker) 안내 정리
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>