# [M-6 2026-10-02] WebSocket 인증 토큰(?token=)이 접근 로그에 남지 않도록 쿼리를 뺀 경로만 기록 log_format ws_noquery '$remote_addr - $remote_user [$time_local] "$request_method $uri" ' '$status $body_bytes_sent "$http_referer" "$http_user_agent"'; server { listen 80; server_name localhost; # ── [M-2 2026-10-02] 실제 클라이언트 IP 복원 ────────────────────────────── # 이 컨테이너는 127.0.0.1:3001 로만 바인딩돼 NAS 리버스 프록시(사설 대역/도커 게이트웨이)를 # 거쳐서만 도달한다. 그 프록시가 X-Forwarded-For 끝에 실제 클라이언트를 덧붙이므로, # 신뢰 대역을 오른쪽부터 걷어내고 남는 첫 주소를 클라이언트로 쓴다(클라이언트가 앞에 위조한 # 값은 무시된다). 이후 백엔드로는 X-Real-IP/X-Forwarded-For 를 이 값으로 **덮어써서** 보낸다. set_real_ip_from 127.0.0.1; set_real_ip_from 10.0.0.0/8; set_real_ip_from 172.16.0.0/12; set_real_ip_from 192.168.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; root /usr/share/nginx/html; index index.html; # ── 보안 헤더 ──────────────────────────────────────────────────────────── add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self';" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; # Gzip 압축 설정 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # index.html — 항상 최신 버전 제공 (Vite 해시 파일은 별도 캐시) location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate" always; add_header Pragma "no-cache" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; font-src 'self';" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; } # SPA 라우팅 지원 location / { try_files $uri $uri/ /index.html; } # 헬스체크 — 백엔드로 프록시 (2026-10-02). 이전에는 SPA 의 index.html 이 200 으로 응답해 # Layout 의 백엔드 연결 표시가 백엔드가 죽어도 항상 '연결됨'이었다. location = /health { proxy_pass http://backend:8000/health; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; access_log off; } # API 프록시 location /api { proxy_pass http://backend:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_cache_bypass $http_upgrade; } # WebSocket 프록시 location /ws { proxy_pass http://backend:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; access_log /var/log/nginx/access.log ws_noquery; proxy_read_timeout 86400s; # 24시간 (장중 끊김 방지) proxy_send_timeout 86400s; } # 캐시 설정 (보안 헤더 유지) location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable" always; add_header X-Content-Type-Options "nosniff" always; } }