nginx 설정 읽기 — server·location·upstream
설정 파일을 처음 열면 중괄호만 잔뜩 보인다. 그런데 요청 하나가 들어왔을 때 nginx 가 무엇을 순서대로 고르는지를 알면 파일이 그 순서대로 읽힌다.
1. 블록의 계층
# 전역 — 프로세스 수준
worker_processes auto;
events { ... } # 연결 처리 방식
http { # HTTP 전반의 기본값
include mime.types;
sendfile on;
keepalive_timeout 65;
upstream backend { ... } # 뒤로 보낼 서버 묶음
server { # 가상 호스트 하나
listen 443 ssl;
server_name shop.example.com;
location /api/ { ... } # 경로별 처리
location / { ... }
}
}
바깥 블록의 지시자는 안쪽으로 상속된다 안쪽에서 같은 지시자를 다시 쓰면 그것이 이긴다
- http — 사이트 전체 기본값
- server — 이 도메인만
- location 이 경로만
2. 어느 server 인가 — 가상 호스트 선택
한 IP·한 포트에 여러 사이트가 있을 수 있다. nginx 는 이렇게 고른다.
- ① listen 이 맞는 server 들을 추린다 (IP · 포트 · 옵션)
- ② 그중 Host 헤더와 server_name 이 맞는 것을 고른다
- 정확히 일치 → 앞쪽 와일드카드(.example.com) → 뒤쪽(www.) → 정규식
- ③ 맞는 게 없으면 default_server 로 간다
- 지정하지 않았으면 그 listen 의 '첫 번째' server 가 기본이 된다
server {
listen 443 ssl default_server;
server_name _; # 어디에도 안 맞는 요청
ssl_certificate /etc/ssl/dummy.crt;
ssl_certificate_key /etc/ssl/dummy.key;
return 444; # 응답 없이 연결을 끊는다 (nginx 전용 코드)
}
왜 이런 걸 두나 — 도메인을 우리 IP 로 가리키게 해 놓고 들어오는 스캔 트래픽이 첫 번째 server 블록으로 흘러들어 엉뚱한 사이트를 노출하는 일이 실제로 생긴다. 명시적 default_server 를 두고 거기서 끊는 것이 안전하다.
HTTPS 는 순서가 하나 더 있다 — TLS 핸드셰이크가 먼저이므로 Host 헤더를 읽기 전에 인증서를 골라야 한다. 이때 쓰는 것이 SNI(Server Name Indication)다. SNI 로 인증서를 고르고, 복호화한 뒤 Host 로 server 를 확정한다.
3. 어느 location 인가 — 이게 가장 헷갈린다
location = /exact { } # ① 정확히 일치. 맞으면 즉시 확정
location ^~ /images/ { } # ② 접두사 일치 + 정규식 검사 생략
location ~ \.php$ { } # ③ 정규식 (대소문자 구분)
location ~* \.(jpg|png)$ { } # ④ 정규식 (대소문자 무시)
location /docs/ { } # ⑤ 일반 접두사
location / { } # ⑥ 모든 것에 맞는 접두사
매칭 순서 — 파일에 적힌 순서가 아니다.
- '=' 정확히 일치가 있으면 → 즉시 확정. 끝
- 접두사 중 '가장 긴 것' 을 기억해 둔다
- 그 접두사가 '^~' 였다면 → 정규식을 건너뛰고 확정
- 아니면 정규식을 '파일에 적힌 순서대로' 검사해 첫 일치를 쓴다
- 정규식에 하나도 안 맞으면 → 2번에서 기억해 둔 접두사를 쓴다
접두사끼리는 '길이' 로 경쟁하고, 정규식끼리는 '순서' 로 경쟁한다 그리고 정규식이 접두사보다 우선한다 (^~ 로 막지 않는 한)
실제로 물려서 사고가 나는 예:
location /static/ { root /var/www; }
location ~ \.js$ { deny all; } # 의도: 특정 경로의 js 차단
# /static/app.js 요청 →
# 접두사 /static/ 이 맞지만, 정규식 \.js$ 도 맞는다
# 정규식이 이기므로 → 403
# /static/ 아래 js 가 통째로 막힌다
# 고치려면 — 정적 경로에 정규식 검사를 아예 건너뛰게 한다
location ^~ /static/ { root /var/www; }
4. root 와 alias — 한 글자 차이로 경로가 달라진다
location /images/ {
root /var/www; # → /var/www/images/logo.png
}
location /images/ {
alias /var/www/pics/; # → /var/www/pics/logo.png
}
-
alias — location 경로를 '대체한다'
-
alias 는 끝의 슬래시를 맞춰야 한다
- location /images/ + alias /var/www/pics; ← 슬래시 불일치 → 경로가 어긋난다
alias를 정규식 location 과 함께 쓰면 캡처 그룹을 명시해야 해서 실수가 잦다. 가능하면root로 푼다 는 것이 실무 기본값이다.
5. proxy_pass — 슬래시 하나가 경로를 바꾼다
location /api/ {
proxy_pass http://backend; # URI 없음 → 원래 경로를 그대로 붙인다
# /api/orders → http://backend/api/orders
}
location /api/ {
proxy_pass http://backend/; # URI 있음(/) → location 부분을 '대체' 한다
# /api/orders → http://backend/orders
}
- proxy_pass 에 경로가 있으면 — location 접두사를 그 경로로 바꾼다
- proxy_pass 에 경로가 없으면 — 요청 URI 를 그대로 넘긴다
"백엔드에서 404 가 난다" 의 절반이 이 슬래시다 로그에서 업스트림이 실제로 받은 경로를 확인하면 바로 보인다
6. upstream — 뒤에 여러 대를 둔다
upstream backend {
least_conn; # 분배 방식
server 10.0.1.10:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.12:8080 backup; # 위가 다 죽으면 쓴다
keepalive 32; # 업스트림 연결 재사용
}
분배 방식
(기본) 라운드로빈
least_conn 활성 연결이 가장 적은 곳으로 — 요청 처리 시간이 들쭉날쭉할 때 유리
ip_hash 클라이언트 IP 해시 — 같은 사용자를 같은 서버로
hash $key 임의 키 기준
max_fails / fail_timeout fail_timeout 안에 max_fails 번 실패하면, 그 시간 동안 그 서버를 뺀다
-
죽은 서버로 계속 보내지 않는 최소한의 장치다 ※ 능동 헬스체크(주기적으로 찔러 보는 것)는 상용판 기능이다.
- 오픈소스판은 '실제 요청이 실패하는 것' 을 보고 판단한다
keepalive 를 빠뜨리면 생기는 일
upstream backend {
server 10.0.1.10:8080;
keepalive 32;
}
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1; # ← keepalive 는 1.1 이 필요하다
proxy_set_header Connection ""; # ← 기본 'close' 를 지운다
}
이 세 줄이 없으면 nginx 는 요청마다 업스트림에 새 TCP 연결을 만든다
- 매번 3-way 핸드셰이크
- 업스트림에 TIME_WAIT 소켓이 쌓인다
- 트래픽이 오르면 포트 고갈로 이어진다
keepalive 만 쓰고 아래 두 줄을 빠뜨리는 것이 가장 흔한 미완성 설정이다
7. 자주 쓰는 실전 블록
정적 자산은 길게 캐시하고, HTML 은 캐시하지 않는다
location ~* \.(?:css|js|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location = /index.html {
add_header Cache-Control "no-cache"; # 재검증은 하되 매번 받지는 않는다
}
-
빌드 산출물에 해시가 붙는다면(app.9f2c1.js) 내용이 바뀌면 이름이 바뀐다
-
그 파일은 영원히 캐시해도 안전하다 (immutable)
-
대신 그것을 가리키는 HTML 은 캐시하면 안 된다
-
"배포했는데 옛 화면이 보인다" 의 대부분이 이 조합이 뒤집힌 경우다
압축
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1024; # 너무 작은 응답은 압축이 손해다
gzip_comp_level 5; # 6 이상은 CPU 대비 이득이 급감한다
gzip_vary on; # Vary: Accept-Encoding — 캐시가 섞이지 않게
주의
- 이미 압축된 것(jpg·png·woff2·zip)은 다시 압축하지 않는다 — 오히려 커진다
- gzip_vary 를 빼면 중간 캐시가 압축본을 비압축 클라이언트에게 줄 수 있다
- 정적 파일은 gzip_static 으로 미리 압축해 두면 요청마다 CPU 를 안 쓴다
업로드 크기
client_max_body_size 20m; # 기본 1m — 넘으면 413 Payload Too Large
"파일 업로드가 413 으로 실패한다" → 대개 여기다 그리고 계층이 여럿이면 '전부' 올려야 한다
- CDN → LB → nginx → WAS
- 하나만 낮아도 거기서 막힌다
8. 설정을 확인하는 방법
nginx -t # 문법 검사
nginx -T # include 를 전부 펼친 '최종' 설정을 출력
-T 가 실무에서 훨씬 유용하다
- include 로 흩어진 파일이 실제로 어떻게 합쳐졌는지 보여 준다
- "어디서 이 지시자가 들어왔지" 를 여기서 찾는다
- 배포 전후 diff 를 뜨면 변경이 정확히 무엇인지 드러난다
한눈에 정리
- server 선택 — listen → server_name(정확 > 앞 와일드카드 > 뒤 > 정규식) → default_server
- HTTPS 는 SNI 로 인증서를 먼저 고른다
- location — '=' 즉시 확정 → 가장 긴 접두사 기억 → '^~' 면 정규식 생략
- 정규식은 '적힌 순서' 로 → 없으면 기억해 둔 접두사
- 정규식이 접두사를 이긴다는 점이 사고의 근원
- root/alias — root 는 붙이고 alias 는 대체한다. 되도록 root 를 쓴다
- proxy_pass — 끝에 경로가 있으면 location 을 대체, 없으면 URI 를 그대로
- upstream — least_conn · max_fails/fail_timeout
- keepalive 는 proxy_http_version 1.1 + Connection "" 과 세트
- 캐시 — 해시 붙은 자산은 immutable, HTML 은 no-cache
- 확인 — nginx -t 는 문법, nginx -T 는 펼쳐진 최종 설정
출처 — nginx docs:
ngx_http_core_module(server_name · location · root · alias · client_max_body_size) ·ngx_http_proxy_module(proxy_pass · proxy_set_header) ·ngx_http_upstream_module(keepalive · max_fails) ·ngx_http_gzip_module· "Server names" 문서