Nginx 리버스 프록시 설정 완전 가이드: API·관리자·프론트를 한 도메인으로 라우팅하기
백엔드는 3000번에 잘 떠 있는데, 도메인으로 붙이려니 막막하다
VPS 한 대에 API 서버(:3000), 관리자 백오피스(:4000), 그리고 빌드된 정적 프론트엔드를 올려뒀다고 해봅시다. 로컬에서 curl http://127.0.0.1:3000은 잘 응답하는데, 막상 https://example.com/api로 붙이려니 어디서부터 손대야 할지 막막합니다.
이때 필요한 게 리버스 프록시(Reverse Proxy) 입니다. 외부에서 들어오는 80/443 요청을 Nginx가 한 번에 받아서, 경로에 따라 적절한 내부 포트로 넘겨주는 역할이죠. 백엔드 포트는 외부에 노출하지 않고 127.0.0.1에만 바인딩한 뒤, Nginx만 방화벽을 열어두는 게 요즘 1인 개발/사이드프로젝트의 표준 구성입니다.
이 글의 최종 목표는 명확합니다. 하나의 도메인(example.com)으로 API·관리자·프론트를 모두 서비스하고, WebSocket과 HTTPS까지 자력으로 구성하는 것. "일단 복붙 → 동작 확인 → 왜 그런지 이해" 순서로 갑니다.
⚠️ 이 글은 "설정을 올바르게 작성" 하는 관점입니다. 설정은 맞는데 502 Bad Gateway가 뜬다면 그건 백엔드가 죽었거나 포트가 안 맞는 경우로, 진단 관점의 별도 글(👉 nginx 502 Bad Gateway 해결)을 참고하세요. 역할을 분리해두면 문제 추적이 훨씬 빠릅니다.
1단계. 가장 기본형: proxy_pass로 백엔드 1개 연결하기
먼저 백엔드 하나만 프록시하는 최소 server 블록입니다. /etc/nginx/sites-available/example.com에 저장하고 심볼릭 링크를 걸면 됩니다.
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
location / {
# 들어온 요청을 내부 3000번 백엔드로 그대로 넘긴다
proxy_pass http://127.0.0.1:3000;
}
}proxy_pass http://127.0.0.1:3000; 이 한 줄이 핵심입니다. "클라이언트가 보낸 요청을 그대로 127.0.0.1:3000으로 전달하라"는 뜻이죠. Nginx가 클라이언트와 백엔드 사이에 중계 서버로 끼어드는 겁니다.
▸ 왜 필요한가: 백엔드를 외부에 직접 노출하지 않고 Nginx 한 곳에서 트래픽을 받기 위해서입니다.
▸ 검증:
# 설정 문법 검사 (반드시 OK가 떠야 reload)
sudo nginx -t
# 무중단 재적용
sudo systemctl reload nginx
# 응답 헤더 확인 (200 또는 백엔드 상태코드가 떠야 정상)
curl -I http://example.com/curl -I에서 백엔드가 보낸 Server, Content-Type 헤더가 보이면 프록시가 정상 동작하는 겁니다.
2단계. location 경로별 다중 백엔드 분기
이제 본론입니다. /api는 3000번, /admin은 4000번, 나머지 /는 정적 프론트로 분기합니다.
server {
listen 80;
server_name example.com;
# 정적 프론트엔드 (빌드 결과물 디렉터리)
root /var/www/frontend/dist;
index index.html;
# /api/* → API 서버(3000)
location /api/ {
proxy_pass http://127.0.0.1:3000/; # 끝 슬래시 주의!
}
# /admin/* → 관리자 서버(4000)
location /admin/ {
proxy_pass http://127.0.0.1:4000/;
}
# 그 외 모든 경로 → SPA 라우팅 (없으면 index.html로)
location / {
try_files $uri $uri/ /index.html;
}
}trailing slash 하나가 경로를 바꾼다 — 404의 가장 흔한 원인
복붙했는데 404가 뜨거나 경로가 깨지는 사고의 90%는 proxy_pass 끝의 슬래시(/) 때문입니다. location /api/ 기준으로 /api/users 요청이 백엔드에 어떻게 도착하는지 비교해봅시다.
| proxy_pass 설정 | 요청 URL | 백엔드 도착 경로 | 설명 |
|---|---|---|---|
proxy_pass http://127.0.0.1:3000; (슬래시 없음) | /api/users | /api/users | location 경로를 그대로 붙여 전달 |
proxy_pass http://127.0.0.1:3000/; (슬래시 있음) | /api/users | /users | location 매칭 부분(/api)을 떼고 전달 |
규칙은 간단합니다. proxy_pass에 URI(끝 슬래시 포함)가 있으면 location에 매칭된 앞부분을 잘라내고 나머지를 붙입니다. API 서버 내부 라우트가 /users로 시작한다면 슬래시를 붙여야 하고, /api/users로 정의돼 있다면 슬래시를 빼야 합니다. 둘 다 멀쩡한 설정이지만 결과가 정반대라 헷갈리기 쉽습니다.
▸ 검증:
sudo nginx -t && sudo systemctl reload nginx
curl -I http://example.com/api/health # API 백엔드 응답?
curl -I http://example.com/admin/ # 관리자 응답?
curl -I http://example.com/ # index.html 응답?3단계. 실서비스 필수 설정: 원본 정보 헤더 + WebSocket
여기까지만 하면 동작은 하지만, 백엔드 입장에서는 "모든 요청이 127.0.0.1(Nginx)에서 왔다" 고 보입니다. 실제 클라이언트 IP, 원본 호스트, http/https 여부가 전부 사라지죠. 이걸 헤더로 전달해줘야 합니다.
location /api/ {
proxy_pass http://127.0.0.1:3000/;
# 원본 정보 전달 세트 (실서비스 필수)
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 $scheme;
}각 헤더가 백엔드에서 결정하는 것
| 헤더 | 백엔드에서 결정하는 것 |
|---|---|
Host $host | 백엔드가 인식하는 요청 도메인. 멀티 도메인 분기·리다이렉트 URL 생성에 사용 |
X-Real-IP $remote_addr | 실제 클라이언트 IP 한 개. 접속 로그·차단 목록에 사용 |
X-Forwarded-For $proxy_add_x_forwarded_for | 프록시를 거친 클라이언트 IP 체인. 다단 프록시 환경에서 원본 IP 추적 |
X-Forwarded-Proto $scheme | 클라이언트가 http/https 중 무엇으로 왔는지. 백엔드의 https 인식·리다이렉트 URL 결정 |
이 헤더들이 없으면 백엔드 로그에는 모든 사용자가 127.0.0.1로 찍히고, https로 접속했는데도 백엔드가 http로 착각해 리다이렉트 루프가 도는 사고가 납니다. 실무에서 "로그인 후 무한 리다이렉트" 이슈를 며칠 잡았더니 범인이 X-Forwarded-Proto 누락이었던 적이 있는데, 이 네 줄은 처음부터 세트로 넣는 습관을 권합니다.
WebSocket 프록시 (채팅·알림·HMR 개발 서버)
WebSocket은 일반 HTTP 요청을 Upgrade로 전환하는 핸드셰이크가 필요합니다. Nginx는 기본적으로 이 헤더를 백엔드로 넘기지 않으므로 명시해줘야 합니다. 먼저 http 컨텍스트에 map을 추가합니다.
# /etc/nginx/nginx.conf 의 http { } 블록 안에 추가
# (server 블록 안이 아니라 http 컨텍스트여야 함!)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}그리고 WebSocket 엔드포인트 location에 Upgrade 헤더를 넘깁니다.
location /ws/ {
proxy_pass http://127.0.0.1:3000/;
# WebSocket 핸드셰이크 필수 3종
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# 원본 정보 헤더도 동일하게
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}▸ 왜 필요한가: Upgrade/Connection 헤더가 있어야 HTTP 연결이 WebSocket으로 승격(101 Switching Protocols)됩니다.
▸ 검증: WebSocket은 curl로 핸드셰이크 응답(101)을 직접 확인할 수 있습니다.
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
-H "Sec-WebSocket-Version: 13" \
http://example.com/ws/
# → HTTP/1.1 101 Switching Protocols 가 떠야 성공4단계. Let's Encrypt SSL 종단(443) + 80→443 리다이렉트
마지막은 HTTPS입니다. certbot의 nginx 플러그인이 사실상 표준이라, 발급과 설정 자동 삽입을 한 번에 처리합니다.
# certbot 설치 (Ubuntu/Debian 예시)
sudo apt install certbot python3-certbot-nginx
# 인증서 발급 + nginx 설정 자동 수정
sudo certbot --nginx -d example.comcertbot --nginx를 돌리면 위에서 만든 server 블록에 다음과 같은 443 설정과 80→443 리다이렉트가 자동으로 추가됩니다.
server {
listen 443 ssl http2; # HTTP/2 활성화
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; # TLS 1.3 등 권장값
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# ... (여기에 2~3단계의 location 블록들이 들어감)
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri; # 모든 http → https 리다이렉트
}▸ 검증 + 자동 갱신 점검:
curl -I https://example.com/api/health # https로 정상 응답?
sudo certbot renew --dry-run # 자동 갱신 시뮬레이션 (에러 없어야 함)최종 통합 server 블록 전체
위 모든 단계를 하나로 합친 완성본입니다. 그대로 복붙하고 도메인·경로·포트만 바꿔 쓰세요.
# /etc/nginx/nginx.conf 의 http { } 안에 한 번만
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# 80 포트: 전부 https로 리다이렉트
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
# 443 포트: 실제 서비스
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/frontend/dist;
index index.html;
# 공통 헤더 세트 (재사용)
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 $scheme;
# API 서버
location /api/ {
proxy_pass http://127.0.0.1:3000/;
}
# 관리자 서버
location /admin/ {
proxy_pass http://127.0.0.1:4000/;
}
# WebSocket
location /ws/ {
proxy_pass http://127.0.0.1:3000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
# 정적 프론트(SPA)
location / {
try_files $uri $uri/ /index.html;
}
}흔한 실수 체크리스트
| 증상 | 원인 | 해결 |
|---|---|---|
/api/users가 404 | proxy_pass 끝 슬래시 유무 잘못 | 2단계 표 보고 슬래시 맞추기 |
unknown directive "map" | map을 server 안에 넣음 | http 컨텍스트(nginx.conf)로 이동 |
로그에 모든 IP가 127.0.0.1 | 원본 헤더 누락 | X-Real-IP/X-Forwarded-For 추가 |
| https인데 무한 리다이렉트 | X-Forwarded-Proto 누락 | 헤더 추가 후 백엔드 재시작 |
| WebSocket 연결 끊김 | Upgrade/Connection 미설정 | 3단계 WS 블록 적용 |
| 인증서 만료로 접속 불가 | 갱신 자동화 미점검 | certbot renew --dry-run 정기 확인 |
💡 설정은 다 맞는데 502 Bad Gateway가 뜬다면, 그건 Nginx 문제가 아니라 백엔드(3000/4000)가 죽었거나 포트가 안 맞는 신호입니다. 진단 절차는 nginx 502 Bad Gateway 해결 글에서 별도로 다룹니다. 이 글(설정 작성)과 그 글(에러 진단)을 역할로 나눠 기억해두면 문제 해결이 빠릅니다.
자주 묻는 질문 (FAQ)
Q. proxy_pass에 슬래시를 붙여야 하나요, 빼야 하나요?
A. 백엔드 라우트 정의에 달렸습니다. 백엔드가 /users로 라우트를 받으면 location /api/에서 슬래시를 붙여(...:3000/) /api를 떼고 넘기고, 백엔드가 /api/users로 정의돼 있으면 슬래시를 빼서 경로를 그대로 전달하세요.
Q. map 블록을 server 안에 넣었더니 에러가 납니다.
A. map은 http 컨텍스트 전용 지시어입니다. server { } 안이 아니라 /etc/nginx/nginx.conf의 http { } 블록에 한 번만 선언하세요. 여러 server에서 공유됩니다.
Q. Let's Encrypt 인증서는 수동으로 갱신해야 하나요?
A. 아니요. certbot은 설치 시 systemd 타이머나 cron으로 자동 갱신을 등록합니다. 정상 동작 여부만 sudo certbot renew --dry-run으로 가끔 점검하면 됩니다. 갱신 후 Nginx reload도 플러그인이 처리합니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...