Split View: Nginx 설정 완벽 가이드: 아키텍처부터 프로덕션 최적화까지 15가지 핵심 주제
Nginx 설정 완벽 가이드: 아키텍처부터 프로덕션 최적화까지 15가지 핵심 주제
- 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
Nginx Configuration Complete Guide: 15 Essential Topics from Architecture to Production Optimization
- 1. Nginx Architecture and Configuration Structure
- 2. Virtual Host / Server Block Configuration
- 3. Reverse Proxy Configuration
- 4. Load Balancing
- 5. SSL/TLS Configuration
- 6. Caching Configuration
- 7. Rate Limiting and Connection Limiting
- 8. Gzip/Brotli Compression
- 9. Security Headers
- 10. Access Control
- 11. Logging Configuration
- 12. Performance Tuning
- 13. URL Rewriting and Redirection
- 14. Static File Serving Optimization
- 15. Health Checks and Monitoring
- Production Configuration Checklist
- References
- Quiz
1. Nginx Architecture and Configuration Structure
1.1 Event-Driven Architecture: Master-Worker Model
Nginx adopts a fundamentally different event-driven architecture compared to Apache httpd's process/thread-based model. This design philosophy is the core reason why Nginx can handle hundreds of thousands of concurrent connections on a single server.
┌─────────────────────────────────────────────────────────┐
│ Master Process │
│ - Read and validate configuration files │
│ - Create/manage Worker processes (fork) │
│ - Port binding (80, 443) │
│ - Signal handling (reload, stop, reopen) │
└─────────┬───────────┬───────────┬───────────┬───────────┘
│ │ │ │
┌─────▼─────┐ ┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│ Worker 0 │ │Worker 1│ │Worker 2│ │Worker 3│
│ Event Loop │ │ ... │ │ ... │ │ ... │
│ epoll/kq │ │ │ │ │ │ │
│ 1000s conn │ │ │ │ │ │ │
└───────────┘ └────────┘ └────────┘ └────────┘
The Master Process runs with root privileges and is responsible for parsing configuration files, binding ports, and managing Worker processes. It creates Workers via the fork() system call, and during configuration reload, it spawns new Workers and gracefully shuts down the old ones without dropping existing connections.
The Worker Process is the core unit that handles actual client requests. Each Worker runs an independent event loop, leveraging the OS I/O multiplexing mechanism (epoll on Linux, kqueue on FreeBSD/macOS) to handle thousands of connections concurrently without blocking. This dramatically reduces context switching and memory overhead compared to per-connection thread allocation.
1.2 nginx.conf Configuration Context Structure
Nginx configuration follows a hierarchical context structure. Child contexts inherit settings from the parent, and redeclaring the same directive in a child context overrides the parent value.
# ============================================
# Main Context (Global Settings)
# ============================================
user nginx; # User running Worker processes
worker_processes auto; # Create Workers matching CPU core count
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
# ============================================
# Events Context (Connection Handling)
# ============================================
events {
worker_connections 1024; # Max concurrent connections per Worker
multi_accept on; # Accept multiple connections at once
use epoll; # Use epoll on Linux (default)
}
# ============================================
# HTTP Context (HTTP Protocol Settings)
# ============================================
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ========================================
# Server Context (Virtual Host)
# ========================================
server {
listen 80;
server_name example.com;
# ====================================
# Location Context (URL Path Matching)
# ====================================
location / {
root /var/www/html;
index index.html;
}
location /api/ {
proxy_pass http://backend;
}
}
}
# ============================================
# Stream Context (TCP/UDP Proxy, L4)
# ============================================
stream {
server {
listen 3306;
proxy_pass mysql_backend;
}
}
Context Hierarchy Summary:
| Context | Location | Purpose |
|---|---|---|
| Main | Top-level | Global settings (user, worker, pid, error_log) |
| Events | Inside Main | Connection handling mechanism (worker_connections) |
| HTTP | Inside Main | All HTTP protocol-related settings |
| Server | Inside HTTP | Virtual host (per-domain settings) |
| Location | Inside Server | Request handling rules per URL path |
| Upstream | Inside HTTP | Backend server group (load balancing) |
| Stream | Inside Main | TCP/UDP L4 proxy |
1.3 Configuration File Structure Best Practice
In production environments, rather than putting everything in a single nginx.conf, configurations are modularized for management.
/etc/nginx/
├── nginx.conf # Main config (split with include)
├── conf.d/ # Common config snippets
│ ├── ssl-params.conf # SSL/TLS common parameters
│ ├── proxy-params.conf # Reverse proxy common headers
│ ├── security-headers.conf # Security headers
│ └── gzip.conf # Compression settings
├── sites-available/ # Per-site config files
│ ├── example.com.conf
│ ├── api.example.com.conf
│ └── admin.example.com.conf
├── sites-enabled/ # Active sites (symlinks)
│ ├── example.com.conf -> ../sites-available/example.com.conf
│ └── api.example.com.conf -> ../sites-available/api.example.com.conf
└── snippets/ # Reusable config fragments
├── letsencrypt.conf
└── fastcgi-php.conf
# nginx.conf main file
http {
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*.conf;
}
2. Virtual Host / Server Block Configuration
Nginx Server Blocks are the equivalent of Apache Virtual Hosts, allowing you to host multiple domains on a single server.
2.1 Basic Server Block Configuration
# /etc/nginx/sites-available/example.com.conf
# -- Primary domain --
server {
listen 80;
listen [::]:80; # IPv6 support
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm;
# Separate access logs per domain
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
}
# -- Second domain --
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)
This is the default server block that handles requests for undefined domains. For security, responding with 444 (connection drop) is recommended.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _; # All unmatched domains
# Immediately close connections for undefined host requests
return 444;
}
2.3 Server Name Matching Priority
Nginx follows this priority order for server_name matching:
- Exact name:
server_name example.com - Leading wildcard:
server_name *.example.com - Trailing wildcard:
server_name example.* - Regular expression:
server_name ~^(?<subdomain>.+)\.example\.com$ - default_server: When none of the above match
# Capture subdomain with regex
server {
listen 80;
server_name ~^(?<subdomain>.+)\.example\.com$;
location / {
root /var/www/$subdomain;
}
}
2.4 Site Enable/Disable
# Enable site
sudo ln -s /etc/nginx/sites-available/example.com.conf \
/etc/nginx/sites-enabled/example.com.conf
# Validate configuration
sudo nginx -t
# Reload (zero downtime)
sudo systemctl reload nginx
3. Reverse Proxy Configuration
3.1 Basic Reverse Proxy
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
# -- Essential proxy headers --
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;
}
}
Purpose of each header:
| Header | Purpose |
|---|---|
Host | Pass the original request Host header |
X-Real-IP | Actual client IP (identify original IP behind proxy) |
X-Forwarded-For | Accumulated client IP list through proxy chain |
X-Forwarded-Proto | Original protocol (http/https) -- used for redirect decisions |
X-Forwarded-Host | Original Host header |
X-Forwarded-Port | Original port |
3.2 Reusable Proxy Parameter Snippet
# /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;
# Reuse with include in Server Block
location / {
proxy_pass http://backend;
include /etc/nginx/conf.d/proxy-params.conf;
}
3.3 WebSocket Proxy
WebSocket uses the HTTP Upgrade mechanism, so the Upgrade and Connection hop-by-hop headers must be explicitly forwarded. Nginx does not forward these headers by default.
# -- Dynamically set Connection header with map --
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 required settings
proxy_http_version 1.1; # HTTP/1.1 required (Upgrade support)
proxy_set_header Upgrade $http_upgrade; # Forward client's Upgrade header
proxy_set_header Connection $connection_upgrade; # Dynamic Connection header
# Standard proxy headers
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 connections are long-lived, extend timeouts
proxy_read_timeout 86400s; # 24 hours (default 60s would drop idle connections)
proxy_send_timeout 86400s;
}
}
3.4 Path-Based Routing (Microservices)
server {
listen 80;
server_name api.example.com;
# User service
location /api/users/ {
proxy_pass http://user-service:3001/; # Note trailing /: strips /api/users/ before forwarding
include /etc/nginx/conf.d/proxy-params.conf;
}
# Order service
location /api/orders/ {
proxy_pass http://order-service:3002/;
include /etc/nginx/conf.d/proxy-params.conf;
}
# Payment service
location /api/payments/ {
proxy_pass http://payment-service:3003/;
include /etc/nginx/conf.d/proxy-params.conf;
proxy_read_timeout 120s; # Extended timeout for payments
}
}
Note: If the
proxy_passURL ends with/, the portion matching thelocationis stripped. A request to/api/users/123is forwarded tohttp://user-service:3001/123. Without the trailing/, the full URI is forwarded as-is.
4. Load Balancing
4.1 Upstream Block and Algorithms
# ============================================
# 1. Round Robin (Default)
# Distributes requests sequentially
# ============================================
upstream backend_roundrobin {
server 10.0.0.1:8080; # Default weight 1
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
# ============================================
# 2. Weighted Round Robin
# Distributes proportionally based on server capacity
# ============================================
upstream backend_weighted {
server 10.0.0.1:8080 weight=5; # 5/8 of requests
server 10.0.0.2:8080 weight=2; # 2/8 of requests
server 10.0.0.3:8080 weight=1; # 1/8 of requests
}
# ============================================
# 3. Least Connections
# Forwards to the server with fewest active 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 (Session Affinity)
# Same client IP -> Same server
# ============================================
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 (Custom Hash)
# Hash based on arbitrary variable
# ============================================
upstream backend_hash {
hash $request_uri consistent; # URI-based + consistent hashing
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
}
Algorithm Selection Guide:
| Algorithm | Suitable For | Considerations |
|---|---|---|
| Round Robin | Stateless services, identical servers | Imbalance if server specs differ |
| Weighted | Servers with different specs | Manual weight management needed |
| Least Connections | High variance in request processing time | Similar to Round Robin for short requests |
| IP Hash | Session affinity needed (legacy apps) | Redistribution occurs on server add/remove |
| Generic Hash | Cache optimization (same URI -> same server) | Consistent hashing recommended |
4.2 Server Status and Backup
upstream backend {
least_conn;
server 10.0.0.1:8080; # Active server
server 10.0.0.2:8080; # Active server
server 10.0.0.3:8080 backup; # Backup: activated when all above servers are down
server 10.0.0.4:8080 down; # Disabled (maintenance)
server 10.0.0.5:8080 max_fails=3 fail_timeout=30s;
# max_fails=3: Marked unhealthy after 3 failures within 30s
# fail_timeout=30s: Excluded from requests for 30s after being marked unhealthy
}
4.3 Keepalive Connection Pool
Reuses TCP connections to backend servers to reduce connection setup/teardown overhead.
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
keepalive 32; # Number of idle connections to maintain per Worker
keepalive_requests 1000; # Max requests per keepalive connection
keepalive_timeout 60s; # Idle connection keep time
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # HTTP/1.1 required for keepalive
proxy_set_header Connection ""; # Empty value instead of "close" enables keepalive
}
}
5. SSL/TLS Configuration
5.1 Basic HTTPS Setup
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# -- Certificates --
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# -- Protocols --
ssl_protocols TLSv1.2 TLSv1.3; # Disable TLS 1.0, 1.1
# -- Cipher Suites (for 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; # Recommended off for TLS 1.3
# -- Elliptic Curves --
ssl_ecdh_curve X25519:secp384r1:secp256r1;
# -- Session Reuse --
ssl_session_cache shared:SSL:10m; # 10MB = ~40,000 sessions
ssl_session_timeout 1d; # Session validity period
ssl_session_tickets off; # Ensure Forward Secrecy
root /var/www/example.com/html;
index index.html;
}
5.2 HTTP to HTTPS Redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
# 301 redirect all HTTP requests to HTTPS
return 301 https://$host$request_uri;
}
5.3 HSTS (HTTP Strict Transport Security)
# Add inside HTTPS server block
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
max-age=63072000: Use HTTPS only for 2 years (minimum 1 year recommended)includeSubDomains: Apply to all subdomains as wellpreload: Qualifies for browser HSTS Preload list registrationalways: Send header even on error responses (4xx, 5xx)
5.4 OCSP Stapling
OCSP Stapling has the server handle certificate validity verification on behalf of the client, eliminating the client's direct CA lookup. This improves initial connection speed and protects privacy.
ssl_stapling on;
ssl_stapling_verify on;
# Trust chain for OCSP response verification (includes Let's Encrypt intermediate cert)
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# DNS resolver for OCSP responder lookup
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
5.5 Production SSL Integrated Snippet
# /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;
# Include in Server Block
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;
# ... remaining configuration
}
5.6 Let's Encrypt Auto-Renewal (Certbot)
# Issue certificate
sudo certbot --nginx -d example.com -d www.example.com
# Test auto-renewal
sudo certbot renew --dry-run
# Cron auto-renewal (certbot already sets this up, but explicit)
echo "0 0,12 * * * root certbot renew --quiet --deploy-hook 'systemctl reload nginx'" \
| sudo tee /etc/cron.d/certbot-renew
6. Caching Configuration
6.1 Proxy Cache (Reverse Proxy Cache)
# -- Define cache path in HTTP Context --
proxy_cache_path /var/cache/nginx/proxy
levels=1:2 # 2-level directory structure (file distribution)
keys_zone=proxy_cache:10m # Shared memory for cache keys (1MB ~ 8,000 keys)
max_size=1g # Maximum disk cache size
inactive=60m # Remove cache unused for 60 minutes
use_temp_path=off; # No temp file path (write directly -> performance gain)
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://backend;
proxy_cache proxy_cache; # Specify cache zone
proxy_cache_valid 200 302 10m; # Cache 200, 302 responses for 10 min
proxy_cache_valid 404 1m; # Cache 404 responses for 1 min
proxy_cache_use_stale error timeout
updating
http_500 http_502
http_503 http_504; # Serve stale cache on backend errors
proxy_cache_lock on; # Only one request to backend for concurrent requests
proxy_cache_min_uses 2; # Only cache URLs requested 2+ times
# Cache status header for debugging
add_header X-Cache-Status $upstream_cache_status;
}
# Cache bypass (for admins)
location /api/ {
proxy_pass http://backend;
proxy_cache proxy_cache;
# Bypass when Cookie has nocache or header has Cache-Control: no-cache
proxy_cache_bypass $http_cache_control;
proxy_no_cache $cookie_nocache;
}
}
X-Cache-Status values:
| Status | Meaning |
|---|---|
HIT | Served directly from cache |
MISS | No cache -> requested from backend |
EXPIRED | Expired cache -> re-requested from backend |
STALE | Expired but served via stale policy |
UPDATING | Stale cache served while being updated |
BYPASS | Cache was bypassed |
6.2 FastCGI Cache (PHP etc.)
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;
# Define cache bypass conditions
set $skip_cache 0;
# Do not cache POST requests
if ($request_method = POST) {
set $skip_cache 1;
}
# Do not cache requests with query strings
if ($query_string != "") {
set $skip_cache 1;
}
# Exclude WordPress admin pages from cache
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php") {
set $skip_cache 1;
}
# Exclude logged-in users from cache
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 Browser Caching (Static Resources)
# -- Long-term browser cache for static files --
location ~* \.(jpg|jpeg|png|gif|ico|webp|avif|svg)$ {
expires 30d; # Set Expires header
add_header Cache-Control "public, immutable"; # Browser uses cache without revalidation
access_log off; # Disable static file logging (I/O savings)
}
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 "*"; # Font CORS
}
# -- Short cache or no-cache for HTML --
location ~* \.html$ {
expires -1; # no-cache
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
7. Rate Limiting and Connection Limiting
7.1 Rate Limiting (Request Rate Limiting)
Nginx Rate Limiting uses the Leaky Bucket algorithm. Requests enter the bucket (zone) and are processed at a configured rate.
# -- Define Zone in HTTP Context --
# Key: client IP
# Shared memory: 10MB (approx. 160,000 IP addresses)
# Rate: 10 requests per second
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# Login endpoint: 1 per second (Brute Force prevention)
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# API endpoint: 50 per second
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;
# Log level when Rate Limit is exceeded
limit_req_status 429; # 429 Too Many Requests instead of default 503
limit_req_log_level warn;
server {
listen 80;
server_name example.com;
# -- General pages --
location / {
limit_req zone=general burst=20 nodelay;
# burst=20: Allow up to 20 excess requests momentarily
# nodelay: Process requests within burst range immediately without delay
proxy_pass http://backend;
}
# -- Login page --
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 and nodelay behavior:
Rate: 10r/s, Burst: 20
30 requests arrive at time 0:
├── 10: Processed immediately (within rate)
├── 20: Stored in burst queue
│ Without nodelay: Processed at 100ms intervals (over 2 seconds)
│ With nodelay: Processed immediately (only queue slots consumed)
└── Remaining: Rejected with 429
7.2 Connection Limiting (Concurrent Connection Limiting)
# Limit concurrent connections per client IP
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
# Limit total concurrent connections per server
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;
# Max 100 concurrent connections per IP
limit_conn conn_per_ip 100;
# Max 10,000 concurrent connections per server
limit_conn conn_per_server 10000;
# Download bandwidth limiting (optional)
location /downloads/ {
limit_conn conn_per_ip 5; # Limit downloads to 5 per IP
limit_rate 500k; # 500KB/s speed limit per connection
limit_rate_after 10m; # No limit for first 10MB, then limited
}
}
7.3 IP Whitelist Combined with Rate Limiting
# Exempt internal network from Rate Limiting
geo $limit {
default 1;
10.0.0.0/8 0; # Internal network
192.168.0.0/16 0; # Internal network
172.16.0.0/12 0; # Internal network
}
map $limit $limit_key {
0 ""; # Empty key -> Rate Limiting not applied
1 $binary_remote_addr; # External IP -> Rate Limiting applied
}
limit_req_zone $limit_key zone=api:10m rate=10r/s;
8. Gzip/Brotli Compression
8.1 Gzip Compression Settings
# /etc/nginx/conf.d/gzip.conf
gzip on;
gzip_vary on; # Add Vary: Accept-Encoding header
gzip_proxied any; # Compress proxy responses too
gzip_comp_level 6; # Compression level (1-9, 6 balances performance/compression)
gzip_min_length 1000; # Files under 1KB have no compression benefit -> exclude
gzip_buffers 16 8k; # Compression buffers
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;
# Exclude already compressed files (images, videos not in gzip_types)
gzip_disable "msie6"; # Disable for IE6 (legacy)
8.2 Brotli Compression Settings
Brotli provides 15-25% better compression ratios compared to Gzip. Most modern browsers support it, and Nginx requires the ngx_brotli module.
# Brotli dynamic compression
brotli on;
brotli_comp_level 6; # Level 6 recommended for dynamic compression (11 causes CPU overload)
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 static compression (serve pre-compressed .br files)
brotli_static on;
8.3 Dual Compression Strategy
Serve Brotli to browsers that support it, fall back to Gzip for those that do not.
# Pre-compress static files during build (CI/CD pipeline)
# gzip -k -9 dist/**/*.{js,css,html,json,svg}
# brotli -k -q 11 dist/**/*.{js,css,html,json,svg}
# Nginx configuration
brotli_static on; # Serve .br files first if available
gzip_static on; # Serve .gz files (when Brotli not supported)
gzip on; # Dynamic gzip if no pre-compressed file exists
Compression Performance Comparison:
| Algorithm | Compression Ratio (typical JS) | CPU Load (dynamic) | Browser Support |
|---|---|---|---|
| Gzip L6 | 70-75% | Low | 99%+ |
| Brotli L6 | 75-80% | Medium | 96%+ |
| Brotli L11 | 80-85% | High (static only) | 96%+ |
9. Security Headers
9.1 Comprehensive Security Header Configuration
# /etc/nginx/conf.d/security-headers.conf
# -- Clickjacking Prevention --
add_header X-Frame-Options "DENY" always;
# DENY: No site can embed via iframe
# SAMEORIGIN: Only same domain allowed in iframe
# Note: CSP frame-ancestors is the more modern alternative
# -- MIME Type Sniffing Prevention --
add_header X-Content-Type-Options "nosniff" always;
# Prevent browsers from ignoring Content-Type and making their own determination
# -- XSS Filter (Legacy, modern browsers use CSP) --
add_header X-XSS-Protection "0" always;
# Latest recommendation: "0" (disabled) -- CSP is safer and more accurate
# -- Referrer Information Control --
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Same domain: Send full URL
# Different domain: Send only origin (domain)
# HTTP to HTTPS downgrade: Send nothing
# -- Permissions Policy (camera, microphone, location control) --
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
# Disable all features (only enable what is needed)
# -- HSTS --
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# -- Cross-Origin Policies --
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 is the most powerful security header but complex to configure. It is recommended to start with Report-Only mode to monitor violations, then gradually apply the policy.
# -- Step 1: Report-Only mode (report violations without blocking) --
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;
# -- Step 2: Enforce (block on violation) --
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 Integrated Application in 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;
# Additional security-related settings
server_tokens off; # Hide Nginx version information
more_clear_headers Server; # Remove Server header (headers-more module)
# ...
}
10. Access Control
10.1 IP-Based Access Control
# -- Admin page: Allow only specific IPs --
location /admin/ {
allow 10.0.0.0/8; # Internal network
allow 203.0.113.50; # Specific admin IP
deny all; # Deny all others
proxy_pass http://backend;
}
# -- Block specific IPs --
location / {
deny 192.168.1.100; # Block specific IP
deny 10.0.0.0/24; # Block subnet
allow all; # Allow all others
# Note: Order of allow/deny matters! The first matching rule is applied
proxy_pass http://backend;
}
10.2 HTTP Basic Authentication
# Create htpasswd file
sudo apt install apache2-utils # Debian/Ubuntu
# sudo yum install httpd-tools # RHEL/CentOS
# Create user (-c: create new file, -B: bcrypt hash)
sudo htpasswd -cB /etc/nginx/.htpasswd admin
# Add user
sudo htpasswd -B /etc/nginx/.htpasswd developer
# -- Apply Basic Auth to specific path --
location /admin/ {
auth_basic "Administrator Area"; # Auth prompt message
auth_basic_user_file /etc/nginx/.htpasswd; # Password file
proxy_pass http://backend;
}
# -- Exempt specific path from auth --
location /admin/health {
auth_basic off; # Health check exempt from auth
proxy_pass http://backend;
}
10.3 IP + Auth Combined (satisfy directive)
location /admin/ {
# satisfy any -> Access allowed if IP is permitted OR auth succeeds
# satisfy all -> Access allowed only when BOTH IP and auth are satisfied
satisfy any;
# IP whitelist
allow 10.0.0.0/8;
deny all;
# Basic Auth (for access outside IP whitelist)
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://backend;
}
10.4 GeoIP-Based Access Control
# Requires GeoIP2 module (ngx_http_geoip2_module)
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 60m;
$geoip2_data_country_code country iso_code;
}
# Block specific countries
map $geoip2_data_country_code $allowed_country {
default yes;
CN no; # China
RU no; # Russia
}
server {
if ($allowed_country = no) {
return 403;
}
}
11. Logging Configuration
11.1 Basic Log Configuration
# -- Access Log --
# Default 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 --
# Levels: debug, info, notice, warn, error, crit, alert, emerg
error_log /var/log/nginx/error.log warn;
11.2 JSON Log Format (Log Analysis Tool Integration)
JSON-formatted structured logging is essential for integration with analysis tools like Elasticsearch, Datadog, and 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 Conditional Logging
# -- Exclude health check request logs --
map $request_uri $loggable {
~*^/health 0;
~*^/ready 0;
~*^/metrics 0;
default 1;
}
access_log /var/log/nginx/access.log combined if=$loggable;
# -- Log only error requests separately --
map $status $is_error {
~^[45] 1;
default 0;
}
access_log /var/log/nginx/error_requests.log combined if=$is_error;
# -- Log slow requests (over 1 second) --
map $request_time $is_slow {
~^[1-9] 1; # 1 second or more
~^[0-9]{2} 1; # 10 seconds or more
default 0;
}
access_log /var/log/nginx/slow_requests.log json_combined if=$is_slow;
11.4 Per-Domain Log Separation
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 Log Rotation (logrotate)
# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily # Rotate daily
missingok # No error if log file is missing
rotate 14 # Keep 14 days
compress # gzip compression
delaycompress # Don't compress the most recent file
notifempty # Don't rotate empty files
create 0640 nginx adm # New file permissions
sharedscripts
postrotate
# Signal Nginx to reopen log files
if [ -f /run/nginx.pid ]; then
kill -USR1 $(cat /run/nginx.pid)
fi
endscript
}
12. Performance Tuning
12.1 Worker Processes and Connections
# -- Main Context --
worker_processes auto; # Match CPU core count (manual: 4, 8, etc.)
worker_rlimit_nofile 65535; # Max file descriptors per Worker
events {
worker_connections 4096; # Max concurrent connections per Worker
multi_accept on; # Accept multiple connections per event loop
use epoll; # Linux: epoll (default)
}
Maximum Concurrent Connections Formula:
Max connections = worker_processes x worker_connections
Example: 4 workers x 4096 connections = 16,384 concurrent connections
With reverse proxy (2 connections per client + backend):
Actual concurrent clients = 16,384 / 2 = 8,192
12.2 Keepalive Settings
http {
# -- Client Keepalive --
keepalive_timeout 65; # Client keepalive duration (seconds)
keepalive_requests 1000; # Max requests per keepalive connection
# -- Timeouts --
client_body_timeout 12; # Client request body receive timeout
client_header_timeout 12; # Client request header receive timeout
send_timeout 10; # Response send timeout to client
reset_timedout_connection on; # Immediately reset timed-out connections (free memory)
}
12.3 Buffer Settings
http {
# -- Client Request Buffers --
client_body_buffer_size 16k; # Request body buffer (writes to disk when exceeded)
client_header_buffer_size 1k; # Request header buffer
client_max_body_size 100m; # Maximum upload size (default 1MB)
large_client_header_buffers 4 16k; # Buffer for large headers
# -- Proxy Buffers --
proxy_buffers 16 32k; # Backend response storage buffers
proxy_buffer_size 16k; # First response (header) buffer
proxy_busy_buffers_size 64k; # Buffer size being sent to client
proxy_temp_file_write_size 64k; # Temp file write size to disk
}
12.4 Comprehensive Production Performance Tuning
# /etc/nginx/nginx.conf -- Production Optimization
user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
pid /run/nginx.pid;
events {
worker_connections 4096;
multi_accept on;
use epoll;
}
http {
# -- MIME & Basic Settings --
include /etc/nginx/mime.types;
default_type application/octet-stream;
charset utf-8;
server_tokens off; # Hide version information
# -- File Transfer Optimization --
sendfile on; # Kernel-level file transfer
tcp_nopush on; # Used with sendfile: packet optimization
tcp_nodelay on; # Disable Nagle algorithm
aio on; # Asynchronous I/O
# -- Keepalive --
keepalive_timeout 65;
keepalive_requests 1000;
# -- Timeouts --
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
reset_timedout_connection on;
# -- Buffers --
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;
# -- Compression, SSL, Logging includes --
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*.conf;
}
13. URL Rewriting and Redirection
13.1 return Directive (Recommended)
return is simpler and more efficient than rewrite. For most URL change cases, return should be considered first.
# -- HTTP to HTTPS Redirect --
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
# -- www to non-www normalization --
server {
listen 443 ssl http2;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
# -- Domain change redirect --
server {
listen 80;
listen 443 ssl http2;
server_name old-domain.com www.old-domain.com;
return 301 https://new-domain.com$request_uri;
}
# -- Specific path redirect --
location /old-page {
return 301 /new-page;
}
# -- Maintenance mode --
location / {
return 503; # Service Unavailable
}
# Combine with error_page
error_page 503 /maintenance.html;
location = /maintenance.html {
root /var/www/html;
internal;
}
13.2 rewrite Directive
rewrite is used when regex-based URL transformation is needed.
# -- Basic syntax --
# rewrite regex replacement [flag];
# flag: last | break | redirect (302) | permanent (301)
# -- Convert to versionless API path --
rewrite ^/api/v1/(.*)$ /api/$1 last;
# /api/v1/users -> /api/users (internal rewrite)
# last: Start new location matching
# -- Remove extensions (Clean URL) --
rewrite ^/(.*)\.html$ /$1 permanent;
# /about.html -> /about (301 redirect)
# -- Rewrite with query string --
rewrite ^/search/(.*)$ /search?q=$1? last;
# Appending ? at end removes original query string
# -- Multilingual URLs --
rewrite ^/ko/(.*)$ /$1?lang=ko last;
rewrite ^/en/(.*)$ /$1?lang=en last;
rewrite ^/ja/(.*)$ /$1?lang=ja last;
rewrite flag comparison:
| Flag | Behavior | Use Case |
|---|---|---|
last | Rewrite then start new location matching | Internal routing change |
break | Rewrite then process within current location | Transformation within current block |
redirect | 302 temporary redirect | Temporary move |
permanent | 301 permanent redirect | Permanent move |
13.3 try_files Directive
try_files sequentially checks for file/directory existence and is essential for SPAs and frameworks.
# -- SPA (React, Vue, Angular, etc.) --
location / {
root /var/www/spa;
try_files $uri $uri/ /index.html;
# 1. $uri: Check if requested file exists
# 2. $uri/: Check if directory exists
# 3. /index.html: If none above exist, serve index.html (SPA routing)
}
# -- 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;
}
# -- Static files first -> backend fallback --
location / {
root /var/www/static;
try_files $uri @backend;
}
location @backend {
proxy_pass http://app_server;
}
13.4 Conditional Redirects
# -- Mobile device redirect --
if ($http_user_agent ~* "(Android|iPhone|iPad)") {
return 302 https://m.example.com$request_uri;
}
# -- Based on specific query parameter --
if ($arg_redirect) {
return 302 $arg_redirect;
}
# -- Manage complex redirects with 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. Static File Serving Optimization
14.1 Core File Transfer Directives
http {
# -- sendfile --
# Use the kernel's sendfile() system call to transfer files directly to sockets
# Eliminates user-space memory copy, reducing CPU usage and context switching
sendfile on;
# -- tcp_nopush --
# Works with sendfile: sends HTTP headers and file start in a single packet
# Reduces network packet count for better bandwidth efficiency
tcp_nopush on;
# -- tcp_nodelay --
# Disable Nagle algorithm: send small packets immediately
# Reduces latency on keepalive connections (can be used with tcp_nopush)
tcp_nodelay on;
}
Transfer Method Comparison:
sendfile off (default):
Disk -> Kernel Buffer -> User Memory (Nginx) -> Kernel Socket Buffer -> Network
[read] [copy] [write]
sendfile on:
Disk -> Kernel Buffer ─────────────────────────-> Kernel Socket Buffer -> Network
[read] [zero-copy: no CPU involvement] [transfer]
14.2 Open File Cache
Caches file descriptors, sizes, and modification times of frequently requested files to minimize filesystem lookups.
http {
# Cache up to 10,000 file info entries, remove after 30s of inactivity
open_file_cache max=10000 inactive=30s;
# Re-validate cached info every 60 seconds
open_file_cache_valid 60s;
# Only cache files requested 2+ times (filter one-time requests)
open_file_cache_min_uses 2;
# Cache file-not-found (ENOENT) errors too (prevent repeated lookups for missing files)
open_file_cache_errors on;
}
14.3 Comprehensive Static File Serving Configuration
server {
listen 443 ssl http2;
server_name static.example.com;
root /var/www/static;
# -- Images --
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; # Disable 404 logging too
# Image-specific limits
limit_rate 2m; # 2MB/s per connection
}
# -- CSS/JS (with cache busting strategy) --
location ~* \.(css|js)$ {
expires 365d; # Long-term cache if hash-based filenames
add_header Cache-Control "public, immutable";
gzip_static on;
brotli_static on;
}
# -- Fonts --
location ~* \.(woff|woff2|ttf|eot|otf)$ {
expires 365d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
# -- Media files --
location ~* \.(mp4|webm|ogg|mp3|wav)$ {
expires 30d;
add_header Cache-Control "public";
# Range request support (video seeking)
add_header Accept-Ranges bytes;
}
# -- Prevent directory listing --
location / {
autoindex off;
}
# -- Block access to hidden files --
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
14.4 HTTP/2 Server Push (Optional)
# Pre-send key resources before client requests them
location = /index.html {
http2_push /css/main.css;
http2_push /js/app.js;
http2_push /images/logo.webp;
}
Note: HTTP/2 Server Push support is being discontinued in some browsers, and
103 Early Hintsis emerging as an alternative.
15. Health Checks and Monitoring
15.1 Stub Status (Basic Monitoring)
The stub_status module included in Nginx OSS provides real-time connection status.
server {
listen 8080; # Separate port
server_name localhost;
# Allow access only from internal network
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 output example:
Active connections: 291
server accepts handled requests
16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
| Field | Meaning |
|---|---|
Active connections | Current active connections (Reading + Writing + Waiting) |
accepts | Total accepted connections |
handled | Total handled connections (= accepts means normal) |
requests | Total processed requests |
Reading | Connections reading client request headers |
Writing | Connections sending response to client |
Waiting | Keepalive idle connections |
15.2 Passive Health Check (Upstream Monitoring)
Nginx OSS supports only passive health checks based on actual traffic.
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: Marked unhealthy after 3 consecutive failures within 30s
# fail_timeout=30s: No requests sent to server for 30s after being marked unhealthy
# After 30s, requests are sent again to check recovery
}
server {
location / {
proxy_pass http://backend;
# Define what responses count as "failure"
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_next_upstream_timeout 10s; # Max time to try next server
proxy_next_upstream_tries 3; # Max retry count
}
}
15.3 Active Health Check (NGINX Plus or External Solutions)
# -- Active health check in NGINX Plus --
upstream backend {
zone backend_zone 64k; # Shared memory zone required
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}
server {
location / {
proxy_pass http://backend;
health_check interval=5s # Check every 5 seconds
fails=3 # Marked unhealthy after 3 failures
passes=2 # Recovered after 2 successes
uri=/health; # Health check endpoint
}
}
15.4 Prometheus Integration (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 Custom Health Check Endpoints
# -- Liveness Probe (Nginx itself operational check) --
location = /healthz {
access_log off;
return 200 "alive\n";
add_header Content-Type text/plain;
}
# -- Readiness Probe (including backend connection check) --
location = /readyz {
access_log off;
proxy_pass http://backend/health;
proxy_connect_timeout 2s;
proxy_read_timeout 2s;
# Return 503 on backend response failure
error_page 502 503 504 = @not_ready;
}
location @not_ready {
return 503 "not ready\n";
add_header Content-Type text/plain;
}
# Usage in 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
Production Configuration Checklist
A summary of essential items to verify when deploying Nginx to production environments.
| Category | Item | Status |
|---|---|---|
| Architecture | worker_processes auto configured | [ ] |
| Architecture | worker_connections set to appropriate value | [ ] |
| SSL/TLS | TLSv1.2 + TLSv1.3 only enabled | [ ] |
| SSL/TLS | Strong Cipher Suites configured | [ ] |
| SSL/TLS | OCSP Stapling enabled | [ ] |
| SSL/TLS | HSTS header configured | [ ] |
| SSL/TLS | HTTP to HTTPS redirect | [ ] |
| Security | server_tokens off | [ ] |
| Security | Security headers (CSP, X-Frame-Options, etc.) configured | [ ] |
| Security | Rate Limiting configured | [ ] |
| Security | Admin page access control | [ ] |
| Performance | sendfile, tcp_nopush, tcp_nodelay enabled | [ ] |
| Performance | Gzip/Brotli compression configured | [ ] |
| Performance | open_file_cache configured | [ ] |
| Performance | Static file browser caching configured | [ ] |
| Performance | Upstream keepalive configured | [ ] |
| Caching | proxy_cache or fastcgi_cache configured | [ ] |
| Caching | proxy_cache_use_stale configured | [ ] |
| Monitoring | stub_status enabled (internal only) | [ ] |
| Monitoring | Health check endpoints configured | [ ] |
| Logging | JSON log format configured | [ ] |
| Logging | Log rotation configured | [ ] |
| Logging | Health check/static file logging excluded | [ ] |
| Proxy | Essential proxy headers configured | [ ] |
| Proxy | WebSocket proxy configured (if needed) | [ ] |
| Load Balancing | Appropriate algorithm selected | [ ] |
| Load Balancing | Backup server configured | [ ] |
References
- 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
Quiz
Q1: What is the main topic covered in "Nginx Configuration Complete Guide: 15 Essential Topics
from Architecture to Production Optimization"?
From Nginx event-driven architecture and configuration structure to reverse proxy, load balancing, SSL/TLS, caching, rate limiting, security headers, performance tuning, and health checks -- a comprehensive guide to 15 essential production-ready configurations with practical exam...
Q2: What are the key steps for Nginx Architecture and Configuration Structure?
1.1 Event-Driven Architecture: Master-Worker Model Nginx adopts a fundamentally different
event-driven architecture compared to Apache httpd's process/thread-based model.
Q3: What are the key steps for Virtual Host / Server Block Configuration?
Nginx Server Blocks are the equivalent of Apache Virtual Hosts, allowing you to host multiple
domains on a single server. 2.1 Basic Server Block Configuration 2.2 Default Server (catch-all)
This is the default server block that handles requests for undefined domains.
Q4: What are the key steps for Reverse Proxy Configuration?
3.1 Basic Reverse Proxy Purpose of each header: 3.2 Reusable Proxy Parameter Snippet 3.3 WebSocket
Proxy WebSocket uses the HTTP Upgrade mechanism, so the Upgrade and Connection hop-by-hop headers
must be explicitly forwarded. Nginx does not forward these headers by default.
Q5: How does Load Balancing work?
4.1 Upstream Block and Algorithms Algorithm Selection Guide: 4.2 Server Status and Backup 4.3
Keepalive Connection Pool Reuses TCP connections to backend servers to reduce connection
setup/teardown overhead.