Nginx 리버스 프록시로 Node.js 서비스 앞단 구성하기 | SSL·upstream·로그

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 업그레이드 설정을 맞추실 수 있습니다

목차

  1. 개념: 리버스 프록시와 Nginx 역할
  2. 실전: nginx.conf 설정
  3. 고급: 로드 밸런싱·버퍼·제한
  4. 성능 비교 관점
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

개념: 리버스 프록시와 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.1 Connection 처리와 맞물립니다).
  • 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 @ NginxNode에서 암호화 부담 감소인증서 갱신 자동화(certbot)
keepalive upstream연결 재사용으로 지연 감소백엔드 수·타임아웃 맞춤
gzip / brotli전송량 감소CPU 사용 증가

실무 사례

  • 블루그린/무중단: upstream 서버 그룹을 바꾸거나, 새 컨테이너 기동 후 헬스체크 통과 시만 트래픽 전환.
  • 스테이징: server_name만 분리해 동일 Node 이미지에 다른 DATABASE_URL 주입.
  • 배포 파이프라인: GitHub Actions로 Node.js CI/CD에서 이미지 배포 후 Nginx는 설정 리로드(nginx -s reload)만 수행.

트러블슈팅

증상원인해결
502 Bad GatewayNode 미기동·포트 불일치ss -tlnp, 컨테이너 로그 확인
리다이렉트가 http로 감X-Forwarded-Proto 누락proxy_set_header X-Forwarded-Proto $scheme
WebSocket 끊김Upgrade 헤더 미전달socket.io 예시처럼 설정
실제 IP가 모두 Nginxtrust proxy 미설정Express trust proxy, 로그는 $http_x_forwarded_for 참고
업로드 실패 413client_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) 점검합니다.