1. 요청은 어떤 server 블록으로 가는가
하나의 Nginx가 여러 도메인을 서비스할 때(가상 호스트), 요청이 들어오면 Nginx는 두 단계로 server 블록을 고릅니다.
listen으로 1차 필터링: 요청이 들어온 IP:포트와 일치하는 server들만 후보가 됩니다.server_name으로 2차 선택: Host 헤더와 비교합니다. 우선순위는 다음과 같습니다.- 정확한 이름 (
example.com) - 앞쪽 와일드카드 중 가장 긴 것 (
*.example.com) - 뒤쪽 와일드카드 중 가장 긴 것 (
mail.*) - 정규식, 파일에 나온 순서대로 첫 번째 일치 (
~^www\d+\.example\.com$)
- 정확한 이름 (
- 아무것도 일치하지 않으면 해당 포트의 default server로 갑니다.
listen 80 default_server;로 명시하지 않으면 그 포트의 첫 번째 server 블록이 default가 됩니다.
이 마지막 규칙 때문에 엉뚱한 도메인(혹은 IP로 직접 접근한 요청)이 첫 번째 사이트로 빠지는 일이 생깁니다. 운영 환경에서는 "알 수 없는 Host는 버린다"는 catch-all을 두는 것이 좋습니다.
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 (내부 리다이렉트 전용) |
선택 알고리즘
1. "=" 정확 일치가 있으면 → 즉시 선택, 종료
2. 모든 접두사 location 중 "가장 긴 일치"를 찾아 기억
2-1. 그것이 "^~"면 → 즉시 선택, 종료
3. 정규식 location을 "파일에 적힌 순서대로" 검사
3-1. 처음 일치하는 것 → 선택, 종료
4. 정규식이 하나도 안 맞으면 → 2단계에서 기억한 접두사 location 선택핵심은 "접두사는 길이로, 정규식은 순서로", 그리고 정규식이 일반 접두사보다 우선한다는 점입니다.
퀴즈로 익히기
location = / { # A
}
location / { # B
}
location /documents/ { # C
}
location ^~ /images/ { # D
}
location ~* \.(gif|jpg|png)$ { # E
}| 요청 | 선택 | 이유 |
|---|---|---|
/ | A | 정확 일치 |
/index.html | B | 가장 긴 접두사는 B, 정규식 불일치 |
/documents/doc.html | C | 가장 긴 접두사는 C, 정규식 불일치 |
/documents/1.jpg | E | 접두사 C가 기억되지만 정규식 E가 우선 |
/images/1.jpg | D | ^~이므로 정규식 검사 생략 |
/documents/1.jpg가 C가 아니라 E로 간다는 것이 많은 사람의 예상을 깹니다. 정적 파일 디렉터리처럼 "이 경로는 무조건 여기서 처리"하고 싶다면 ^~를 쓰세요.
3. 리버스 프록시 기본
proxy_pass
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}⚠️ proxy_pass의 슬래시 규칙
proxy_pass 뒤에 URI 부분(포트 뒤의 /...)이 있느냐 없느냐로 동작이 완전히 달라집니다.
# (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)가 됩니다. 원래 정보를 헤더로 전달해 줘야 합니다.
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하는 게 일반적입니다.
# /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;location / {
proxy_pass http://127.0.0.1:3000;
include snippets/proxy-headers.conf;
}타임아웃과 버퍼링
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 헤더라 프록시가 기본적으로 전달하지 않습니다. 명시적으로 넘겨야 합니다.
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
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으로 정합니다.
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 (가장 보편적)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.comcertbot이 인증서를 받고 Nginx 설정에 ssl_certificate 등을 자동으로 삽입합니다. 갱신은 systemd timer(또는 cron)로 자동 등록됩니다.
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 패키지로 설치할 수 있습니다. 비교적 새로운 모듈이므로 도입 전 공식 문서에서 현재 지원 범위(챌린지 종류 등)를 꼭 확인하세요.
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 설정
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이 있어야 합니다.
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
# 단순 리다이렉트는 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 프록시를 다룹니다.