본문 바로가기
Deployment/CICD

Nginx 파일 작성법

by curious week 2026. 1. 2.

Nginx란?

웹 서버이자 리버스 프록시이자 로드 밸런서로 사용되는 고성능 네트워크 서버 소프트웨어이다.

클라이언트 요청을 가장 앞에서 받아서, 빠르게 처리하거나 다른 서버로 전달해주는 Gateway 역할을 한다.

[ Internet ]
     ↓
[ Nginx ]
     ↓
[ Application Server ]
     ↓
[ DB / Cache ]

Nginx의 역할

Nginx는 크게 3가지 역할로 쓰인다.

  1. 정적 파일 서버: HTML/CSS/JS, 이미지 같은 파일을 빠르게 서빙
  2. 리버스 프록시(Reverse Proxy): 브라우저 요청을 받아서 내부 서비스(예: Spring, Next, FastAPI)로 전달
  3. L7 로드밸런서: 여러 백엔드 인스턴스로 트래픽 분산(라운드로빈, least_conn 등)
어떤 요청을 잡을지 location
어디로 보낼지 proxy_pass
로드밸런싱 upstream
URI 변형 proxy_pass
인증/차단 location

Nginx의 요청 처리 흐름

'https://api.spring.com/api/todos?limit=10'으로 요청한다고 가정한다.

TCP 연결

  • 클라이언트가 443 포트로 연결을 시도하면,
  • Nginx는 listen 443 ssl;로 받은 소켓에서 연결을 수용해야한다
  • TLS 인증서/키를 바탕으로 암호화 채널을 만든 뒤에야 HTTP 요청을 읽기 시작함

어떤 server{} 블록이 받을지 결정: listen(포트), server_name(도메인)으로 매칭

server {
  listen 443 ssl;
  server_name api.spring.com;
}

그 다음 어떤 location{} 블록이 처리할지 결정: location /api/ 같은 prefix, 정규식 location 등 규칙으로 매칭

server {
  location /api/ { ... }
  location / { ... }
}

location 안의 지시자에 따라 행동:

1. 정적 파일로 응답하거나

location / {
  root /usr/share/nginx/html;
  try_files $uri $uri/ /index.html;
}
  • $uri 파일이 있으면 그 파일 반환
  • 없으면 $uri/ 디렉터리(그리고 index) 확인
  • 그것도 없으면 /index.html 반환(= SPA 라우팅 지원)

2. 리버스 프록시로 (Nginx가 대신 요청을 만들어서) 백엔드에 전달하거나

  • 백엔드 주소 결정(backend는 upstream(요청을 보낼 대상(백엔드)들의 묶음) 또는 DNS)
  • 백엔드에 HTTP 요청을 새로 만들어 전송
  • 백엔드 응답을 받아서 클라이언트에 전달
location /api/ {
  proxy_pass http://backend; 백엔드에 /api/todos 그대로 전달
  proxy_pass http://backend/; (앞의 /api/가 제거)백엔드에 /todos로 전달
  
  # 기본적으로 프록시 환경에서는 거의 다음을 넣음.
  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;
}

3. 리다이렉트/차단/인증 등을 수행

응답 생성/가공(압축, 헤더 추가 등)

  • 응답 헤더/바디 수신
  • 필요하면 헤더 추가/수정
  • gzip 같은 압축 적용(설정되어 있으면)

클라이언트에 응답 전송 + keep-alive 여부 결정

  • 클라이언트로 전송
  • 커넥션 유지(keep-alive) 여부 판단

로그(access/error) 기록

  • access log: 요청이 들어왔고 응답 코드를 무엇으로 보냈는지
  • error log: 설정/프록시/DNS/업스트림 오류 등

자주 보는 케이스

  • 404: location/try_files/root 문제
  • 502 Bad Gateway: 백엔드 연결 실패(컨테이너 죽음, 포트, DNS)
  • 504 Gateway Timeout: 백엔드가 응답 늦음(proxy_read_timeout)
  • 499: 클라이언트가 먼저 끊음(브라우저/프론트 타임아웃)

설정 파일 구조

/etc/nginx/
├── nginx.conf
├── conf.d/
│   └── app.example.com.conf   # server 뼈대
└── locations/
    ├── app.static.conf        # location / (정적/SPA)
    ├── app.api.conf           # location /api/
    └── app.ws.conf            # location /ws/

Nginx는 실행될 때 보통 메인 설정 파일 1개를 기준으로 시작

  • 일반 리눅스 패키지 설치: /etc/nginx/nginx.conf
  • 도커 이미지(공식 nginx): /etc/nginx/nginx.conf + /etc/nginx/conf.d/*.conf

include ...;가 있으면, 거기서 지정한 파일들을 텍스트 그대로 이어붙여서 읽는다.

  • main: Nginx 프로세스 자체(워커 수, 사용자, pid 등)
  • events: 커넥션 처리 방식(워커가 커넥션을 얼마나 잡는지 등)
  • http: HTTP 전역 정책(로그 포맷, gzip, 타임아웃, 프록시 공통 설정)
  • server: 사이트/도메인 단위(포트/도메인/인증서)
  • location: 경로 단위 처리(/api, /static, /ws)
# 기본 nginx.conf
# ---- main context ----
user  nginx;
worker_processes auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  worker_connections 1024;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  /var/log/nginx/access.log  main;

  sendfile        on;
  keepalive_timeout  65;

  # 여기서 server / upstream 등을 include
  include /etc/nginx/conf.d/*.conf;
}

 

  • http {} 안에 서버 설정을 include하는 게 일반적이고, server {}를 http {} 밖에 쓰면 안 된다.
  • 보통 conf.d/*.conf에는 보통 server 블록만 둔다(권장)

- 각 레벨에서 가능한 지시자가 들어가게 작성

1) http 전역 공통(모든 서비스에 적용) → http {} 안, 혹은 http 컨텍스트에서 include 되는 파일로

  • gzip/브로틀리
  • 공통 타임아웃
  • 공통 보안 헤더
  • 공통 로그 포맷
  • 업스트림(백엔드 그룹)
  • map 변수(예: WebSocket Connection 헤더)

2) server(도메인 단위) → server 단위 파일로

  • listen
  • server_name
  • TLS 인증서
  • 도메인 리다이렉트(예: www → non-www)
  • 해당 도메인의 access_log 분리

3) location(경로 단위) → 해당 server 파일 안에서, 필요하면 location include로 쪼갬

  • 정적 파일 서빙
  • /api/ 프록시
  • /ws/ 웹소켓 프록시
  • 캐시 정책(정적에만)
  • 특정 경로 인증/차단
# 문법 검사
nginx -t
# 최종 합쳐진 전체 설정 출력
nginx -T

location 매칭 규칙

location 매칭 우선순위

  1. location = /exact (완전 일치가 최우선)
  2. location ^~ /prefix/ (이런 prefix면 정규식 무시)
  3. location ~ regex (정규식)
  4. location /prefix/ (가장 긴 prefix가 선택)
  5. 마지막으로 location / 같은 기본

① location = /exact

  • 완전 일치
  • 발견되는 순간 즉시 종료
  • 다른 location 전부 무시
location = /health { return 200 "ok"; }

/health 요청 → 무조건 여기로 온다.
/health/ ❌ (/ 다르면 매칭이 안 됨)

② prefix location 중 가장 긴 것 탐색

이 단계에서 두 종류의 prefix가 함께 고려돼.

location /api/ { ... } 
location ^~ /static/ { ... } 
location / { ... }

/api/foo → /api/가 현재 후보가 됨.
/static/js/app.js → /static/가 후보가 됨.

이 단계에서는 정규식은 안 봄

③ 만약 선택된 prefix가 ^~라면

  • 정규식 location 검사 자체를 건너뜀
  • 바로 해당 prefix location 확정
location ^~ /static/ { 
	root /assets; 
}
# /static/something은 아래 같은 정규식이 있어도 절대 안 탐
location ~ \.js$ { ... } # 무시됨

④ ^~가 아니었다면 → 정규식 location 검사

location ~ \.js$ { ... }
location ~ ^/api/.* { ... }
  • 정규식은 위에서부터 순서대로 검사
  • 처음 매칭되는 정규식으로 감. 구체적인 것보다 “먼저 나온 정규식”을 우선함. -> 여기서 많은 설정 사고가 난다

⑤ 정규식이 하나라도 매칭되면

  • 정규식 location이 prefix보다 우선

⑥ 정규식이 하나도 매칭되지 않으면

  • ②단계에서 골랐던 가장 긴 prefix location이 최종 선택

upstream이란? 

upstream은 요청을 보낼 대상(백엔드)들의 묶음, location은 요청을 어떻게 처리할지에 대한 규칙

upstream은 절대 요청을 직접 받지 않는다, URI 개념이 없다, location은 upstream 이름만 알면 된다

 

  • 어떤 서버들로 보낼지
  • 여러 개면 어떻게 분산할지
    • round-robin (기본)
    • least_conn
    • ip_hash
  • 서버가 죽었는지/살아있는지 판단
upstream api_backend {
  server api-1:8080;
  server api-2:8080;
}

server {
  location /api/ {
    proxy_pass http://api_backend;
  }
}

요청이 /api/users로 들어오면

  1. /api/ location이 선택됨
  2. location 안에서 proxy_pass 실행
  3. api_backend upstream 참조
  4. api-1 또는 api-2 중 하나로 요청 전달

upstream 사용 이유

1. 로드밸런싱

upstream backend {
  least_conn;
  server app1:8080;
  server app2:8080;
}

2. 서버 교체, 확장에 유리함. location은 그대로 두고 upstream만 수정.

3. 설정 가독성

proxy_pass http://10.0.0.12:8080;
// upstream을 사용하면 훨씬 가독성이 좋음.
proxy_pass http://api_backend;

upstream이 꼭 필요한 경우

  • 백엔드가 여러 개
  • 로드밸런싱 필요
  • 서버 교체/확장 잦음
  • 운영 환경

리버스 프록시

리버스 프록시란? 클라이언트 대신 Nginx가 백엔드 서버와 통신해주는 구조

클라이언트 입장에서는
Browser ──HTTP──▶ Nginx
실제 내부에서 동작은
Browser ─▶ Nginx ─▶ Backend

Nginx는 클라이언트의 요청을 그대로 전달하지 않는다.
Nginx가 “새 요청”을 만들어서 백엔드에 보낸다.


필요한 헤더 & proxy 옵션

백엔드는

  • “요청을 누가 보냈는지”
  • “원래 어떤 도메인/프로토콜이었는지”
  • “진짜 클라이언트 IP는 누구인지”

 기본적으로 모른다. 이 정보를 헤더로 다시 심어줘야 한다

거의 필수인 프록시 헤더

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 : 외부에서 본 도메인을 그대로 전달하는 용도

proxy_set_header Host $host;

이게 없으면 백엔드는 Host를 backend:8080 같은 내부 주소로 인식하고, 도메인 기반 로직이 깨진다.

  • Spring에서:
    • 절대 URL 생성
    • OAuth redirect URI
    • 멀티 도메인 서비스
  • HTTPS → HTTP 프록시 구조에서 origin 판단

② X-Real-IP : 실제 클라이언트 IP 하나만 전달

proxy_set_header X-Real-IP $remote_addr;
  • 로그
  • 간단한 IP 추적
  • 보안 필터

③ X-Forwarded-For :프록시 체인을 모두 기록 (client, proxy1, proxy2 ...)

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

 

  • 여러 프록시를 거치는 환경(Cloudflare → Nginx → App)에서 더 중요
  • 보안/감사 로그
  • 레이트 리미팅

X-Forwarded-For가 표준

④ X-Forwarded-Proto : TLS 종단이 Nginx일 때 필수

proxy_set_header X-Forwarded-Proto $scheme;

 

  • 이게 없으면 외부는 HTTPS로 내부 백엔드는 HTTP, 백엔드는 “내가 HTTP로 호출됐구나”라고 착각한다.
  • 작성하지 않으면, Spring에서 HTTPS 리다이렉트 무한 루프, OAuth callback URL mismatch, 보안 쿠키(Secure) 깨짐이 발생할 수 있다.

⑤ 자주 추가하는 보조 헤더들

클라이언트 정보 유지

proxy_set_header User-Agent $http_user_agent;
proxy_set_header Referer $http_referer;

WebSocket용 (필수)

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_* timeout 옵션의 의미

전체 프록시 요청 흐름

Nginx
  ├─ (1) 백엔드에 연결 시도
  ├─ (2) 요청 전송
  ├─ (3) 응답 수신

각 단계마다 별도의 타이머가 있다.

① proxy_connect_timeout :백엔드 서버에 TCP 연결을 시도하는 최대 시간

proxy_connect_timeout 3s;
  • 실패하면 502 Bad Gateway
  • 짧게 가져가는 게 보통 좋다

문제가 되는 상황

  • 백엔드 컨테이너 죽음
  • 포트 틀림
  • 네트워크 문제
  • DB처럼 느린 연결이 아니라, “연결 자체가 안 됨”

② proxy_send_timeout: Nginx가 백엔드로 요청을 보내는 동안 기다리는 최대 시간

  • 요청 바디(POST/PUT 업로드 포함)
proxy_send_timeout 60s;
  • 실패하면 504 Gateway Timeout

문제가 되는 상황

  • 큰 파일 업로드
  • 느린 네트워크
  • proxy_request_buffering off 상태에서 백엔드가 늦게 읽을 때

③ proxy_read_timeout : 백엔드 응답을 기다리는 시간 (제일 중요)

  • 응답 전체가 아니라 “다음 바이트를 기다리는 시간”
  • “응답이 느린 API”의 대부분 문제는 여기서 난다
proxy_read_timeout 60s;
  • 백엔드 처리 시간이 길면 여기서 504 발생

문제가 되는 상황

  • DB 락, 외부 API 호출, 대용량 처리
  • 백엔드 로직 느리거나 proxy_read_timeout 너무 짧으면 504가 자주 발생할 수 있다.

proxy_buffering / proxy_request_buffering (중요)

기본값

proxy_buffering on;
proxy_request_buffering on;
  • 요청/응답을 Nginx가 먼저 다 받고
  • 그 다음 백엔드/클라이언트로 전달

끄는 경우

proxy_request_buffering off;
  • 대용량 스트리밍, 실시간 업로드 처리가 가능하지만
  • Nginx 메모리/보안 영향을 줄 수 있으므로 무작정 끄면 위험하다

기본 프록시 템플릿

location /api/ {
  proxy_pass http://backend;

  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_connect_timeout 3s;
  proxy_send_timeout 60s;
  proxy_read_timeout 60s;
}

정적 파일 라우팅

정적 파일 라우팅의 기본: root vs alias

  • “사이트 루트”를 서빙할 때는 보통 root
  • “특정 경로만 다른 디렉토리로 매핑”할 때는 alias

1) root: root는 “요청 URI를 파일 경로에 붙여서” 찾는다

location / {
  root /usr/share/nginx/html;
}

요청이 /assets/app.js면 실제 파일은: /usr/share/nginx/html/assets/app.js과 같이 location의 URI가 유지된 채로 root 뒤에 붙음.

2) alias: alias는 “location에 매칭된 prefix를 파일 경로로 대체”한다

location /static/ {
  alias /var/www/static/;
}

요청 /static/logo.png → 실제 파일: /var/www/static/logo.png

  • alias는 뒤에 슬래시 유무가 매우 중요
  • location /static/ ↔ alias /var/www/static/; 처럼 둘 다 trailing slash를 맞추는 게 안전함

try_files 정적 라우팅의 파일을 찾는 핵심 규칙

location / {
  root /usr/share/nginx/html;
  try_files $uri $uri/ =404;
}

 

  • $uri가 파일이면 반환
  • $uri/가 디렉터리면 (설정에 따라) 그 디렉터리의 index를 반환
  • 아니면 404

즉, 파일이 있으면 파일, 없으면 404.

SPA(React/Vue) 라우팅 지원: “없는 경로는 index.html로”

SPA는 /about 같은 경로가 사실 "파일 없음"이다. 브라우저에서 새로고침하면 서버는 /about 파일을 찾다가 404를 낸다.

location / {
  root /usr/share/nginx/html;
  try_files $uri $uri/ /index.html;
}
  • 실제 파일이 있으면 반환
  • 없으면 index.html을 반환
  • 그러면 프론트 라우터가 /about을 처리함

정적 캐싱: 이미지/폰트는 길게, HTML은 짧게 또는 no-cache

1) 해시된 정적 파일(webpack/vite 빌드 산출물)

app.3f9a2c.js처럼 파일명에 해시가 붙는 애들은 바뀌면 파일명이 바뀜 → 캐시를 길게 걸어도 안전.

location /assets/ {
  root /usr/share/nginx/html;
  expires 1y;
  add_header Cache-Control "public, immutable";
}

2) index.html은 길게 캐시하면 위험

배포 후에도 브라우저가 옛 index.html을 들고 있으면, 새 JS 파일을 못 찾는 문제가 생김.

location = /index.html {
  root /usr/share/nginx/html;
  add_header Cache-Control "no-cache";
}

 

흔하게 발생하는 /api 프록시와 SPA 라우팅 충돌

문제 상황:

  • SPA를 위해 try_files ... /index.html을 넣었더니 /api/todos도 index.html로 가버림
  • 결과: 백엔드 요청이 프론트 HTML을 받아서 깨짐

해결 방법:

server {
  # 1) API는 먼저 확정
  location ^~ /api/ {
    proxy_pass http://backend;
    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;
  }

  # 2) 나머지는 SPA
  location / {
    root /usr/share/nginx/html;
    try_files $uri $uri/ /index.html;
  }
}
  • ^~로 /api/를 정규식 location에 뺏기지 않게 확정
  • SPA fallback은 API보다 아래에

Next.js는 “정적 서빙”이 두 종류

  • “Nginx로 Next를 정적 서빙”하려면 export/정적 빌드인지 확인이 필요

A) next export 기반 “완전 정적 사이트”

  • /out 디렉토리 생성
  • Nginx로 root 서빙 가능

B) 일반 Next.js(App Router) 서버

  • Node 서버가 필요함
  • Nginx는 보통 리버스 프록시 역할만 함

템플릿: “정적 + SPA + 캐시 + API”  예시

server {
  listen 80;
  server_name _;

  # API 프록시
  location ^~ /api/ {
    proxy_pass http://backend;
    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;
  }

  # 해시 자산 캐시
  location ^~ /assets/ {
    root /usr/share/nginx/html;
    expires 1y;
    add_header Cache-Control "public, immutable";
    try_files $uri =404;
  }

  # index.html은 캐시 최소화
  location = /index.html {
    root /usr/share/nginx/html;
    add_header Cache-Control "no-cache";
  }

  # SPA 라우팅
  location / {
    root /usr/share/nginx/html;
    try_files $uri $uri/ /index.html;
  }
}

템플릿: “Nginx 기본 리버스 프록시 템플릿 (HTTP)”  예시

server {
  listen 80;
  server_name app.example.com;

  # 모든 요청을 Next.js로 전달
  location / {
    proxy_pass http://nextjs: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;

    # WebSocket / streaming 대비
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    proxy_read_timeout 60s;
  }
}
  • /, /_next, /api, RSC 요청 전부 Next가 직접 처리
  • Nginx는 경로 구분

 


WebSocket 프록시

WebSocket은 처음부터 별도 프로토콜로 시작하는 게 아니라:

  1. 처음엔 일반 HTTP 요청으로 시작
  2. 서버가 “업그레이드”를 승인하면 그 순간부터 같은 TCP 연결에서 WebSocket 프레임을 주고받는 방식으로 바뀜
Client  --HTTP 요청(Upgrade: websocket)-->  Nginx  -->  Backend
Client  <--101 Switching Protocols----------  Nginx  <-- Backend
(이후부터는 WebSocket 프레임이 양방향으로 계속 흐름)
  • “101 Switching Protocols”가 성공해야 한다 (응답이 101 Switching Protocols인지 확인)
  • 브라우저 개발자도구(Network → WS)에서 확인하거나, 서버 로그에서 업그레이드 헤더를 확인
  • 그 이후는 오래 유지되는 “롱 커넥션”이 된다

Nginx에서 WebSocket 프록시에 필요한 최소 조건

WebSocket 업그레이드가 실패하는 가장 흔한 이유는 Nginx가 업그레이드 헤더를 제대로 전달하지 않기 때문이다.

WebSocket 프록시 최소 템플릿

location /ws/ {
  proxy_pass http://backend;

  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 $scheme;
}
  • proxy_http_version 1.1
    WebSocket 업그레이드 헤더는 HTTP/1.1 기반 동작이기 때문에 안정적으로 1.1로 둠
  • Upgrade, Connection
    이 두 헤더가 WebSocket으로 업그레이드하고 싶다는 신호

Connection 헤더를 더 안전하게 쓰는 패턴

"upgrade"를 하드코딩하기보다, 업그레이드가 없으면 close로 처리하도록 map을 쓰는 경우가 많다.

http 컨텍스트에 추가

map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}

location에서 사용

proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
  • 평범한 HTTP 요청에서 Connection: upgrade를 억지로 넣는 실수를 줄임
  • WebSocket과 일반 HTTP가 섞여도 안정적

WebSocket은 타임아웃이 핵심

HTTP는 보통 요청/응답이 끝나면 연결을 끊거나 재사용하지만, WebSocket은 연결을 오래 잡고 있는다..

그래서 기본 proxy_read_timeout이 짧으면 자주 끊김.

location /ws/ {
  proxy_pass http://backend;

  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;

  proxy_read_timeout 1h;
  proxy_send_timeout 1h;
}
  • proxy_read_timeout: 백엔드에서 “다음 데이터”가 올 때까지 기다리는 시간
    WebSocket은 이벤트가 없으면 한동안 아무 것도 안 올 수 있으니 크게 잡는 게 일반적이다.
  • proxy_send_timeout: Nginx가 백엔드로 데이터를 보낼 때 타임아웃
  • “처음 연결은 되는데 60초 후 끊김” → proxy_read_timeout이 기본값/짧은 값이라 끊김

버퍼링/압축과 WebSocket

WebSocket은 스트리밍성, 즉시성이 중요하므로 일반 HTTP처럼 버퍼링하면 문제가 될 수 있다.

location /ws/ {
  proxy_pass http://backend;

  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;

  proxy_buffering off;
}
  • proxy_buffering off;를 WebSocket location에만 적용하는 경우가 많음

ping/pong keepalive

  • 클라이언트가 주기적으로 ping을 보내거나
  • 서버가 주기적으로 ping을 보내서
  • 중간 프록시의 idle timeout을 피함

예를 들어, 30초마다 ping을 보냄 (이건 Nginx 설정이라기보다 앱(WebSocket 서버)에서 처리)


HTTPS/TLS 처리

TLS termination이란?

클라이언트와 Nginx 사이에서만 HTTPS를 쓰고, Nginx와 백엔드 사이 통신은 HTTP로 두는 구조

  • 브라우저 ↔ Nginx: HTTPS
  • Nginx ↔ 백엔드: HTTP

이렇게 하면 인증서 관리를 한 곳(Nginx)에서만 하면 되고, 백엔드들은 단순 HTTP로 운영 가능하고, 로드밸런서/게이트웨이에서 보안/정책을 일괄 적용 가능

내부 통신까지 TLS를 하는 경우도 있는데(“end-to-end TLS”), 시작 단계에선 termination이 훨씬 일반적이다.

Nginx HTTPS 기본 구성: 80에서 443으로 보내고, 443에서 서비스

(A) HTTP(80) → HTTPS(443) 리다이렉트 서버

server {
  listen 80;
  server_name app.example.com;

  return 301 https://$host$request_uri;
}
  • 301은 영구 리다이렉트(브라우저가 캐시할 수 있음)
  • 테스트 단계에서 리다이렉트 정책 바꾸면 캐시 때문에 헷갈릴 수 있음

(B) HTTPS(443) 서버

server {
  listen 443 ssl http2;
  server_name app.example.com;

  ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

  location / {
    proxy_pass http://nextjs:3000;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
  }
}
  • ssl_certificate는 보통 fullchain.pem (체인 포함)
  • 백엔드가 HTTPS였음을 알게 하려면 X-Forwarded-Proto https가 중요

인증서 파일 3종 세트: fullchain, cert, privkey

Let’s Encrypt(무료 TLS 인증서 제공) 기준으로 많이 보는 파일:

  • privkey.pem : 서버 개인키(절대 노출 금지)
  • cert.pem : 서버 인증서
  • chain.pem : 중간 인증서 체인
  • fullchain.pem : cert.pem + chain.pem 합친 것

Nginx는 보통:

  • ssl_certificate → fullchain.pem
  • ssl_certificate_key → privkey.pem

어떤 클라이언트는 중간 인증서를 서버가 제공해주길 기대한다. cert.pem만 쓰면 일부 환경에서 “인증서 신뢰 실패”가 뜰 수 있다.

TLS 버전/암호군/HTTP2: “안전한 보안 기본값”으로 시작

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
  • TLS 1.2/1.3만 허용하는 게 일반적인 기본
  • 세션 캐시는 핸드셰이크 비용을 줄여서 속도에 도움
  • 세션 티켓은 운영 정책에 따라 끄는 경우가 많음(키 관리 이슈)

HTTP/2는:

listen 443 ssl http2;

정도로 켜는 게 흔하고, 대부분 문제 없이 성능에 이점이 있다.

(주의 필요) HSTS: “HTTPS 강제”를 브라우저에 각인시키는 기능 

HSTS는 브라우저에게: 이 도메인은 앞으로 무조건 HTTPS로만 접속하라고 강제하는 헤더다.

add_header Strict-Transport-Security "max-age=31536000" always;
  • 한 번 브라우저에 저장되면 쉽게 되돌리기 어렵다
  • 초기엔 max-age를 짧게(예: 300초)로 테스트하고 늘리는 걸 추천

서브도메인까지 강제하려면:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

 

프록시 뒤 백엔드가 “HTTPS 요청”으로 인식하게 만들기

TLS termination 구조에서는 백엔드가 실제로는 HTTP 요청만 받게 된다. 그런데 백엔드가 “원래는 HTTPS였음”을 알아야 할 때가 많다.

꼭 전달하는 헤더

proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
  • 로그인/리다이렉트 URL이 http로 생성됨
  • Secure 쿠키가 잘못 적용됨
  • OAuth callback URL mismatch
  • HTTPS 강제 리다이렉트 루프

이런 문제는 대개 X-Forwarded-Proto/Host 처리로 해결

WebSocket/SSE가 있을 때 HTTPS에서 추가

HTTPS 자체가 WebSocket을 막는 건 아니지만, 프록시 설정이 없으면 업그레이드가 안 된다.

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

 

nginx.conf : HTTP/2 + Next.js(static) + Spring Boot(/api)

user  nginx;
worker_processes auto;

# 전역 에러 로그
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
  # 워커 하나가 동시에 처리할 수 있는 최대 커넥션 수
  worker_connections 1024;
}

http {
  # MIME 타입
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  # 로그 포맷 (디버깅에 유용한 값 포함)
  log_format main '$remote_addr - $remote_user [$time_local] '
                  '"$request" $status $body_bytes_sent '
                  '"$http_referer" "$http_user_agent" '
                  'rt=$request_time uct=$upstream_connect_time '
                  'uht=$upstream_header_time urt=$upstream_response_time '
                  'uaddr=$upstream_addr';

  access_log /var/log/nginx/access.log main;

  # 성능/기본 옵션
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  keepalive_timeout 65;

  # 업로드 제한(필요 시 조정)
  client_max_body_size 20m;

  # gzip (기본 압축 설정)
  gzip on;
  gzip_comp_level 5;
  gzip_min_length 1024;
  gzip_types
    text/plain
    text/css
    application/json
    application/javascript
    application/xml
    image/svg+xml;

  # WebSocket Upgrade용 안전한 Connection 값
  map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
  }

  # Upstream: Spring Boot API
  # - Docker Compose면 spring 서비스명 사용 가능, 로컬이면 127.0.0.1:8080 등으로 변경해야 함
  upstream spring_api {
    server spring:8080;
    keepalive 32;
  }

  # HTTP :80 -> HTTPS :443 redirect
  server {
    listen 80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
  }

  # HTTPS :443 (HTTP/2 enabled)
  server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # ---- TLS certificates (Let's Encryp에서 인증키) ----
    # 실제 경로로 바꿔서 사용
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # ---- TLS baseline ----
    ssl_protocols TLSv1.2 TLSv1.3;

    # 세션 캐시(성능)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # (초기에는 짧게 테스트 후 늘리는 것을 추천)
    add_header Strict-Transport-Security "max-age=300" always;

    location ^~ /api/ {
      proxy_pass http://spring_api;

      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_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;

      proxy_connect_timeout 3s;
      proxy_send_timeout    60s;
      proxy_read_timeout    60s;
    }

    root /usr/share/nginx/html;
    index index.html;

    # (1) 해시 자산 캐시 (Next export에서도 assets가 있을 수 있음)
    # 경로는 프로젝트 산출물에 맞춰 조정: ex) /_next/static/ 또는 /assets/ 
    location ^~ /_next/ {
      try_files $uri =404;
      expires 1y;
      add_header Cache-Control "public, immutable";
    }

    # (2) index.html 캐시는 짧게/무효화 (배포 꼬임 방지)
    location = /index.html {
      add_header Cache-Control "no-cache";
      try_files /index.html =404;
    }

    # (3) SPA 라우팅 fallback
    # 파일이 있으면 파일, 없으면 index.html
    location / {
      try_files $uri $uri/ /index.html;
    }
  }
}
  • Next.js 정적 사이트는 Nginx가 직접 서빙 (root, try_files)
  • Spring Boot는 /api/로만 프록시
  • HTTP/2는 listen 443 ssl http2;로 활성화
  • /api/는 ^~로 먼저 확정해서 SPA fallback이 먹지 않게 함

 

Nginx 자주 쓰는 명령어

설정 문법/적용 상태 확인

nginx -t
nginx -T
  • -t: 문법 검사(실제 적용 전에 필수)
  • -T: include까지 합쳐서 “전체 설정” 출력(디버깅 최강)

프로세스 제어 (systemd 환경)

sudo systemctl status nginx
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx
  • restart: 프로세스를 재시작(연결 끊길 수 있음)
  • reload: 설정만 다시 읽음(보통 무중단)

Nginx 자체 시그널(컨테이너/비-systemd 환경에서 유용)

nginx -s reload
nginx -s quit
nginx -s stop
nginx -s reopen
  • reload: 설정 리로드
  • quit: graceful shutdown(연결 정리)
  • stop: 즉시 종료
  • reopen: 로그 파일 재오픈(로그 로테이션에 사용)

로그 확인

tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

'Deployment > CICD' 카테고리의 다른 글

Docker file과 docker-compose, 명령어  (0) 2025.12.29