Nginx 리버스 프록시로 Node.js 서비스 앞단 구성하기 | SSL·upstream·로그
이 글의 핵심
Nginx로 Node.js 앞단에 TLS 종료·upstream·로깅·프록시 헤더를 두고 프로덕션에 가까운 리버스 프록시를 구성합니다.
들어가며
Nginx는 고성능 웹 서버이자 리버스 프록시로, Node.js 앱 앞에서 TLS 종료, 정적 파일 서빙, 로드 밸런싱, 압축, 레이트 리밋을 담당하는 구성이 매우 흔합니다. Node는 비즈니스 로직에 집중하고, 엣지에서 연결·암호화·라우팅을 처리하면 운영이 단순해집니다.
이 글은 단일 Node 인스턴스 뒤에 Nginx를 두는 기본 패턴과, 확장 시 upstream으로 여러 Node 프로세스를 묶는 방법을 다룹니다. 컨테이너 기반 스택은 Docker Compose 글과 함께 보시면 연결되고, 오케스트레이션 단계는 Kubernetes(minikube)로 이어집니다. 백엔드가 붙는 DB는 Node.js 데이터베이스 연동·C++ DB 연동(libpq 등)·PostgreSQL vs MySQL과, 캐시는 Redis 캐싱 패턴과 묶어서 보시면 엣지 → 앱 → 캐시 → DB가 한 흐름으로 잡힙니다. 서버 디스크·inode 이슈는 Linux 트러블슈팅과 겹칠 수 있습니다.
요청 흐름은 클라이언트 → (TLS·HTTP/2) → Nginx → (HTTP·keepalive) → Node upstream 순으로 이해하시면 됩니다. Node는 비즈니스 로직, Nginx는 연결·암호화·분배를 맡는 앞단 관문에 가깝습니다.
이 글을 읽으면
- 프록시 헤더와 upstream 블록을 이해하고 실제
nginx.conf에 적용하실 수 있습니다 - Let’s Encrypt 인증서와 함께 쓰는 SSL 서버 블록 패턴을 살펴보실 수 있습니다
- 액세스·에러 로그와 WebSocket 업그레이드 설정을 맞추실 수 있습니다
목차
개념: 리버스 프록시와 Nginx 역할
기본 개념
- 리버스 프록시: 클라이언트는 Nginx에만 붙고, Nginx가 백엔드(Node) 로 요청을 전달합니다.
- TLS 종료: 브라우저 ↔ Nginx는 HTTPS, Nginx ↔ Node는 HTTP(내부 네트워크)인 구성이 일반적입니다.
- upstream: 여러 백엔드 주소를 묶고 라운드 로빈, least_conn, ip_hash(세션 고정) 등을 선택합니다.
왜 필요한가
Node 단일 프로세스는 CPU 코어 하나를 주로 쓰므로, PM2 클러스터나 여러 컨테이너 + Nginx 로드 밸런싱으로 수평 확장합니다. 또한 정적 자산은 Nginx가 직접 서빙해 Node 부하를 줄입니다.
실전: nginx.conf 설정
최소 예시: HTTP → Node (개발·내부망)
# /etc/nginx/conf.d/app.conf
upstream node_app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_http_version 1.1;
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;
proxy_pass http://node_app;
}
}
upstream+keepalive: Nginx와 Node 사이 TCP 연결을 재사용해 요청마다 핸드셰이크 비용을 줄입니다(HTTP/1.1Connection처리와 맞물립니다).proxy_http_version 1.1: 업스트림에 HTTP/1.1을 씁니다(keepalive·청크 전송 등과 궁합이 좋습니다).Host: Node가 가상 호스트·절대 URL 생성을 할 때 원래 요청 호스트를 알 수 있게 합니다.X-Forwarded-For/X-Forwarded-Proto: Node가 클라이언트 IP·원래 스킴(https) 을 알아 리다이렉트·쿠키·레이트 리밋에 씁니다.
HTTPS + Let’s Encrypt (인증서 경로는 certbot 기본 가정)
upstream node_app {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/api.access.log combined;
error_log /var/log/nginx/api.error.log warn;
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
proxy_pass http://node_app;
}
# WebSocket (Socket.io 등)
location /socket.io/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_pass http://node_app;
}
}
listen 443 ssl http2: TLS 종료와 HTTP/2를 켭니다(클라이언트 ↔ Nginx 구간).ssl_protocols TLSv1.2 TLSv1.3: 구형 TLS 비활성으로 보안·호환을 맞춥니다.- 일반
location에서proxy_set_header Connection ""는 upstream keepalive와 함께 쓸 때 흔한 패턴입니다(모듈·버전에 따라 기본값이 달라 명시하기도 합니다). - WebSocket
location에서는Upgrade·Connection: upgrade를 넘겨 HTTP 업그레이드가 끝까지 전달되게 합니다.
Node(Express)에서 신뢰 프록시 설정
// Express: X-Forwarded-* 반영 (프록시 뒤에 있을 때)
import express from 'express';
const app = express();
app.set('trust proxy', 1); // Nginx 한 홉
app.get('/health', (_req, res) => res.send('ok'));
Docker Compose와 함께
Nginx 컨테이너와 API 컨테이너가 같은 네트워크에 있으면 upstream은 server api:3000; 형태가 됩니다. 자세한 스택 구성은 Docker Compose로 Node API·DB·Redis 한 번에 띄우기를 참고하세요.
고급: 로드 밸런싱·버퍼·제한
로드 밸런싱 알고리즘
| 지시어 | 용도 |
|---|---|
round-robin (기본) | 균등 분산 |
least_conn | 연결 수 적은 서버 우선(처리 시간이 긴 요청에 유리) |
ip_hash | 클라이언트 IP 기준 고정(세션 스티키 필요 시) |
클라이언트 본문·타임아웃
client_max_body_size 10m;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
레이트 리밋(간단)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location / {
limit_req zone=api_limit burst=20 nodelay;
# ... proxy 설정
}
성능 비교 관점
Nginx는 이벤트 기반으로 수만 동시 연결에 적합하고, Node는 I/O에 강점이 있습니다. 정적 파일은 Nginx가 직접 서빙할 때 디스크 I/O와 Node 이벤트 루프를 동시에 아낄 수 있습니다.
| 구성 | 장점 | 주의 |
|---|---|---|
| TLS @ Nginx | Node에서 암호화 부담 감소 | 인증서 갱신 자동화(certbot) |
| keepalive upstream | 연결 재사용으로 지연 감소 | 백엔드 수·타임아웃 맞춤 |
| gzip / brotli | 전송량 감소 | CPU 사용 증가 |
실무 사례
- 블루그린/무중단: upstream 서버 그룹을 바꾸거나, 새 컨테이너 기동 후 헬스체크 통과 시만 트래픽 전환.
- 스테이징:
server_name만 분리해 동일 Node 이미지에 다른DATABASE_URL주입. - 배포 파이프라인: GitHub Actions로 Node.js CI/CD에서 이미지 배포 후 Nginx는 설정 리로드(
nginx -s reload)만 수행.
트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
| 502 Bad Gateway | Node 미기동·포트 불일치 | ss -tlnp, 컨테이너 로그 확인 |
| 리다이렉트가 http로 감 | X-Forwarded-Proto 누락 | proxy_set_header X-Forwarded-Proto $scheme |
| WebSocket 끊김 | Upgrade 헤더 미전달 | 위 socket.io 예시처럼 설정 |
| 실제 IP가 모두 Nginx | trust proxy 미설정 | Express trust proxy, 로그는 $http_x_forwarded_for 참고 |
| 업로드 실패 413 | client_max_body_size 기본 1m | 값 상향 |
디버깅 팁: curl -v https://api.example.com로 TLS·헤더를 보고, Nginx error_log 레벨을 info로 잠시 올려 upstream 에러를 확인합니다.
마무리
- Nginx upstream과 프록시 헤더는 Node를 프록시 뒤에 둘 때의 기본기입니다.
- TLS·로그·WebSocket까지 한 번에 정의해 두시면 스테이징과 프로덕션 차이를 줄일 수 있습니다.
- Node 배포 전체 맥락은 Node.js 배포 가이드와 함께 보시면 좋습니다.
프로덕션 체크리스트
- 인증서 만료 전에 갱신(cron·certbot)이 도는지,
fullchain경로가 배포와 일치하는지 확인합니다. client_max_body_size·proxy_read_timeout을 업로드·장시간 API에 맞춥니다.- Nginx·Node 액세스 로그에 실제 클라이언트 IP가 남는지(
X-Forwarded-For) 점검합니다.