← posts/b.log()

blog92@web:~$ cat posts/nginx-caddy-03-nginx-intermediate.md

DEVOPS7 min read

Nginx & Caddy 완전 정복 3편 — Nginx 중급: location 매칭, 리버스 프록시, 로드 밸런싱, HTTPS

server/location 선택 알고리즘, proxy_pass 슬래시 규칙, 프록시 헤더와 WebSocket, upstream 로드 밸런싱, certbot과 공식 ACME 모듈을 이용한 HTTPS 설정까지 다룹니다.

1. 요청은 어떤 server 블록으로 가는가

하나의 Nginx가 여러 도메인을 서비스할 때(가상 호스트), 요청이 들어오면 Nginx는 두 단계로 server 블록을 고릅니다.

  1. listen으로 1차 필터링: 요청이 들어온 IP:포트와 일치하는 server들만 후보가 됩니다.
  2. server_name으로 2차 선택: Host 헤더와 비교합니다. 우선순위는 다음과 같습니다.
    1. 정확한 이름 (example.com)
    2. 앞쪽 와일드카드 중 가장 긴 것 (*.example.com)
    3. 뒤쪽 와일드카드 중 가장 긴 것 (mail.*)
    4. 정규식, 파일에 나온 순서대로 첫 번째 일치 (~^www\d+\.example\.com$)
  3. 아무것도 일치하지 않으면 해당 포트의 default server로 갑니다. listen 80 default_server;로 명시하지 않으면 그 포트의 첫 번째 server 블록이 default가 됩니다.

이 마지막 규칙 때문에 엉뚱한 도메인(혹은 IP로 직접 접근한 요청)이 첫 번째 사이트로 빠지는 일이 생깁니다. 운영 환경에서는 "알 수 없는 Host는 버린다"는 catch-all을 두는 것이 좋습니다.

nginx
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;   # 1.19.4+: 인증서 없이 TLS 핸드셰이크 거부
    return 444;                # Nginx 전용: 응답 없이 연결 종료
}

2. location 매칭 규칙 완전 정리

Nginx에서 가장 중요하고 가장 헷갈리는 부분입니다.

수식어(modifier) 종류

문법의미
location = /path정확히 일치
location ^~ /path접두사 일치, 일치하면 정규식 검사 생략
location ~ regex정규식 (대소문자 구분)
location ~* regex정규식 (대소문자 무시)
location /path일반 접두사 일치
location @name이름 있는 location (내부 리다이렉트 전용)

선택 알고리즘

text
1. "=" 정확 일치가 있으면 → 즉시 선택, 종료
2. 모든 접두사 location 중 "가장 긴 일치"를 찾아 기억
   2-1. 그것이 "^~"면 → 즉시 선택, 종료
3. 정규식 location을 "파일에 적힌 순서대로" 검사
   3-1. 처음 일치하는 것 → 선택, 종료
4. 정규식이 하나도 안 맞으면 → 2단계에서 기억한 접두사 location 선택

핵심은 "접두사는 길이로, 정규식은 순서로", 그리고 정규식이 일반 접두사보다 우선한다는 점입니다.

퀴즈로 익히기

nginx
location = / {              # A
}
location / {                # B
}
location /documents/ {      # C
}
location ^~ /images/ {      # D
}
location ~* \.(gif|jpg|png)$ {  # E
}
요청선택이유
/A정확 일치
/index.htmlB가장 긴 접두사는 B, 정규식 불일치
/documents/doc.htmlC가장 긴 접두사는 C, 정규식 불일치
/documents/1.jpgE접두사 C가 기억되지만 정규식 E가 우선
/images/1.jpgD^~이므로 정규식 검사 생략

/documents/1.jpg가 C가 아니라 E로 간다는 것이 많은 사람의 예상을 깹니다. 정적 파일 디렉터리처럼 "이 경로는 무조건 여기서 처리"하고 싶다면 ^~를 쓰세요.

3. 리버스 프록시 기본

proxy_pass

nginx
server {
    listen 80;
    server_name app.example.com;
 
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

⚠️ proxy_pass의 슬래시 규칙

proxy_pass 뒤에 URI 부분(포트 뒤의 /...)이 있느냐 없느냐로 동작이 완전히 달라집니다.

nginx
# (1) URI 없음 → 원래 요청 URI를 그대로 전달
location /api/ {
    proxy_pass http://backend;
}
# /api/users → http://backend/api/users
 
# (2) URI 있음 (슬래시 하나라도) → location에 매칭된 부분을 그 URI로 "치환"
location /api/ {
    proxy_pass http://backend/;
}
# /api/users → http://backend/users
 
location /api/ {
    proxy_pass http://backend/v2/;
}
# /api/users → http://backend/v2/users

백엔드가 /api 접두사를 기대하는지 아닌지에 따라 골라 쓰면 됩니다. location과 proxy_pass 양쪽의 끝 슬래시를 맞추는 습관을 들이면 //users 같은 이중 슬래시 사고를 줄일 수 있습니다.

정규식 location 안이나 rewrite를 거친 뒤에는 proxy_pass에 URI를 쓸 수 없거나 무시됩니다. 경로를 바꾸고 싶다면 rewrite ^/api/(.*)$ /$1 break; 후 URI 없는 proxy_pass를 쓰세요.

반드시 넘겨야 할 헤더

Nginx가 프록시를 하면 백엔드 입장에서 요청자는 Nginx(127.0.0.1)가 됩니다. 원래 정보를 헤더로 전달해 줘야 합니다.

nginx
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;
}
  • Host: 기본값은 proxy_pass에 적힌 호스트(127.0.0.1:3000)입니다. 백엔드가 도메인 기반 처리를 한다면 꼭 넘겨야 합니다.
  • X-Forwarded-For: $proxy_add_x_forwarded_for는 기존 XFF 헤더에 $remote_addr를 덧붙입니다.
  • X-Forwarded-Proto: 백엔드가 "지금 HTTPS인가?"를 판단할 근거입니다. 이게 없으면 Next.js, Spring 등에서 리다이렉트 URL이 http://로 생성되는 문제가 자주 생깁니다.

이 블록은 매번 반복되므로 파일로 빼서 include하는 게 일반적입니다.

nginx
# /etc/nginx/snippets/proxy-headers.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;
nginx
location / {
    proxy_pass http://127.0.0.1:3000;
    include snippets/proxy-headers.conf;
}

타임아웃과 버퍼링

nginx
proxy_connect_timeout 5s;     # 백엔드 연결 수립 대기 (기본 60s)
proxy_send_timeout    60s;    # 백엔드로 보내는 중 두 write 사이 간격
proxy_read_timeout    60s;    # 백엔드 응답 두 read 사이 간격 (기본 60s)
 
proxy_buffering on;           # 기본 on: 응답을 버퍼에 받아두고 클라이언트에 전송
  • 오래 걸리는 리포트 생성 API에서 504 Gateway Timeout이 뜬다면 proxy_read_timeout을 늘립니다.
  • SSE(Server-Sent Events)나 스트리밍 응답(LLM 토큰 스트리밍 등)은 버퍼링 때문에 한꺼번에 도착할 수 있습니다. 해당 location에서 proxy_buffering off;를 쓰거나, 백엔드가 X-Accel-Buffering: no 헤더를 내려주면 됩니다.

WebSocket 프록시

WebSocket은 HTTP/1.1의 Upgrade 메커니즘을 사용하는데, Upgrade와 Connection은 hop-by-hop 헤더라 프록시가 기본적으로 전달하지 않습니다. 명시적으로 넘겨야 합니다.

nginx
http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
 
    server {
        location /ws/ {
            proxy_pass http://127.0.0.1:4000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host       $host;
            proxy_read_timeout 1h;   # 유휴 연결이 60초 만에 끊기지 않도록
        }
    }
}

map은 입력 변수 값에 따라 새 변수를 만드는 지시어입니다. Upgrade 요청이면 upgrade, 일반 요청이면 close를 넣어줍니다.

4. 로드 밸런싱: upstream

nginx
upstream app_servers {
    least_conn;                                  # 알고리즘 (생략 시 round-robin)
 
    server 10.0.0.11:3000 weight=3;              # 가중치
    server 10.0.0.12:3000 max_fails=3 fail_timeout=30s;
    server 10.0.0.13:3000 backup;                # 나머지가 모두 죽었을 때만 사용
    server 10.0.0.14:3000 down;                  # 일시적으로 제외
 
    keepalive 32;                                # 업스트림 커넥션 재사용
}
 
server {
    listen 80;
    location / {
        proxy_pass http://app_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";          # keepalive를 위해 필수
        include snippets/proxy-headers.conf;
    }
}

알고리즘

지시어동작
(없음)가중치 라운드 로빈
least_conn;활성 연결이 가장 적은 서버
ip_hash;클라이언트 IP(IPv4는 앞 3옥텟) 기반 고정
hash $key consistent;임의 키 기반 일관 해싱 (예: $request_uri로 캐시 서버 분산)
random two least_conn;무작위로 두 개 고른 뒤 덜 바쁜 쪽

헬스 체크: 오픈소스는 패시브만

오픈소스 Nginx는 패시브 헬스 체크만 지원합니다.

  • max_fails=3 fail_timeout=30s: fail_timeout 시간 안에 3번 실패하면 그 서버를 fail_timeout 동안 제외합니다.
  • 무엇을 "실패"로 볼지는 proxy_next_upstream으로 정합니다.
nginx
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;

주기적으로 /health를 호출하는 액티브 헬스 체크(health_check 지시어)는 상용 NGINX Plus 전용입니다. 오픈소스에서 필요하다면 서드파티 모듈을 직접 빌드하거나, 바깥의 모니터링/오케스트레이터(쿠버네티스 readiness probe 등)에 맡겨야 합니다. 이 부분은 Caddy와 비교할 때 중요한 차이입니다.

keepalive가 중요한 이유

keepalive 없이 프록시하면 요청마다 백엔드와 TCP 연결을 새로 맺고 끊습니다. 트래픽이 많으면 TIME_WAIT 소켓이 쌓이고 지연이 늘어납니다. keepalive N + proxy_http_version 1.1 + Connection "" 세 가지를 세트로 기억하세요.

5. HTTPS 설정

방법 1: certbot (가장 보편적)

bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

certbot이 인증서를 받고 Nginx 설정에 ssl_certificate 등을 자동으로 삽입합니다. 갱신은 systemd timer(또는 cron)로 자동 등록됩니다.

bash
sudo certbot renew --dry-run   # 갱신 테스트
systemctl list-timers | grep certbot

설정 파일을 certbot이 수정하는 게 싫다면 certbot certonly --webroot -w /var/www/certbot -d example.com으로 인증서만 받고, 설정은 직접 쓰는 방식도 있습니다. 이 경우 갱신 후 Nginx가 새 인증서를 읽도록 --deploy-hook "systemctl reload nginx"를 걸어줘야 합니다.

방법 2: 공식 ACME 모듈 (ngx_http_acme_module)

2025년 8월, Nginx 팀은 Nginx 자체에서 ACME로 인증서를 발급·갱신하는 공식 모듈을 공개했습니다. Rust로 작성된 동적 모듈이며, nginx.org 저장소에서 nginx-module-acme 패키지로 설치할 수 있습니다. 비교적 새로운 모듈이므로 도입 전 공식 문서에서 현재 지원 범위(챌린지 종류 등)를 꼭 확인하세요.

nginx
load_module modules/ngx_http_acme_module.so;   # main 컨텍스트
 
http {
    resolver 1.1.1.1:53 ipv6=off;               # ACME 서버 이름 해석용
 
    acme_issuer letsencrypt {
        uri         https://acme-v02.api.letsencrypt.org/directory;
        contact     admin@example.com;
        state_path  /var/lib/nginx/acme-letsencrypt;
        accept_terms_of_service;
    }
 
    acme_shared_zone zone=ngx_acme_shared:1M;
 
    server {
        listen 443 ssl;
        server_name example.com;
 
        acme_certificate letsencrypt;
        ssl_certificate       $acme_certificate;
        ssl_certificate_key   $acme_certificate_key;
        ssl_certificate_cache max=2;
 
        location / { proxy_pass http://127.0.0.1:3000; }
    }
 
    server {
        listen 80;   # HTTP-01 챌린지 응답을 위해 80 포트 필요
        server_name example.com;
        location / { return 301 https://$host$request_uri; }
    }
}

certbot 같은 외부 도구 없이 인증서 수명 주기를 Nginx 안에서 끝낼 수 있게 되었다는 점에서 의미가 큽니다. 다만 Caddy처럼 "도메인만 적으면 끝"인 수준은 아니고, 여전히 명시적인 설정이 필요합니다.

권장 TLS 설정

nginx
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;                                    # 1.25.1+ 문법
    server_name 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;
    ssl_prefer_server_ciphers off;               # TLS 1.3 시대에는 클라이언트 선택 존중
 
    ssl_session_cache   shared:SSL:10m;          # 세션 재개로 핸드셰이크 비용 절감
    ssl_session_timeout 1d;
    ssl_session_tickets off;
 
    add_header Strict-Transport-Security "max-age=63072000" always;
}
 
server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
  • 예전 문서에서 흔히 보이는 listen 443 ssl http2; 표기는 1.25.1부터 deprecated되었고, http2 on; 지시어로 바뀌었습니다.
  • 암호화 스위트(cipher)를 직접 고르기 어렵다면 Mozilla SSL Configuration Generator의 "Intermediate" 프로필을 기준으로 삼는 것이 업계 표준입니다.
  • HSTS는 한 번 설정하면 브라우저가 max-age 동안 HTTP 접근을 거부하므로, 처음에는 짧은 값으로 테스트한 뒤 늘리세요.

HTTP/3 (QUIC)

1.25.0부터 HTTP/3가 공식 지원됩니다. nginx -V에 --with-http_v3_module이 있어야 합니다.

nginx
server {
    listen 443 quic reuseport;   # UDP 443 (reuseport는 포트당 한 번만)
    listen 443 ssl;              # TCP 443 (HTTP/1.1, HTTP/2)
    http2 on;
 
    add_header Alt-Svc 'h3=":443"; ma=86400';   # 브라우저에 HTTP/3 존재 알림
    # ...
}

방화벽에서 UDP 443을 열어야 한다는 점을 잊기 쉽습니다.

6. rewrite와 return

nginx
# 단순 리다이렉트는 rewrite보다 return이 명확하고 빠르다
location /old-blog/ {
    return 301 /blog/;
}
 
# 패턴 기반 경로 변환
rewrite ^/users/(\d+)$ /profile?id=$1 last;

rewrite 플래그:

  • last: rewrite 후 location 매칭을 다시 수행
  • break: rewrite 후 현재 location에서 계속 처리
  • redirect / permanent: 302 / 301 리다이렉트 응답

정리

  • server 선택은 listen → server_name → default server 순이며, catch-all server를 두는 것이 안전하다.
  • location은 "정확 일치 → 가장 긴 접두사(^~면 확정) → 정규식(순서대로) → 기억한 접두사" 순으로 결정된다.
  • proxy_pass 뒤 URI 유무에 따라 경로 치환 여부가 갈린다.
  • 프록시 헤더, WebSocket 헤더, upstream keepalive 세트는 외워둘 가치가 있다.
  • 오픈소스 Nginx는 패시브 헬스 체크만 지원한다.
  • HTTPS는 certbot이 보편적이며, 새로 나온 공식 ACME 모듈로 Nginx 안에서 자동화할 수도 있다.

다음 편에서는 Nginx를 운영 수준으로 끌어올리는 캐싱, Rate Limit, 보안 헤더, 성능 튜닝, L4 프록시를 다룹니다.

COMMENTS (…)

댓글을 불러오는 중이에요.

NEW COMMENT0 / 1000
⌘↵ 전송

blog92@web:~$ cd ..