Nginx란?
웹 서버이자 리버스 프록시이자 로드 밸런서로 사용되는 고성능 네트워크 서버 소프트웨어이다.
클라이언트 요청을 가장 앞에서 받아서, 빠르게 처리하거나 다른 서버로 전달해주는 Gateway 역할을 한다.
[ Internet ]
↓
[ Nginx ]
↓
[ Application Server ]
↓
[ DB / Cache ]
Nginx의 역할
Nginx는 크게 3가지 역할로 쓰인다.
- 정적 파일 서버: HTML/CSS/JS, 이미지 같은 파일을 빠르게 서빙
- 리버스 프록시(Reverse Proxy): 브라우저 요청을 받아서 내부 서비스(예: Spring, Next, FastAPI)로 전달
- 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 매칭 우선순위
- location = /exact (완전 일치가 최우선)
- location ^~ /prefix/ (이런 prefix면 정규식 무시)
- location ~ regex (정규식)
- location /prefix/ (가장 긴 prefix가 선택)
- 마지막으로 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로 들어오면
- /api/ location이 선택됨
- location 안에서 proxy_pass 실행
- api_backend upstream 참조
- 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은 처음부터 별도 프로토콜로 시작하는 게 아니라:
- 처음엔 일반 HTTP 요청으로 시작
- 서버가 “업그레이드”를 승인하면 그 순간부터 같은 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 |
|---|