Skip to content

Split View: Nginx 설정 완벽 가이드: 아키텍처부터 프로덕션 최적화까지 15가지 핵심 주제

|

Nginx 설정 완벽 가이드: 아키텍처부터 프로덕션 최적화까지 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 3Event 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)
EventsMain 내부연결 처리 메커니즘 (worker_connections)
HTTPMain 내부HTTP 프로토콜 관련 전체 설정
ServerHTTP 내부가상 호스트 (도메인별 설정)
LocationServer 내부URL 경로별 요청 처리 규칙
UpstreamHTTP 내부백엔드 서버 그룹 (로드 밸런싱)
StreamMain 내부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 매칭 시 다음 우선순위를 따른다:

  1. 정확한 이름: server_name example.com
  2. 앞쪽 와일드카드: server_name *.example.com
  3. 뒤쪽 와일드카드: server_name example.*
  4. 정규표현식: server_name ~^(?<subdomain>.+)\.example\.com$
  5. 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 메커니즘을 사용하므로, UpgradeConnection 홉별(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_pass URL 끝에 /가 있으면 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

시간 030개 요청 도착:
├── 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 L670-75%낮음99%+
Brotli L675-80%중간96%+
Brotli L1180-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 디렉티브 (권장)

returnrewrite보다 단순하고 효율적이다. 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 내에서 처리현재 블록 내 변환
redirect302 임시 리다이렉트임시 이동
permanent301 영구 리다이렉트영구 이동

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클라이언트에 응답을 보내는 중인 연결 수
Waitingkeepalive 유휴 연결 수

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/TLSTLSv1.2 + TLSv1.3만 활성화[ ]
SSL/TLS강력한 Cipher Suites 설정[ ]
SSL/TLSOCSP Stapling 활성화[ ]
SSL/TLSHSTS 헤더 설정[ ]
SSL/TLSHTTP → 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 Configuration Complete Guide: 15 Essential Topics from Architecture to Production Optimization


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 3Event 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:

ContextLocationPurpose
MainTop-levelGlobal settings (user, worker, pid, error_log)
EventsInside MainConnection handling mechanism (worker_connections)
HTTPInside MainAll HTTP protocol-related settings
ServerInside HTTPVirtual host (per-domain settings)
LocationInside ServerRequest handling rules per URL path
UpstreamInside HTTPBackend server group (load balancing)
StreamInside MainTCP/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:

  1. Exact name: server_name example.com
  2. Leading wildcard: server_name *.example.com
  3. Trailing wildcard: server_name example.*
  4. Regular expression: server_name ~^(?<subdomain>.+)\.example\.com$
  5. 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:

HeaderPurpose
HostPass the original request Host header
X-Real-IPActual client IP (identify original IP behind proxy)
X-Forwarded-ForAccumulated client IP list through proxy chain
X-Forwarded-ProtoOriginal protocol (http/https) -- used for redirect decisions
X-Forwarded-HostOriginal Host header
X-Forwarded-PortOriginal 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_pass URL ends with /, the portion matching the location is stripped. A request to /api/users/123 is forwarded to http://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:

AlgorithmSuitable ForConsiderations
Round RobinStateless services, identical serversImbalance if server specs differ
WeightedServers with different specsManual weight management needed
Least ConnectionsHigh variance in request processing timeSimilar to Round Robin for short requests
IP HashSession affinity needed (legacy apps)Redistribution occurs on server add/remove
Generic HashCache 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 well
  • preload: Qualifies for browser HSTS Preload list registration
  • always: 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:

StatusMeaning
HITServed directly from cache
MISSNo cache -> requested from backend
EXPIREDExpired cache -> re-requested from backend
STALEExpired but served via stale policy
UPDATINGStale cache served while being updated
BYPASSCache 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:

AlgorithmCompression Ratio (typical JS)CPU Load (dynamic)Browser Support
Gzip L670-75%Low99%+
Brotli L675-80%Medium96%+
Brotli L1180-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

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:

FlagBehaviorUse Case
lastRewrite then start new location matchingInternal routing change
breakRewrite then process within current locationTransformation within current block
redirect302 temporary redirectTemporary move
permanent301 permanent redirectPermanent 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 Hints is 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
FieldMeaning
Active connectionsCurrent active connections (Reading + Writing + Waiting)
acceptsTotal accepted connections
handledTotal handled connections (= accepts means normal)
requestsTotal processed requests
ReadingConnections reading client request headers
WritingConnections sending response to client
WaitingKeepalive 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.

CategoryItemStatus
Architectureworker_processes auto configured[ ]
Architectureworker_connections set to appropriate value[ ]
SSL/TLSTLSv1.2 + TLSv1.3 only enabled[ ]
SSL/TLSStrong Cipher Suites configured[ ]
SSL/TLSOCSP Stapling enabled[ ]
SSL/TLSHSTS header configured[ ]
SSL/TLSHTTP to HTTPS redirect[ ]
Securityserver_tokens off[ ]
SecuritySecurity headers (CSP, X-Frame-Options, etc.) configured[ ]
SecurityRate Limiting configured[ ]
SecurityAdmin page access control[ ]
Performancesendfile, tcp_nopush, tcp_nodelay enabled[ ]
PerformanceGzip/Brotli compression configured[ ]
Performanceopen_file_cache configured[ ]
PerformanceStatic file browser caching configured[ ]
PerformanceUpstream keepalive configured[ ]
Cachingproxy_cache or fastcgi_cache configured[ ]
Cachingproxy_cache_use_stale configured[ ]
Monitoringstub_status enabled (internal only)[ ]
MonitoringHealth check endpoints configured[ ]
LoggingJSON log format configured[ ]
LoggingLog rotation configured[ ]
LoggingHealth check/static file logging excluded[ ]
ProxyEssential proxy headers configured[ ]
ProxyWebSocket proxy configured (if needed)[ ]
Load BalancingAppropriate algorithm selected[ ]
Load BalancingBackup server configured[ ]

References

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.