- Published on
Nginx 설정 완벽 가이드: 아키텍처부터 프로덕션 최적화까지 15가지 핵심 주제
- Authors
- Name
- 1. Nginx 아키텍처와 설정 구조
- 2. Virtual Host / Server Block 설정
- 3. 리버스 프록시 설정
- 4. 로드 밸런싱
- 5. SSL/TLS 설정
- 6. 캐싱 설정
- 7. Rate Limiting과 Connection Limiting
- 8. Gzip/Brotli 압축
- 9. 보안 헤더
- 10. 접근 제어 (Access Control)
- 11. 로깅 설정
- 12. 성능 튜닝
- 13. URL Rewriting과 Redirection
- 14. 정적 파일 서빙 최적화
- 15. 헬스체크와 모니터링
- 종합 프로덕션 설정 체크리스트
- 참고 자료
1. Nginx 아키텍처와 설정 구조
1.1 이벤트 기반 아키텍처: Master-Worker 모델
Nginx는 Apache httpd의 프로세스/스레드 기반 모델과 근본적으로 다른 이벤트 기반(Event-Driven) 아키텍처를 채택했다. 이 설계 철학이 Nginx가 단일 서버에서 수십만 개의 동시 접속을 처리할 수 있는 핵심 이유다.
┌─────────────────────────────────────────────────────────┐
│ Master Process │
│ - 설정 파일 읽기 및 검증 │
│ - Worker 프로세스 생성/관리 (fork) │
│ - 포트 바인딩 (80, 443) │
│ - 시그널 처리 (reload, stop, reopen) │
└─────────┬───────────┬───────────┬───────────┬───────────┘
│ │ │ │
┌─────▼─────┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│ Worker 0 │ │Worker 1│ │Worker 2│ │Worker 3│
│ Event Loop │ │ ... │ │ ... │ │ ... │
│ epoll/kq │ │ │ │ │ │ │
│ 수천 conn │ │ │ │ │ │ │
└───────────┘ └────────┘ └────────┘ └────────┘
Master Process는 root 권한으로 실행되며 설정 파일 파싱, 포트 바인딩, Worker 프로세스 관리를 담당한다. fork() 시스템 콜로 Worker를 생성하며, 설정 리로드 시 기존 연결을 끊지 않고 새로운 Worker를 띄운 뒤 기존 Worker를 graceful shutdown한다.
Worker Process는 실제 클라이언트 요청을 처리하는 핵심 단위다. 각 Worker는 독립적인 이벤트 루프를 운영하며, OS의 I/O 다중화 메커니즘(epoll on Linux, kqueue on FreeBSD/macOS)을 활용해 블로킹 없이 수천 개의 연결을 동시에 처리한다. 커넥션마다 스레드를 할당하는 방식 대비 컨텍스트 스위칭과 메모리 오버헤드가 극적으로 줄어든다.
1.2 nginx.conf 설정 컨텍스트 구조
Nginx 설정은 계층적 컨텍스트(Context) 구조를 따른다. 자식 컨텍스트는 부모의 설정을 상속하며, 동일한 디렉티브를 자식에서 재선언하면 오버라이드된다.
# ============================================
# Main Context (전역 설정)
# ============================================
user nginx; # Worker 프로세스 실행 사용자
worker_processes auto; # CPU 코어 수만큼 Worker 생성
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
# ============================================
# Events Context (연결 처리 설정)
# ============================================
events {
worker_connections 1024; # Worker당 최대 동시 연결 수
multi_accept on; # 한 번에 여러 연결 수락
use epoll; # Linux에서 epoll 사용 (기본값)
}
# ============================================
# HTTP Context (HTTP 프로토콜 설정)
# ============================================
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ========================================
# Server Context (가상 호스트)
# ========================================
server {
listen 80;
server_name example.com;
# ====================================
# Location Context (URL 경로 매칭)
# ====================================
location / {
root /var/www/html;
index index.html;
}
location /api/ {
proxy_pass http://backend;
}
}
}
# ============================================
# Stream Context (TCP/UDP 프록시, L4)
# ============================================
stream {
server {
listen 3306;
proxy_pass mysql_backend;
}
}
컨텍스트 계층 요약:
| 컨텍스트 | 위치 | 역할 |
|---|---|---|
| Main | 최상위 | 전역 설정 (user, worker, pid, error_log) |
| Events | Main 내부 | 연결 처리 메커니즘 (worker_connections) |
| HTTP | Main 내부 | HTTP 프로토콜 관련 전체 설정 |
| Server | HTTP 내부 | 가상 호스트 (도메인별 설정) |
| Location | Server 내부 | URL 경로별 요청 처리 규칙 |
| Upstream | HTTP 내부 | 백엔드 서버 그룹 (로드 밸런싱) |
| Stream | Main 내부 | TCP/UDP L4 프록시 |
1.3 설정 파일 구조화 Best Practice
프로덕션 환경에서는 단일 nginx.conf에 모든 설정을 넣지 않고, 모듈화하여 관리한다.
/etc/nginx/
├── nginx.conf # 메인 설정 (include로 분리)
├── conf.d/ # 공통 설정 스니펫
│ ├── ssl-params.conf # SSL/TLS 공통 파라미터
│ ├── proxy-params.conf # 리버스 프록시 공통 헤더
│ ├── security-headers.conf # 보안 헤더
│ └── gzip.conf # 압축 설정
├── sites-available/ # 사이트별 설정 파일
│ ├── example.com.conf
│ ├── api.example.com.conf
│ └── admin.example.com.conf
├── sites-enabled/ # 활성화된 사이트 (심볼릭 링크)
│ ├── example.com.conf -> ../sites-available/example.com.conf
│ └── api.example.com.conf -> ../sites-available/api.example.com.conf
└── snippets/ # 재사용 가능한 설정 조각
├── letsencrypt.conf
└── fastcgi-php.conf
# nginx.conf 메인 파일
http {
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*.conf;
}
2. Virtual Host / Server Block 설정
Nginx의 Server Block은 Apache의 Virtual Host에 해당하며, 하나의 서버에서 여러 도메인을 호스팅할 수 있게 한다.
2.1 기본 Server Block 설정
# /etc/nginx/sites-available/example.com.conf
# ── 기본 도메인 ──
server {
listen 80;
listen [::]:80; # IPv6 지원
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm;
# 접근 로그를 도메인별로 분리
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
}
# ── 두 번째 도메인 ──
server {
listen 80;
listen [::]:80;
server_name blog.example.com;
root /var/www/blog.example.com/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
2.2 Default Server (catch-all)
정의되지 않은 도메인으로 들어오는 요청을 처리하는 기본 서버 블록이다. 보안을 위해 444(연결 끊기)로 응답하는 것이 권장된다.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _; # 모든 미매칭 도메인
# 정의되지 않은 호스트 요청은 즉시 연결 종료
return 444;
}
2.3 Server Name 매칭 우선순위
Nginx는 server_name 매칭 시 다음 우선순위를 따른다:
- 정확한 이름:
server_name example.com - 앞쪽 와일드카드:
server_name *.example.com - 뒤쪽 와일드카드:
server_name example.* - 정규표현식:
server_name ~^(?<subdomain>.+)\.example\.com$ - default_server: 위 모두 미매칭 시
# 정규표현식으로 서브도메인 캡처
server {
listen 80;
server_name ~^(?<subdomain>.+)\.example\.com$;
location / {
root /var/www/$subdomain;
}
}
2.4 사이트 활성화/비활성화
# 사이트 활성화
sudo ln -s /etc/nginx/sites-available/example.com.conf \
/etc/nginx/sites-enabled/example.com.conf
# 설정 검증
sudo nginx -t
# 리로드 (무중단)
sudo systemctl reload nginx
3. 리버스 프록시 설정
3.1 기본 Reverse Proxy
server {
listen 80;
server_name app.example.com;
location / {
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
}
각 헤더의 역할:
| 헤더 | 목적 |
|---|---|
Host | 원본 요청의 Host 헤더 전달 |
X-Real-IP | 실제 클라이언트 IP (프록시 뒤에서 원본 IP 식별) |
X-Forwarded-For | 프록시 체인을 거치며 누적되는 클라이언트 IP 목록 |
X-Forwarded-Proto | 원본 프로토콜 (http/https) — 백엔드에서 리다이렉트 판단 |
X-Forwarded-Host | 원본 Host 헤더 |
X-Forwarded-Port | 원본 포트 |
3.2 재사용 가능한 프록시 파라미터 스니펫
# /etc/nginx/conf.d/proxy-params.conf
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
# Server Block에서 include로 재사용
location / {
proxy_pass http://backend;
include /etc/nginx/conf.d/proxy-params.conf;
}
3.3 WebSocket 프록시
WebSocket은 HTTP Upgrade 메커니즘을 사용하므로, Upgrade와 Connection 홉별(hop-by-hop) 헤더를 명시적으로 전달해야 한다. Nginx는 기본적으로 이 헤더들을 포워딩하지 않는다.
# ── map으로 Connection 헤더를 동적 설정 ──
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ws.example.com;
location /ws/ {
proxy_pass http://127.0.0.1:8080;
# WebSocket 필수 설정
proxy_http_version 1.1; # HTTP/1.1 필수 (Upgrade 지원)
proxy_set_header Upgrade $http_upgrade; # 클라이언트의 Upgrade 헤더 전달
proxy_set_header Connection $connection_upgrade; # 동적 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;
# WebSocket은 장시간 연결이므로 타임아웃 연장
proxy_read_timeout 86400s; # 24시간 (기본 60초는 idle 연결 끊김)
proxy_send_timeout 86400s;
}
}
3.4 경로 기반 라우팅 (마이크로서비스)
server {
listen 80;
server_name api.example.com;
# 사용자 서비스
location /api/users/ {
proxy_pass http://user-service:3001/; # 끝에 / 주의: /api/users/ 제거 후 전달
include /etc/nginx/conf.d/proxy-params.conf;
}
# 주문 서비스
location /api/orders/ {
proxy_pass http://order-service:3002/;
include /etc/nginx/conf.d/proxy-params.conf;
}
# 결제 서비스
location /api/payments/ {
proxy_pass http://payment-service:3003/;
include /etc/nginx/conf.d/proxy-params.conf;
proxy_read_timeout 120s; # 결제는 타임아웃 연장
}
}
주의:
proxy_passURL 끝에/가 있으면location에 매칭된 부분이 제거된다./api/users/123요청이http://user-service:3001/123으로 전달된다./가 없으면 전체 URI가 그대로 전달된다.
4. 로드 밸런싱
4.1 Upstream 블록과 알고리즘
# ============================================
# 1. Round Robin (기본값)
# 요청을 순차적으로 분배
# ============================================
upstream backend_roundrobin {
server 10.0.0.1:8080; # 기본 가중치 1
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
# ============================================
# 2. Weighted Round Robin (가중 라운드 로빈)
# 서버 성능에 따라 비율 분배
# ============================================
upstream backend_weighted {
server 10.0.0.1:8080 weight=5; # 요청의 5/8
server 10.0.0.2:8080 weight=2; # 요청의 2/8
server 10.0.0.3:8080 weight=1; # 요청의 1/8
}
# ============================================
# 3. Least Connections (최소 연결)
# 활성 연결이 가장 적은 서버로 전달
# ============================================
upstream backend_leastconn {
least_conn;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
# ============================================
# 4. IP Hash (세션 고정)
# 동일 클라이언트 IP → 동일 서버
# ============================================
upstream backend_iphash {
ip_hash;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
# ============================================
# 5. Generic Hash (커스텀 해시)
# 임의 변수 기반 해시
# ============================================
upstream backend_hash {
hash $request_uri consistent; # URI 기반 + 일관적 해싱
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
알고리즘 선택 가이드:
| 알고리즘 | 적합한 상황 | 주의점 |
|---|---|---|
| Round Robin | 무상태 서비스, 동일 스펙 서버 | 서버 스펙 차이 시 불균형 |
| Weighted | 서버 스펙이 다른 경우 | 가중치 수동 관리 필요 |
| Least Connections | 요청 처리 시간 편차가 큰 경우 | 짧은 요청 위주면 Round Robin과 유사 |
| IP Hash | 세션 고정이 필요한 경우 (레거시 앱) | 서버 추가/제거 시 재분배 발생 |
| Generic Hash | 캐시 최적화 (동일 URI → 동일 서버) | consistent 해싱 권장 |
4.2 서버 상태 및 백업
upstream backend {
least_conn;
server 10.0.0.1:8080; # 정상 서버
server 10.0.0.2:8080; # 정상 서버
server 10.0.0.3:8080 backup; # 백업: 위 서버 모두 다운 시 활성화
server 10.0.0.4:8080 down; # 비활성화 (유지보수)
server 10.0.0.5:8080 max_fails=3 fail_timeout=30s;
# max_fails=3: 30초 내 3회 실패 시 비정상 판정
# fail_timeout=30s: 비정상 판정 후 30초간 요청 제외
}
4.3 Keepalive 커넥션 풀
백엔드 서버와의 TCP 연결을 재사용하여 연결 설정/해제 오버헤드를 줄인다.
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 32; # 각 Worker당 유지할 유휴 커넥션 수
keepalive_requests 1000; # 하나의 커넥션으로 처리할 최대 요청 수
keepalive_timeout 60s; # 유휴 커넥션 유지 시간
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # keepalive는 HTTP/1.1 필수
proxy_set_header Connection ""; # "close" 대신 빈 값으로 keepalive 활성화
}
}
5. SSL/TLS 설정
5.1 기본 HTTPS 설정
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# ── 인증서 ──
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ── 프로토콜 ──
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.0, 1.1 비활성화
# ── Cipher Suites (TLS 1.2용) ──
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off; # TLS 1.3에서는 off 권장
# ── Elliptic Curves ──
ssl_ecdh_curve X25519:secp384r1:secp256r1;
# ── 세션 재사용 ──
ssl_session_cache shared:SSL:10m; # 10MB = ~40,000 세션
ssl_session_timeout 1d; # 세션 유효 시간
ssl_session_tickets off; # Forward Secrecy 보장
root /var/www/example.com/html;
index index.html;
}
5.2 HTTP → HTTPS 리다이렉트
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# 모든 HTTP 요청을 HTTPS로 301 리다이렉트
return 301 https://$host$request_uri;
}
5.3 HSTS (HTTP Strict Transport Security)
# HTTPS server block 내에 추가
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
max-age=63072000: 2년간 HTTPS만 사용 (최소 1년 권장)includeSubDomains: 모든 서브도메인에도 적용preload: 브라우저의 HSTS Preload 목록 등록 자격always: 오류 응답(4xx, 5xx)에서도 헤더 전송
5.4 OCSP Stapling
OCSP Stapling은 인증서 유효성 검증을 서버가 대행하여 클라이언트의 CA 직접 조회를 제거한다. 초기 연결 속도가 개선되고 개인정보가 보호된다.
ssl_stapling on;
ssl_stapling_verify on;
# OCSP 응답 검증용 신뢰 체인 (Let's Encrypt 중간 인증서 포함)
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# OCSP 응답서버 조회용 DNS resolver
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
5.5 프로덕션 SSL 통합 스니펫
# /etc/nginx/conf.d/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_ecdh_curve X25519:secp384r1:secp256r1;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Server Block에서 include
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/nginx/conf.d/ssl-params.conf;
# ... 나머지 설정
}
5.6 Let's Encrypt 자동 갱신 (Certbot)
# 인증서 발급
sudo certbot --nginx -d example.com -d www.example.com
# 자동 갱신 테스트
sudo certbot renew --dry-run
# Cron 자동 갱신 (이미 certbot이 설정하지만 명시적으로)
echo "0 0,12 * * * root certbot renew --quiet --deploy-hook 'systemctl reload nginx'" \
| sudo tee /etc/cron.d/certbot-renew
6. 캐싱 설정
6.1 Proxy Cache (리버스 프록시 캐시)
# ── HTTP Context에서 캐시 경로 정의 ──
proxy_cache_path /var/cache/nginx/proxy
levels=1:2 # 2단계 디렉토리 구조 (파일 분산)
keys_zone=proxy_cache:10m # 캐시 키 저장용 공유 메모리 (1MB ≈ 8,000 키)
max_size=1g # 디스크 캐시 최대 크기
inactive=60m # 60분간 미사용 캐시 삭제
use_temp_path=off; # 임시 파일 경로 미사용 (직접 기록 → 성능 향상)
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://backend;
proxy_cache proxy_cache; # 캐시 존 지정
proxy_cache_valid 200 302 10m; # 200, 302 응답 10분 캐싱
proxy_cache_valid 404 1m; # 404 응답 1분 캐싱
proxy_cache_use_stale error timeout
updating
http_500 http_502
http_503 http_504; # 백엔드 오류 시 stale 캐시 제공
proxy_cache_lock on; # 동시 요청 시 하나만 백엔드로
proxy_cache_min_uses 2; # 2회 이상 요청된 URL만 캐싱
# 디버깅용 캐시 상태 헤더
add_header X-Cache-Status $upstream_cache_status;
}
# 캐시 바이패스 (관리자용)
location /api/ {
proxy_pass http://backend;
proxy_cache proxy_cache;
# Cookie에 nocache가 있거나, 헤더에 Cache-Control: no-cache 시 바이패스
proxy_cache_bypass $http_cache_control;
proxy_no_cache $cookie_nocache;
}
}
X-Cache-Status 값:
| 상태 | 의미 |
|---|---|
HIT | 캐시에서 직접 제공 |
MISS | 캐시 없음 → 백엔드 요청 |
EXPIRED | 만료된 캐시 → 백엔드 재요청 |
STALE | 만료됐지만 stale 정책으로 제공 |
UPDATING | 갱신 중에 stale 캐시 제공 |
BYPASS | 캐시 바이패스됨 |
6.2 FastCGI Cache (PHP 등)
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=fastcgi_cache:10m
max_size=512m
inactive=30m;
server {
listen 80;
server_name wordpress.example.com;
# 캐시 바이패스 조건 정의
set $skip_cache 0;
# POST 요청은 캐시하지 않음
if ($request_method = POST) {
set $skip_cache 1;
}
# 쿼리스트링이 있으면 캐시하지 않음
if ($query_string != "") {
set $skip_cache 1;
}
# WordPress 관리자 페이지 캐시 제외
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php") {
set $skip_cache 1;
}
# 로그인 사용자 캐시 제외
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
set $skip_cache 1;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_cache fastcgi_cache;
fastcgi_cache_valid 200 30m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating http_500;
add_header X-FastCGI-Cache $upstream_cache_status;
}
}
6.3 브라우저 캐싱 (정적 리소스)
# ── 정적 파일에 대한 장기 브라우저 캐시 ──
location ~* \.(jpg|jpeg|png|gif|ico|webp|avif|svg)$ {
expires 30d; # Expires 헤더 설정
add_header Cache-Control "public, immutable"; # 브라우저가 재검증 없이 캐시 사용
access_log off; # 정적 파일 로그 비활성화 (I/O 절감)
}
location ~* \.(css|js)$ {
expires 7d;
add_header Cache-Control "public";
}
location ~* \.(woff|woff2|ttf|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*"; # 폰트 CORS
}
# ── HTML은 짧은 캐시 또는 no-cache ──
location ~* \.html$ {
expires -1; # no-cache
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
7. Rate Limiting과 Connection Limiting
7.1 Rate Limiting (요청 속도 제한)
Nginx의 Rate Limiting은 Leaky Bucket 알고리즘을 사용한다. 요청은 버킷(zone)에 들어가고, 설정된 속도(rate)로 처리된다.
# ── HTTP Context에서 Zone 정의 ──
# 키: 클라이언트 IP
# 공유 메모리: 10MB (약 160,000 IP 주소 저장)
# 속도: 초당 10개 요청
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# 로그인 엔드포인트: 초당 1개 (Brute Force 방지)
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# API 엔드포인트: 초당 50개
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;
# Rate Limit 초과 시 로그 레벨
limit_req_status 429; # 기본 503 대신 429 Too Many Requests
limit_req_log_level warn;
server {
listen 80;
server_name example.com;
# ── 일반 페이지 ──
location / {
limit_req zone=general burst=20 nodelay;
# burst=20: 순간 20개까지 초과 허용
# nodelay: burst 범위 내 요청을 지연 없이 즉시 처리
proxy_pass http://backend;
}
# ── 로그인 페이지 ──
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://backend;
}
# ── API ──
location /api/ {
limit_req zone=api burst=100 nodelay;
proxy_pass http://backend;
}
}
burst와 nodelay 동작 원리:
Rate: 10r/s, Burst: 20
시간 0에 30개 요청 도착:
├── 10개: 즉시 처리 (rate 범위)
├── 20개: burst 큐에 저장
│ nodelay 없으면: 100ms 간격으로 처리 (2초간)
│ nodelay 있으면: 즉시 처리 (큐 슬롯만 점유)
└── 나머지: 429 거부
7.2 Connection Limiting (동시 연결 제한)
# 클라이언트 IP당 동시 연결 수 제한
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
# 서버 전체 동시 연결 수 제한
limit_conn_zone $server_name zone=conn_per_server:10m;
limit_conn_status 429;
limit_conn_log_level warn;
server {
listen 80;
server_name example.com;
# IP당 최대 100개 동시 연결
limit_conn conn_per_ip 100;
# 서버당 최대 10,000개 동시 연결
limit_conn conn_per_server 10000;
# 다운로드 대역폭 제한 (선택)
location /downloads/ {
limit_conn conn_per_ip 5; # 다운로드는 IP당 5개로 제한
limit_rate 500k; # 연결당 500KB/s 속도 제한
limit_rate_after 10m; # 첫 10MB는 제한 없이, 이후 제한
}
}
7.3 IP 화이트리스트와 Rate Limiting 결합
# 내부 네트워크는 Rate Limiting 면제
geo $limit {
default 1;
10.0.0.0/8 0; # 내부 네트워크
192.168.0.0/16 0; # 내부 네트워크
172.16.0.0/12 0; # 내부 네트워크
}
map $limit $limit_key {
0 ""; # 빈 키 → Rate Limiting 적용 안 됨
1 $binary_remote_addr; # 외부 IP → Rate Limiting 적용
}
limit_req_zone $limit_key zone=api:10m rate=10r/s;
8. Gzip/Brotli 압축
8.1 Gzip 압축 설정
# /etc/nginx/conf.d/gzip.conf
gzip on;
gzip_vary on; # Vary: Accept-Encoding 헤더 추가
gzip_proxied any; # 프록시 응답도 압축
gzip_comp_level 6; # 압축 레벨 (1-9, 6이 성능/압축 균형점)
gzip_min_length 1000; # 1KB 미만 파일은 압축 효과 없음 → 제외
gzip_buffers 16 8k; # 압축 버퍼
gzip_types
text/plain
text/css
text/javascript
text/xml
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
application/vnd.ms-fontobject
font/opentype
font/ttf
image/svg+xml
image/x-icon;
# 이미 압축된 파일 제외 (이미지, 동영상 등은 gzip_types에 미포함)
gzip_disable "msie6"; # IE6 비활성화 (레거시)
8.2 Brotli 압축 설정
Brotli는 Gzip 대비 15-25% 더 높은 압축률을 제공한다. 모던 브라우저 대부분이 지원하며, Nginx에서 ngx_brotli 모듈이 필요하다.
# Brotli 동적 압축
brotli on;
brotli_comp_level 6; # 동적 압축은 6 권장 (11은 CPU 과부하)
brotli_min_length 1000;
brotli_types
text/plain
text/css
text/javascript
text/xml
application/javascript
application/json
application/xml
application/rss+xml
font/opentype
font/ttf
image/svg+xml;
# Brotli 정적 압축 (미리 압축된 .br 파일 제공)
brotli_static on;
8.3 Dual Compression 전략
Brotli를 지원하는 브라우저에는 Brotli로, 지원하지 않는 브라우저에는 Gzip으로 폴백한다.
# 빌드 시 정적 파일 사전 압축 (CI/CD 파이프라인)
# gzip -k -9 dist/**/*.{js,css,html,json,svg}
# brotli -k -q 11 dist/**/*.{js,css,html,json,svg}
# Nginx 설정
brotli_static on; # .br 파일이 있으면 우선 제공
gzip_static on; # .gz 파일이 있으면 제공 (Brotli 미지원 시)
gzip on; # 사전 압축 파일 없으면 동적 gzip
압축 성능 비교:
| 알고리즘 | 압축률 (일반 JS) | CPU 부하 (동적) | 브라우저 지원 |
|---|---|---|---|
| Gzip L6 | 70-75% | 낮음 | 99%+ |
| Brotli L6 | 75-80% | 중간 | 96%+ |
| Brotli L11 | 80-85% | 높음 (정적 전용) | 96%+ |
9. 보안 헤더
9.1 종합 보안 헤더 설정
# /etc/nginx/conf.d/security-headers.conf
# ── Clickjacking 방지 ──
add_header X-Frame-Options "DENY" always;
# DENY: 어떤 사이트도 iframe으로 삽입 불가
# SAMEORIGIN: 같은 도메인만 iframe 허용
# 참고: CSP frame-ancestors가 더 현대적인 대안
# ── MIME 타입 스니핑 방지 ──
add_header X-Content-Type-Options "nosniff" always;
# 브라우저가 Content-Type을 무시하고 자체 판단하는 것을 방지
# ── XSS 필터 (레거시, 현대 브라우저는 CSP 사용) ──
add_header X-XSS-Protection "0" always;
# 최신 권장: "0" (비활성화) — CSP가 더 안전하고 정확함
# ── Referrer 정보 제어 ──
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 같은 도메인: 전체 URL 전송
# 다른 도메인: origin(도메인)만 전송
# HTTP→HTTPS 다운그레이드: 전송 안 함
# ── 권한 정책 (카메라, 마이크, 위치 등 제어) ──
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# 모든 기능 비활성화 (필요한 것만 허용)
# ── HSTS ──
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# ── Cross-Origin 정책 ──
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
9.2 Content Security Policy (CSP)
CSP는 가장 강력한 보안 헤더지만 설정이 복잡하다. Report-Only 모드로 시작하여 위반 사항을 모니터링한 후 점진적으로 적용하는 것이 권장된다.
# ── 1단계: Report-Only 모드 (차단 없이 위반만 보고) ──
add_header Content-Security-Policy-Report-Only
"default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;" always;
# ── 2단계: 실제 적용 (위반 시 차단) ──
add_header Content-Security-Policy
"default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';" always;
9.3 Server Block에서 통합 적용
server {
listen 443 ssl http2;
server_name example.com;
include /etc/nginx/conf.d/ssl-params.conf;
include /etc/nginx/conf.d/security-headers.conf;
# 보안 관련 추가 설정
server_tokens off; # Nginx 버전 정보 숨김
more_clear_headers Server; # Server 헤더 제거 (headers-more 모듈)
# ...
}
10. 접근 제어 (Access Control)
10.1 IP 기반 접근 제어
# ── 관리자 페이지: 특정 IP만 허용 ──
location /admin/ {
allow 10.0.0.0/8; # 내부 네트워크
allow 203.0.113.50; # 특정 관리자 IP
deny all; # 나머지 모두 거부
proxy_pass http://backend;
}
# ── 특정 IP 차단 ──
location / {
deny 192.168.1.100; # 특정 IP 차단
deny 10.0.0.0/24; # 서브넷 차단
allow all; # 나머지 허용
# 주의: allow/deny 순서가 중요! 먼저 매칭되는 규칙이 적용됨
proxy_pass http://backend;
}
10.2 HTTP Basic Authentication
# htpasswd 파일 생성
sudo apt install apache2-utils # Debian/Ubuntu
# sudo yum install httpd-tools # RHEL/CentOS
# 사용자 생성 (-c: 파일 새로 생성, -B: bcrypt 해시)
sudo htpasswd -cB /etc/nginx/.htpasswd admin
# 추가 사용자
sudo htpasswd -B /etc/nginx/.htpasswd developer
# ── 특정 경로에 Basic Auth 적용 ──
location /admin/ {
auth_basic "Administrator Area"; # 인증 프롬프트 메시지
auth_basic_user_file /etc/nginx/.htpasswd; # 비밀번호 파일
proxy_pass http://backend;
}
# ── 특정 경로만 인증 면제 ──
location /admin/health {
auth_basic off; # 헬스체크는 인증 면제
proxy_pass http://backend;
}
10.3 IP + Auth 결합 (satisfy 디렉티브)
location /admin/ {
# satisfy any → IP 허용 OR 인증 성공 중 하나만 충족하면 접근 허용
# satisfy all → IP 허용 AND 인증 성공 모두 충족해야 접근 허용
satisfy any;
# IP 화이트리스트
allow 10.0.0.0/8;
deny all;
# Basic Auth (IP 화이트리스트 밖에서 접근 시)
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://backend;
}
10.4 GeoIP 기반 접근 제어
# GeoIP2 모듈 필요 (ngx_http_geoip2_module)
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 60m;
$geoip2_data_country_code country iso_code;
}
# 특정 국가 차단
map $geoip2_data_country_code $allowed_country {
default yes;
CN no; # 중국
RU no; # 러시아
}
server {
if ($allowed_country = no) {
return 403;
}
}
11. 로깅 설정
11.1 기본 로그 설정
# ── Access Log ──
# 기본 log_format: combined
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log combined;
# ── Error Log ──
# 레벨: debug, info, notice, warn, error, crit, alert, emerg
error_log /var/log/nginx/error.log warn;
11.2 JSON 로그 포맷 (로그 분석 도구 연동)
JSON 형태의 구조화 로깅은 Elasticsearch, Datadog, Splunk 같은 분석 도구와의 연동에 필수적이다.
log_format json_combined escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request_method":"$request_method",'
'"request_uri":"$request_uri",'
'"server_protocol":"$server_protocol",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"request_time":$request_time,'
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent",'
'"http_x_forwarded_for":"$http_x_forwarded_for",'
'"upstream_addr":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"upstream_response_time":"$upstream_response_time",'
'"ssl_protocol":"$ssl_protocol",'
'"ssl_cipher":"$ssl_cipher",'
'"request_id":"$request_id"'
'}';
access_log /var/log/nginx/access.json.log json_combined;
11.3 조건부 로깅
# ── 헬스체크 요청 로그 제외 ──
map $request_uri $loggable {
~*^/health 0;
~*^/ready 0;
~*^/metrics 0;
default 1;
}
access_log /var/log/nginx/access.log combined if=$loggable;
# ── 에러 요청만 별도 로깅 ──
map $status $is_error {
~^[45] 1;
default 0;
}
access_log /var/log/nginx/error_requests.log combined if=$is_error;
# ── 느린 요청 로깅 (1초 이상) ──
map $request_time $is_slow {
~^[1-9] 1; # 1초 이상
~^[0-9]{2} 1; # 10초 이상
default 0;
}
access_log /var/log/nginx/slow_requests.log json_combined if=$is_slow;
11.4 도메인별 로그 분리
server {
server_name example.com;
access_log /var/log/nginx/example.com.access.log json_combined;
error_log /var/log/nginx/example.com.error.log warn;
}
server {
server_name api.example.com;
access_log /var/log/nginx/api.example.com.access.log json_combined;
error_log /var/log/nginx/api.example.com.error.log warn;
}
11.5 로그 로테이션 (logrotate)
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily # 매일 로테이션
missingok # 로그 파일 없어도 에러 안 남
rotate 14 # 14일치 보관
compress # gzip 압축
delaycompress # 바로 직전 파일은 압축 안 함
notifempty # 빈 파일은 로테이션 안 함
create 0640 nginx adm # 새 파일 권한
sharedscripts
postrotate
# Nginx에 로그 파일 재오픈 시그널
if [ -f /run/nginx.pid ]; then
kill -USR1 $(cat /run/nginx.pid)
fi
endscript
}
12. 성능 튜닝
12.1 Worker 프로세스와 연결
# ── Main Context ──
worker_processes auto; # CPU 코어 수만큼 (수동: 4, 8 등)
worker_rlimit_nofile 65535; # Worker당 최대 파일 디스크립터 수
events {
worker_connections 4096; # Worker당 최대 동시 연결 수
multi_accept on; # 한 이벤트 루프에서 여러 연결 수락
use epoll; # Linux: epoll (기본값)
}
최대 동시 연결 수 공식:
최대 연결 = worker_processes x worker_connections
예: 4 workers x 4096 connections = 16,384 동시 연결
리버스 프록시 시 (클라이언트 + 백엔드 2개 연결):
실제 동시 클라이언트 = 16,384 / 2 = 8,192
12.2 Keepalive 설정
http {
# ── 클라이언트 Keepalive ──
keepalive_timeout 65; # 클라이언트 keepalive 유지 시간 (초)
keepalive_requests 1000; # 하나의 keepalive 연결로 처리할 최대 요청 수
# ── 타임아웃 ──
client_body_timeout 12; # 클라이언트 요청 본문 수신 타임아웃
client_header_timeout 12; # 클라이언트 요청 헤더 수신 타임아웃
send_timeout 10; # 클라이언트에 응답 전송 타임아웃
reset_timedout_connection on; # 타임아웃 연결 즉시 리셋 (메모리 해제)
}
12.3 버퍼 설정
http {
# ── 클라이언트 요청 버퍼 ──
client_body_buffer_size 16k; # 요청 본문 버퍼 (초과 시 디스크 기록)
client_header_buffer_size 1k; # 요청 헤더 버퍼
client_max_body_size 100m; # 최대 업로드 크기 (기본 1MB)
large_client_header_buffers 4 16k; # 큰 헤더용 버퍼
# ── 프록시 버퍼 ──
proxy_buffers 16 32k; # 백엔드 응답 저장 버퍼
proxy_buffer_size 16k; # 첫 번째 응답 (헤더) 버퍼
proxy_busy_buffers_size 64k; # 클라이언트로 전송 중인 버퍼 크기
proxy_temp_file_write_size 64k; # 디스크에 기록하는 임시 파일 크기
}
12.4 프로덕션 성능 튜닝 종합
# /etc/nginx/nginx.conf — 프로덕션 최적화
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
# ── MIME & 기본 설정 ──
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
server_tokens off; # 버전 정보 숨김
# ── 파일 전송 최적화 ──
sendfile on; # 커널 수준 파일 전송
tcp_nopush on; # sendfile과 함께 사용: 패킷 최적화
tcp_nodelay on; # Nagle 알고리즘 비활성화
aio on; # 비동기 I/O
# ── Keepalive ──
keepalive_timeout 65;
keepalive_requests 1000;
# ── 타임아웃 ──
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
reset_timedout_connection on;
# ── 버퍼 ──
client_body_buffer_size 16k;
client_header_buffer_size 1k;
client_max_body_size 100m;
large_client_header_buffers 4 16k;
# ── Open File Cache ──
open_file_cache max=10000 inactive=30s;
open_file_cache_valid 60s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# ── 압축, SSL, 로깅 등 include ──
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*.conf;
}
13. URL Rewriting과 Redirection
13.1 return 디렉티브 (권장)
return은 rewrite보다 단순하고 효율적이다. URL 변경이 필요한 대부분의 경우 return을 먼저 고려해야 한다.
# ── HTTP → HTTPS 리다이렉트 ──
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# ── www → non-www 정규화 ──
server {
listen 443 ssl http2;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
# ── 도메인 변경 리다이렉트 ──
server {
listen 80;
listen 443 ssl http2;
server_name old-domain.com www.old-domain.com;
return 301 https://new-domain.com$request_uri;
}
# ── 특정 경로 리다이렉트 ──
location /old-page {
return 301 /new-page;
}
# ── 유지보수 모드 ──
location / {
return 503; # Service Unavailable
}
# error_page와 조합
error_page 503 /maintenance.html;
location = /maintenance.html {
root /var/www/html;
internal;
}
13.2 rewrite 디렉티브
rewrite는 정규표현식 기반의 URL 변환이 필요할 때 사용한다.
# ── 기본 문법 ──
# rewrite regex replacement [flag];
# flag: last | break | redirect (302) | permanent (301)
# ── 버전 없는 API 경로로 변환 ──
rewrite ^/api/v1/(.*)$ /api/$1 last;
# /api/v1/users → /api/users 로 내부 리라이트
# last: 새로운 location 매칭 시작
# ── 확장자 제거 (Clean URL) ──
rewrite ^/(.*)\.html$ /$1 permanent;
# /about.html → /about (301 리다이렉트)
# ── 쿼리스트링 포함 리라이트 ──
rewrite ^/search/(.*)$ /search?q=$1? last;
# ? 를 끝에 붙이면 원래 쿼리스트링 제거
# ── 다국어 URL ──
rewrite ^/ko/(.*)$ /$1?lang=ko last;
rewrite ^/en/(.*)$ /$1?lang=en last;
rewrite ^/ja/(.*)$ /$1?lang=ja last;
rewrite 플래그 비교:
| 플래그 | 동작 | 용도 |
|---|---|---|
last | 리라이트 후 새 location 매칭 | 내부 라우팅 변경 |
break | 리라이트 후 현재 location 내에서 처리 | 현재 블록 내 변환 |
redirect | 302 임시 리다이렉트 | 임시 이동 |
permanent | 301 영구 리다이렉트 | 영구 이동 |
13.3 try_files 디렉티브
try_files는 파일/디렉토리 존재 여부를 순차적으로 확인하며, SPA나 프레임워크에서 필수적이다.
# ── SPA (React, Vue, Angular 등) ──
location / {
root /var/www/spa;
try_files $uri $uri/ /index.html;
# 1. $uri: 요청 파일이 있는지 확인
# 2. $uri/: 디렉토리가 있는지 확인
# 3. /index.html: 위 모두 없으면 index.html (SPA 라우팅)
}
# ── Next.js / Nuxt.js ──
location / {
try_files $uri $uri/ @proxy;
}
location @proxy {
proxy_pass http://127.0.0.1:3000;
include /etc/nginx/conf.d/proxy-params.conf;
}
# ── PHP (WordPress, Laravel) ──
location / {
try_files $uri $uri/ /index.php?$args;
}
# ── 정적 파일 우선 → 백엔드 폴백 ──
location / {
root /var/www/static;
try_files $uri @backend;
}
location @backend {
proxy_pass http://app_server;
}
13.4 조건부 리다이렉트
# ── 모바일 디바이스 리다이렉트 ──
if ($http_user_agent ~* "(Android|iPhone|iPad)") {
return 302 https://m.example.com$request_uri;
}
# ── 특정 쿼리 파라미터 기반 ──
if ($arg_redirect) {
return 302 $arg_redirect;
}
# ── map으로 복잡한 리다이렉트 관리 ──
map $request_uri $redirect_uri {
/old-blog/post-1 /blog/new-post-1;
/old-blog/post-2 /blog/new-post-2;
/products/legacy /shop/all;
default "";
}
server {
if ($redirect_uri) {
return 301 $redirect_uri;
}
}
14. 정적 파일 서빙 최적화
14.1 핵심 파일 전송 디렉티브
http {
# ── sendfile ──
# 커널의 sendfile() 시스템 콜을 사용하여 파일을 직접 소켓으로 전송
# 유저 스페이스 메모리 복사를 제거하여 CPU 사용량과 컨텍스트 스위칭 감소
sendfile on;
# ── tcp_nopush ──
# sendfile과 함께 작동: HTTP 헤더와 파일 시작 부분을 하나의 패킷으로 전송
# 네트워크 패킷 수를 줄여 대역폭 효율 향상
tcp_nopush on;
# ── tcp_nodelay ──
# Nagle 알고리즘 비활성화: 작은 패킷을 즉시 전송
# keepalive 연결에서 지연 시간 감소 (tcp_nopush와 함께 사용 가능)
tcp_nodelay on;
}
전송 방식 비교:
sendfile off (기본):
디스크 → 커널 버퍼 → 유저 메모리 (Nginx) → 커널 소켓 버퍼 → 네트워크
[읽기] [복사] [쓰기]
sendfile on:
디스크 → 커널 버퍼 ─────────────────────────→ 커널 소켓 버퍼 → 네트워크
[읽기] [제로 카피: CPU 개입 없음] [전송]
14.2 Open File Cache
반복적으로 요청되는 파일의 디스크립터, 크기, 수정 시간을 캐시하여 파일시스템 조회를 최소화한다.
http {
# 최대 10,000개 파일 정보 캐시, 30초간 미사용 시 제거
open_file_cache max=10000 inactive=30s;
# 캐시된 정보를 60초마다 재검증
open_file_cache_valid 60s;
# 최소 2회 이상 요청된 파일만 캐시 (1회성 요청 필터)
open_file_cache_min_uses 2;
# 파일 없음(ENOENT) 에러도 캐시 (존재하지 않는 파일 반복 조회 방지)
open_file_cache_errors on;
}
14.3 정적 파일 서빙 종합 설정
server {
listen 443 ssl http2;
server_name static.example.com;
root /var/www/static;
# ── 이미지 ──
location ~* \.(jpg|jpeg|png|gif|ico|webp|avif|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
access_log off;
log_not_found off; # 404 로그도 비활성화
# 이미지 전용 제한
limit_rate 2m; # 연결당 2MB/s
}
# ── CSS/JS (캐시 버스팅 전략과 함께) ──
location ~* \.(css|js)$ {
expires 365d; # 해시 기반 파일명이면 장기 캐시
add_header Cache-Control "public, immutable";
gzip_static on;
brotli_static on;
}
# ── 폰트 ──
location ~* \.(woff|woff2|ttf|eot|otf)$ {
expires 365d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
# ── 미디어 파일 ──
location ~* \.(mp4|webm|ogg|mp3|wav)$ {
expires 30d;
add_header Cache-Control "public";
# Range 요청 지원 (동영상 시크)
add_header Accept-Ranges bytes;
}
# ── 디렉토리 리스팅 방지 ──
location / {
autoindex off;
}
# ── 숨김 파일 접근 차단 ──
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
14.4 HTTP/2 Server Push (선택)
# 주요 리소스를 클라이언트 요청 전에 미리 전송
location = /index.html {
http2_push /css/main.css;
http2_push /js/app.js;
http2_push /images/logo.webp;
}
참고: HTTP/2 Server Push는 일부 브라우저에서 지원이 중단되고 있어,
103 Early Hints가 대안으로 떠오르고 있다.
15. 헬스체크와 모니터링
15.1 Stub Status (기본 모니터링)
Nginx OSS에 포함된 stub_status 모듈로 실시간 연결 상태를 확인할 수 있다.
server {
listen 8080; # 별도 포트로 분리
server_name localhost;
# 내부 네트워크만 접근 허용
allow 10.0.0.0/8;
allow 127.0.0.1;
deny all;
location /nginx_status {
stub_status;
}
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
stub_status 출력 예시:
Active connections: 291
server accepts handled requests
16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
| 항목 | 의미 |
|---|---|
Active connections | 현재 활성 연결 수 (Reading + Writing + Waiting) |
accepts | 총 수락된 연결 수 |
handled | 총 처리된 연결 수 (= accepts면 정상) |
requests | 총 처리된 요청 수 |
Reading | 클라이언트 요청 헤더를 읽는 중인 연결 수 |
Writing | 클라이언트에 응답을 보내는 중인 연결 수 |
Waiting | keepalive 유휴 연결 수 |
15.2 패시브 헬스체크 (Upstream 모니터링)
Nginx OSS는 실제 트래픽 기반의 패시브 헬스체크만 지원한다.
upstream backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.3:8080 max_fails=3 fail_timeout=30s backup;
# max_fails=3: 30초 내 3회 연속 실패 시 서버를 비정상으로 판정
# fail_timeout=30s: 비정상 판정 후 30초간 해당 서버로 요청 전송 안 함
# 30초 후 다시 요청을 보내 복구 여부 확인
}
server {
location / {
proxy_pass http://backend;
# 어떤 응답을 "실패"로 판정할지 설정
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_timeout 10s; # 다음 서버 시도 최대 시간
proxy_next_upstream_tries 3; # 최대 재시도 횟수
}
}
15.3 액티브 헬스체크 (NGINX Plus 또는 외부 솔루션)
# ── NGINX Plus에서의 액티브 헬스체크 ──
upstream backend {
zone backend_zone 64k; # 공유 메모리 존 필수
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
server {
location / {
proxy_pass http://backend;
health_check interval=5s # 5초마다 체크
fails=3 # 3회 실패 시 비정상
passes=2 # 2회 성공 시 복구
uri=/health; # 헬스체크 엔드포인트
}
}
15.4 Prometheus 연동 (nginx-prometheus-exporter)
# docker-compose.yml
services:
nginx-exporter:
image: nginx/nginx-prometheus-exporter:1.3
command:
- --nginx.scrape-uri=http://nginx:8080/nginx_status
ports:
- '9113:9113'
depends_on:
- nginx
# prometheus.yml
scrape_configs:
- job_name: 'nginx'
static_configs:
- targets: ['nginx-exporter:9113']
15.5 커스텀 헬스체크 엔드포인트
# ── Liveness Probe (Nginx 자체 동작 확인) ──
location = /healthz {
access_log off;
return 200 "alive\n";
add_header Content-Type text/plain;
}
# ── Readiness Probe (백엔드 연결 포함 확인) ──
location = /readyz {
access_log off;
proxy_pass http://backend/health;
proxy_connect_timeout 2s;
proxy_read_timeout 2s;
# 백엔드 응답 실패 시 503
error_page 502 503 504 = @not_ready;
}
location @not_ready {
return 503 "not ready\n";
add_header Content-Type text/plain;
}
# Kubernetes에서의 활용
apiVersion: v1
kind: Pod
spec:
containers:
- name: nginx
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
종합 프로덕션 설정 체크리스트
프로덕션 환경에 Nginx를 배포할 때 확인해야 할 핵심 항목을 정리한다.
| 카테고리 | 항목 | 상태 |
|---|---|---|
| 아키텍처 | worker_processes auto 설정 | [ ] |
| 아키텍처 | worker_connections 적정 값 설정 | [ ] |
| SSL/TLS | TLSv1.2 + TLSv1.3만 활성화 | [ ] |
| SSL/TLS | 강력한 Cipher Suites 설정 | [ ] |
| SSL/TLS | OCSP Stapling 활성화 | [ ] |
| SSL/TLS | HSTS 헤더 설정 | [ ] |
| SSL/TLS | HTTP → HTTPS 리다이렉트 | [ ] |
| 보안 | server_tokens off | [ ] |
| 보안 | 보안 헤더 (CSP, X-Frame-Options 등) 설정 | [ ] |
| 보안 | Rate Limiting 설정 | [ ] |
| 보안 | 관리자 페이지 접근 제어 | [ ] |
| 성능 | sendfile, tcp_nopush, tcp_nodelay 활성화 | [ ] |
| 성능 | Gzip/Brotli 압축 설정 | [ ] |
| 성능 | open_file_cache 설정 | [ ] |
| 성능 | 정적 파일 브라우저 캐싱 설정 | [ ] |
| 성능 | Upstream keepalive 설정 | [ ] |
| 캐싱 | proxy_cache 또는 fastcgi_cache 설정 | [ ] |
| 캐싱 | proxy_cache_use_stale 설정 | [ ] |
| 모니터링 | stub_status 활성화 (내부 전용) | [ ] |
| 모니터링 | 헬스체크 엔드포인트 구성 | [ ] |
| 로깅 | JSON 로그 포맷 설정 | [ ] |
| 로깅 | 로그 로테이션 설정 | [ ] |
| 로깅 | 헬스체크/정적파일 로그 제외 | [ ] |
| 프록시 | 필수 프록시 헤더 설정 | [ ] |
| 프록시 | WebSocket 프록시 설정 (필요 시) | [ ] |
| 로드밸런싱 | 적절한 알고리즘 선택 | [ ] |
| 로드밸런싱 | 백업 서버 구성 | [ ] |
참고 자료
- Nginx Official Documentation
- Nginx Beginner's Guide
- DigitalOcean - Understanding Nginx Configuration File Structure
- Inside NGINX: How We Designed for Performance and Scale
- NGINX Reverse Proxy Guide
- Nginx WebSocket Proxying
- NGINX HTTP Load Balancing
- NGINX TLS 1.3 Hardening Guide
- A Guide to Caching with NGINX
- Rate Limiting with NGINX
- NGINX Gzip Compression Guide
- Tuning NGINX for Performance
- Creating NGINX Rewrite Rules
- NGINX sendfile, tcp_nopush, tcp_nodelay Explained
- NGINX Health Checks
- Mozilla SSL Configuration Generator