# ========================================== # 프론트엔드 nginx — 화면(SPA) 정적 호스팅 + /api 를 백엔드로 넘기는 프록시 # # 같은 주소(origin) 배포 — 브라우저는 화면과 같은 주소의 /api 를 부른다 (docs/mail/api-contract.md 머리말). # 그래서 CORS 가 필요 없고 CSP connect-src 'self' 로 충분하다. 운영의 입구는 이 nginx 하나다. # # ⚠️ 이 파일은 템플릿이다. 컨테이너가 뜰 때 nginx 이미지의 entrypoint 가 이 파일 # (/etc/nginx/templates/default.conf.template)의 ${NGINX_*} 만 envsubst 로 채워 /etc/nginx/conf.d/default.conf 를 만든다. # 값은 nginx-env.envsh 가 루트 env(MAIL_ATTACHMENT_MAX_SIZE_MB·UPLOAD_MAX_SIZE_MB 등)에서 계산한다. # nginx 변수($uri 등)는 NGINX_ 로 시작하지 않아 그대로 남는다 (Dockerfile 의 NGINX_ENVSUBST_FILTER). # ========================================== # 요청 추적 — rid 는 백엔드로 넘기는 X-Request-Id 이자 실패 응답의 error.traceId 와 같은 값이다 log_format main_rid '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" "$http_x_forwarded_for" rid=$request_id rt=$request_time'; # /api 용 — main_rid 와 같되 요청 줄에서 쿼리를 뺀다: 검색어(q) 등이 요청마다 남지 않게 (계약 2절 "받는 사람 추천" — 로그에는 개수만) log_format api_rid '$remote_addr - $remote_user [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" "$http_x_forwarded_for" rid=$request_id rt=$request_time'; server { # 비-root(nginx-unprivileged) 구동이라 1024 미만 포트를 쓸 수 없다 listen 8080; server_name localhost; access_log /var/log/nginx/access.log main_rid; # 기본 요청 본문 한도 — 백엔드 JSON 파서 한도(1mb)와 같다. 업로드·발송·임시 저장 경로만 아래 /api 에서 늘린다 client_max_body_size 1m; # ---------------------------------------- # 보안 헤더 (화면용) # # nginx 는 하위 블록에 add_header 가 하나라도 있으면 상위 블록의 add_header 를 물려주지 않는다. # (정적 파일 location 의 Cache-Control 때문에 JS·CSS 응답에 아래 헤더가 모두 빠져 있었다) # merge 로 상위 것과 합친다 — nginx 1.29.3 이상 (nginx-unprivileged:stable-alpine 은 1.30). # /api 는 아래에서 상속을 끈다 — API 응답의 보안 헤더는 백엔드(helmet)가 정한다. # ---------------------------------------- add_header_inherit merge; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # Content-Security-Policy — XSS 방어의 핵심 헤더 # # ⚠️ 프로젝트에 맞게 조정이 필요한 설정이다. 기본값 설명: # - default-src 'self' : 기본적으로 같은 출처만 허용 # - style-src 'unsafe-inline' : 웹메일 본문 iframe(srcdoc) 때문이다 (아래). srcdoc 문서는 이 정책을 상속하는데, # 그 안에는 앱이 넣는