Skip to main content

Nginx + Cloudflared + Module Sam 연결 가이드

등록일: 2026-04-30


1. 목적

본 문서는 Cloudflared, Nginx Gateway, Spring Boot 기반 Nexus 모듈을 연결하는 운영 구조 설명 문서임. 특히 기존 Thymeleaf 기반 module-system 프록시 설정과 React + Spring Boot 기반 module-sam 프록시 설정의 차이를 설명함. sam.sleepzz.xyz가 초기에는 502 Bad Gateway를 반환하다가, Nginx 프록시 헤더 조정 후 정상 동작하게 된 이유를 정리함.

2. 전체 네트워크 흐름

최종 요청 흐름은 아래와 같음.

Client Browser
-> Cloudflare
-> cloudflared tunnel
-> nginx-gateway container
-> host 172.17.0.1:8007
-> nexus-sam container (Spring Boot + React static assets)

핵심 포인트는 module-sam이 배포 환경에서 별도 프런트 개발 서버를 띄우지 않는 구조라는 점임. React 빌드 산출물은 Spring Boot 정적 리소스로 포함됨. 따라서 최종 서비스 포트는 프런트와 백엔드를 분리하지 않고 8007 단일 포트 구조임.

3. 모듈별 렌더링 구조 차이

3.1. module-system

module-system은 Thymeleaf 기반 서버 렌더링 모듈임. Nginx는 HTML 페이지와 백엔드 응답을 모두 8001로 프록시하면 됨. 브라우저가 받는 첫 문서 자체가 Spring MVC + Thymeleaf가 생성한 서버 응답임.

3.2. module-sam

module-sam은 React + Phaser 기반 SPA 모듈임. 배포 시 React 빌드 결과물 index.html, /assets/*를 Spring Boot가 정적 리소스로 제공함. 브라우저가 받는 첫 문서는 Spring Boot가 정적으로 전달하는 index.html임. 이후 실제 화면 노출 전에는 프런트 useAuth()/api/config, /api/auth/verify를 호출하여 공통 인증 상태를 확인함. 인증 성공 후 실제 화면 구성은 React Router와 브라우저 내 JavaScript가 담당함.

4. 초기 Nginx 설정

초기 sam.sleepzz.xyz 설정은 아래와 같은 구조였음.

server {
listen 7007;
server_name sam.sleepzz.xyz;

location / {
proxy_pass http://172.17.0.1:8007;

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 $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}

겉보기에는 module-system과 거의 동일한 구조임. 그러나 module-sam에서는 이 구성이 502를 유발함.

5. 실제 증상

확인된 증상은 아래와 같았음.

  • 홈서버에서 localhost:8007 접근은 정상 동작 상태였음.
  • Nginx 컨테이너 내부에서 curl http://172.17.0.1:8007/curl http://172.17.0.1:8007/sam 모두 200 응답 상태였음.
  • Cloudflared에서 nginx-gateway:7007 매핑도 정상 상태였음.
  • 그러나 curl -H "Host: sam.sleepzz.xyz" http://127.0.0.1:7007/ 요청은 Nginx에서 502를 반환했음.

이 결과는 앱 자체 장애가 아니라, Nginx가 백엔드로 전달하는 프록시 요청 형태가 문제였음을 의미함.

6. 원인 정리

핵심 원인은 두 가지 헤더 처리 방식 차이였음.

6.1. Host 헤더 전달 방식 문제

초기 설정은 아래와 같이 원래 도메인 값을 그대로 백엔드에 전달했음.

proxy_set_header Host $host;

이 경우 백엔드는 Host: sam.sleepzz.xyz 요청을 받게 됨. 직접 curl http://172.17.0.1:8007/ 할 때의 요청 형태와 달라짐. 즉, Nginx를 경유할 때만 백엔드가 다른 조건의 요청을 받게 되는 구조였음.

6.2. Upgrade / Connection 헤더 강제 전달 문제

초기 설정은 일반 HTTP 요청에도 항상 아래 헤더를 강제로 전달했음.

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

이 설정은 WebSocket 연결 프록시에서는 자주 사용함. 그러나 일반 HTML, 정적 리소스, SPA 엔트리 문서 전달에는 불필요한 경우가 많음. module-sam처럼 정적 index.html과 JS/CSS 파일을 안정적으로 전달해야 하는 SPA 구조에서는 오히려 문제를 유발할 수 있음.

7. 최종 정상 설정

문제 해결 후 sam.sleepzz.xyz는 아래와 같은 구조로 단순화함.

server {
listen 7007;
server_name sam.sleepzz.xyz;

location / {
proxy_pass http://172.17.0.1:8007;

proxy_http_version 1.1;
proxy_set_header Host $proxy_host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;

proxy_redirect off;
}
}

8. 각 헤더의 의미와 동작

8.1. proxy_set_header Host $proxy_host;

백엔드에 전달되는 Host를 업스트림 기준 값으로 고정하는 설정임. 현재 값은 사실상 172.17.0.1:8007 기준이 됨. 직접 curl http://172.17.0.1:8007/ 했을 때와 유사한 요청 조건을 만들어 줌. 초기 문제의 핵심이었던 도메인 기반 Host 전달 차이를 제거함.

8.2. proxy_set_header X-Forwarded-Host $host;

사용자가 실제로 접근한 원래 도메인 sam.sleepzz.xyz를 별도 헤더로 전달하는 설정임. 백엔드가 원래 호스트 정보를 참조해야 하는 경우를 위해 유지하는 메타 정보임. 즉, 라우팅 안정성은 Host $proxy_host로 확보하고, 원래 도메인 정보는 X-Forwarded-Host로 보존하는 구조임.

8.3. proxy_set_header X-Forwarded-Proto https;

Cloudflare 외부 접속이 HTTPS라는 사실을 백엔드에 전달하는 설정임. Spring Boot가 프록시 뒤에 있을 때 원래 프로토콜을 올바르게 인식하도록 보조함. 리다이렉트, 절대 URL 생성, 보안 정책 판단에 중요함.

8.4. proxy_redirect off;

백엔드가 리다이렉트 응답을 반환하는 경우 Nginx가 임의로 Location 헤더를 재작성하지 않도록 막는 설정임. 프록시 체인이 단순할 때 불필요한 리다이렉트 변형을 방지하는 안정화 옵션임.

8.5. Upgrade / Connection "Upgrade" 제거

일반 HTTP 페이지 전달에 불필요한 업그레이드 헤더를 제거한 것임. WebSocket 프록시가 필요한 서비스가 아닌 이상 기본 HTML 및 정적 리소스 프록시에서는 제거하는 편이 더 안전함.

9. 왜 module-system은 기존 설정으로도 동작했고 module-sam은 실패했는가

두 서비스는 동일하게 Spring Boot 기반이지만, 브라우저가 최초에 필요로 하는 응답 구조가 다름.

9.1. module-system

서버 렌더링 HTML 중심 구조임. 일반적인 프록시 헤더 조합에서도 큰 문제 없이 템플릿 응답을 돌려주는 경우가 많음. 일부 리다이렉트나 로그인 흐름이 있어도 서버가 직접 페이지를 생성하므로 동작 범위가 비교적 단순함.

9.2. module-sam

정적 index.html, /assets/*.js, /assets/*.css를 안정적으로 전달해야 하는 SPA 구조임. React Router는 첫 HTML이 정상 전달되어야 그 다음 화면 렌더링이 가능함. 즉 프록시 단계에서 문서 하나라도 비정상 상태가 되면 브라우저는 곧바로 502 또는 빈 화면 증상으로 이어짐. 따라서 Host 헤더와 불필요한 Upgrade 헤더 차이가 더 민감하게 작용함.

10. module-sam이 도메인에 최종 정상 연결된 과정

정상 연결의 핵심 흐름은 아래와 같음.

  1. Cloudflare가 sam.sleepzz.xyz 요청을 수신함.
  2. Cloudflared tunnel이 해당 요청을 nginx-gateway:7007로 전달함.
  3. Nginx는 sam.sleepzz.xyzserver 블록을 매칭함.
  4. Nginx는 요청을 172.17.0.1:8007 업스트림으로 프록시함.
  5. Spring Boot module-sam/ 또는 /sam 요청을 index.html 정적 리소스로 전달함.
  6. 브라우저는 index.html을 수신한 뒤 /assets/* JavaScript, CSS를 추가 요청함.
  7. 프런트 useAuth()GET /api/config로 로그인 URL을 조회함.
  8. 프런트 useAuth()GET /api/auth/verify로 JWT 쿠키 인증 상태를 확인함.
  9. 미인증 상태이면 module-system 로그인 페이지로 리다이렉트하고, 인증 성공 후 원래 module-sam URL로 복귀함.
  10. 인증 성공 상태이면 React Router가 /, /sam, /index.html 경로를 SamGame으로 렌더링함.
  11. 최종적으로 Phaser + React 게임 화면이 브라우저에 표시됨.

10.1. 공통 인증 연계 구조

module-sam은 현재 module-common의 공통 인증 흐름을 사용함. nexus.auth.enabled: true 상태에서 구동함. 로그인 페이지 URL은 공통 설정 application-common.yml, application-common-local.yml에서 공급받음. 로컬 기본 로그인 URL은 http://localhost:8001/auth/login 구조임. 운영 기본 로그인 URL은 https://system.sleepzz.xyz/auth/login 구조임. JWT 토큰 발급과 쿠키 저장은 module-system 로그인 페이지와 module-common 보안 필터가 담당함. module-sam은 해당 쿠키를 사용하여 /api/auth/verify 인증 통과 여부만 확인하는 구조임.

11. 트러블슈팅 체크리스트

11.1. 앱 자체 확인

아래 명령으로 백엔드 컨테이너 자체 동작 확인 가능함.

curl -I http://localhost:8007/
curl -I http://localhost:8007/sam

11.2. Nginx 컨테이너에서 업스트림 확인

Nginx 컨테이너 내부에서 호스트 업스트림에 직접 붙는지 확인함.

docker exec -it nginx-gateway sh
curl -I http://172.17.0.1:8007/
curl -I http://172.17.0.1:8007/sam

이 단계가 성공하면 앱과 호스트 포트 바인딩은 정상임.

11.3. Nginx server block 매칭 확인

실제 server_name 블록이 어떤 응답을 반환하는지 확인함.

curl -I -H "Host: sam.sleepzz.xyz" http://127.0.0.1:7007/

이 단계에서 502가 나오면 Nginx -> upstream 프록시 요청 헤더 조합 문제일 가능성이 높음.

11.4. 설정 덤프 확인

로드된 설정 전체 확인 시 아래 명령 사용함.

nginx -T

11.5. Nginx 에러 로그 확인

최종 에러 원인 추적은 아래 로그 확인이 가장 빠름.

tail -n 100 /var/log/nginx/error.log

12. 운영 권장사항

12.1. SPA 모듈 권장 프록시 원칙

React SPA + Spring Boot 정적 리소스 조합 모듈은 프록시를 최대한 단순하게 유지하는 편이 안전함. 특별한 사유가 없다면 일반 페이지 프록시에 Upgrade 헤더를 강제로 넣지 않는 것을 권장함. Host는 업스트림 기준으로 안정화하고, 외부 도메인 정보는 X-Forwarded-Host로 보존하는 방식을 권장함.

12.2. Thymeleaf 모듈과 SPA 모듈 분리 인식 권장

둘 다 Spring Boot라고 해서 Nginx 설정을 완전히 동일하게 복사하는 방식은 위험함. Thymeleaf 모듈은 서버 렌더링 중심 구조임. SPA 모듈은 정적 엔트리 문서와 브라우저 라우팅 중심 구조임. 프록시 헤더 민감도가 다르므로 운영 설정도 서비스 특성에 맞춰 조정하는 것이 안전함.

13. 최종 요약

module-sam의 502 문제는 Cloudflare 또는 Docker 네트워크 자체 문제라기보다, Nginx가 백엔드로 전달하는 요청 헤더 구성 문제였음. Host $host와 일반 요청에 대한 Connection "Upgrade" 강제 설정이 원인 후보였음. 이를 Host $proxy_host, X-Forwarded-Host $host, Upgrade 제거 구조로 바꾸면서 정상 동작 상태가 되었음. 최종적으로 module-sam은 Cloudflare 도메인 sam.sleepzz.xyz에 정상 연결되었음.