Nginx 실전 운영 — 단순 리버스 프록시를 넘어

처음 Nginx를 쓸 때는 proxy_pass http://localhost:8080; 한 줄 넣고 끝냈다.

동작은 했다. 그런데 트래픽이 늘어나면서 SSE 연결이 60초마다 끊기고, 배포할 때마다 요청이 튀고, 특정 요청이 이상하게 느렸다. Nginx를 제대로 이해하지 못한 대가였다.

처음부터 파고들자.


Apache가 있는데 왜 Nginx를 쓰나?

Apache의 전통적인 방식(mpm_prefork, mpm_worker)은 요청마다 프로세스 또는 스레드를 하나 할당한다.

동시 접속 1,000개면 스레드 1,000개. 스레드당 수 MB 메모리가 필요하고, 컨텍스트 스위칭 비용이 누적된다. 동시 접속이 늘어날수록 메모리와 CPU가 선형으로 증가한다.

이게 C10K Problem이다 — 1만 개 동시 연결을 처리하려면 어떻게 해야 하는가.

Apache MPM Prefork 방식으로 10,000 동시 연결을 처리하면 10,000개 프로세스가 필요하다. 메모리가 수 GB 날아간다.

Nginx의 해법 — 이벤트 기반 비동기 모델:

[Master Process] — 설정 읽기, Worker 프로세스 관리, 시그널 처리

    ├── [Worker Process 1] — epoll 이벤트 루프
    │       ├── 연결 1 (클라이언트 → 데이터 읽기 대기)
    │       ├── 연결 2 (Spring Boot → 응답 수신 중)
    │       ├── 연결 3 (클라이언트 → 쓰기 진행 중)
    │       └── 연결 N ... (수천 개)

    └── [Worker Process 2] — epoll 이벤트 루프
            └── ...

Worker 프로세스 하나가 이벤트 루프로 수천 개의 연결을 처리한다. 네트워크 읽기/쓰기가 완료되면 OS 커널이 이벤트를 발생시키고, 그때 해당 연결을 처리한다. 기다리는 동안 다른 연결을 처리한다. 스레드를 추가로 생성하지 않는다.

이 차이가 고동시성 상황에서 극적인 성능 차이를 만든다. 같은 서버에서 Apache 대비 10배 이상 많은 동시 연결을 처리하는 경우도 있다.


Worker 프로세스가 뭔가? 왜 여러 개인가?

worker_processes auto;

auto는 CPU 코어 수를 자동으로 감지해서 그 수만큼 Worker 프로세스를 생성한다. 4코어 CPU면 Worker 4개.

왜 코어 수에 맞추나?

Worker 1개만 있으면 어떤 문제가 생기나? CPU 코어가 4개인데 Worker가 1개라면, 나머지 3개 코어는 놀고 있다. 연결이 몰리면 그 하나의 Worker가 병목이 된다.

반대로 너무 많으면? Worker가 8개인데 코어가 4개라면, 컨텍스트 스위칭이 늘어난다. 커널이 Worker 간에 CPU를 계속 교체하는 오버헤드가 생긴다. 코어 수 = Worker 수가 최적이다.

Nginx Worker는 CPU 바운드 작업이 아니라 I/O 바운드 작업을 한다. 네트워크에서 읽고, upstream에 보내고, 응답을 받아서 다시 보낸다. CPU는 이 과정에서 많이 쓰이지 않는다. 핵심은 I/O 이벤트를 얼마나 효율적으로 처리하느냐다.

worker_processes auto;
worker_cpu_affinity auto;  # Worker를 특정 CPU 코어에 고정

worker_cpu_affinity auto는 CPU 캐시 지역성을 최대화한다. NUMA 아키텍처 서버에서 메모리 접근 레이턴시를 줄이는 효과가 있다.


worker_connectionsulimit -n이 왜 같이 봐야 하나?

events {
    worker_connections 1024;
}

Worker 프로세스 하나가 동시에 열 수 있는 최대 연결 수다.

여기서 중요한 점 — 클라이언트 연결뿐 아니라 upstream(Spring Boot) 연결도 포함된다.

Nginx가 클라이언트 요청 하나를 처리할 때:

  1. 클라이언트 → Nginx: 연결 1개
  2. Nginx → Spring Boot: 연결 1개

실제 처리 가능한 동시 클라이언트 수 = worker_connections / 2. worker_connections 1024면 Worker 하나가 약 512개 동시 요청을 처리할 수 있다.

이 숫자가 부족하면? 새 연결 요청이 OS Accept 큐에 쌓인다. 큐도 가득 차면 클라이언트가 Connection refused 또는 타임아웃을 받는다.

ulimit -n과 반드시 맞춰야 한다:

# 현재 파일 디스크립터 한도 확인
ulimit -n
# 기본값: 1024 (매우 부족할 수 있음)

Linux는 모든 연결을 파일 디스크립터(fd)로 관리한다. worker_connections를 4096으로 올렸는데 ulimit -n이 1024면 1024개 이상 연결이 불가능하다. too many open files 에러가 발생한다.

# nginx.conf에서 설정
worker_rlimit_nofile 65536;  # Worker 프로세스의 파일 디스크립터 한도

events {
    worker_connections 4096;  # worker_rlimit_nofile의 절반 이하로
}

두 값을 항상 함께 확인해야 한다. 하나만 올리면 나머지가 병목이 된다.


epoll이 왜 빠른가? select/poll과 비교하면 어떻게 다른가?

I/O 멀티플렉싱 메커니즘의 진화다.

select (POSIX 표준):

감시할 fd 목록을 매번 커널에 전달
→ 커널이 모든 fd를 순회
→ 준비된 fd 반환

복잡도: O(n) — fd 1만 개면 1만 번 순회
fd 수 제한: FD_SETSIZE (보통 1024)

연결이 1,000개인데 실제로 데이터가 있는 건 10개뿐이라면, 990개는 확인만 하고 버려진다. 낭비다.

poll (select 개선):

  • fd 수 제한 없음
  • 여전히 O(n) — 준비된 fd가 하나여도 전체 목록을 순회

epoll (Linux 2.5.44+, Nginx가 사용):

epoll_create: 커널에 epoll 인스턴스 생성 (한 번만)
epoll_ctl: 관심 있는 fd를 등록/수정/삭제
epoll_wait: 이벤트가 발생한 fd만 반환

복잡도: O(1) — fd 수와 무관

핵심은 커널이 이벤트를 추적한다는 것이다. 10,000개 연결이 있어도 실제로 데이터가 도착한 연결만 알려준다. 나머지 9,999개는 건드리지 않는다.

동시 연결이 1,000개일 때와 10,000개일 때 epoll의 처리 시간은 거의 같다. select/poll은 10배 느려진다.

events {
    use epoll;        # Linux에서 명시적 지정 (auto로 해도 epoll 선택됨)
    multi_accept on;  # 이벤트 발생 시 가능한 한 많은 연결을 한 번에 Accept
}

multi_accept on은 Accept 큐에 쌓인 연결을 한 번에 모두 받아들인다. off(기본값)이면 이벤트당 연결 하나씩. 갑작스러운 연결 폭발에 더 빠르게 대응한다.


Spring Boot 앞에 Nginx를 두면 keepalive를 어떻게 설정해야 하나?

keepalive를 설정하지 않으면 Nginx는 요청마다 Spring Boot와 새로운 TCP 연결을 맺는다.

요청마다 TCP 3-way handshake + 응답 + FIN. 이 오버헤드가 트래픽이 많아지면 쌓인다. 초당 수백 req/s 트래픽이라면 초당 수백 번의 TCP 연결/종료가 발생한다.

upstream spring_boot {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081;

    keepalive 100;           # Worker당 유지할 upstream keepalive 연결 수
    keepalive_requests 1000; # 연결 하나가 처리할 최대 요청 수 (이후 재생성)
    keepalive_timeout 75s;   # 유휴 keepalive 연결 유지 시간
}

server {
    location / {
        proxy_pass http://spring_boot;
        proxy_http_version 1.1;        # keepalive는 HTTP/1.1에서만 가능
        proxy_set_header Connection ""; # HTTP/1.0의 기본 Connection: close 제거
        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_http_version 1.1proxy_set_header Connection ""은 반드시 세트로 설정해야 한다. 이 두 줄 없이 keepalive 100을 설정해도 keepalive가 동작하지 않는다. Nginx가 upstream에 보내는 기본 버전이 HTTP/1.0이고, HTTP/1.0의 기본이 Connection: close다.

Spring Boot Tomcat 설정과 맞춰야 한다:

# application.yml
server:
  tomcat:
    max-connections: 8192
    threads:
      max: 200
      min-spare: 20
    keep-alive-timeout: 75000  # ms — Nginx keepalive_timeout과 맞춤
    max-keep-alive-requests: 1000  # Nginx keepalive_requests와 맞춤

Nginx keepalive_timeout 75s와 Tomcat keep-alive-timeout 75000ms를 맞추는 것이 중요하다.

Nginx 타임아웃이 더 짧으면 Nginx가 먼저 연결을 끊으면서 Tomcat이 “클라이언트가 연결을 끊었다”는 오류를 로그에 남긴다. Tomcat 쪽이 더 짧으면 Nginx가 이미 닫힌 연결에 요청을 보내다가 502 에러가 발생한다.


SSL을 Nginx에서 끊으면 Spring Boot는 뭘 신경 안 써도 되나? 그런데 X-Forwarded-For가 왜 중요한가?

Nginx에서 TLS를 종료하면 Spring Boot는 HTTP만 받으면 된다. SSL 설정, 인증서 관리가 Spring Boot에서 사라진다.

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    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;
    ssl_prefer_server_ciphers off;  # TLS 1.3에서는 off 권장

    # Worker 간 TLS 세션 공유 (10MB ≈ 40,000 세션)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # OCSP Stapling: CA 서버 별도 조회 없이 인증서 유효성 첨부
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

    # HSTS: 1년간 HTTPS 강제
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        proxy_pass http://spring_boot;  # Spring Boot로는 HTTP
        proxy_set_header X-Forwarded-Proto $scheme;  # "https"를 전달
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

그런데 X-Forwarded-For가 왜 중요한가?

Spring Boot는 HTTP로 요청을 받는다. request.getRemoteAddr()를 호출하면 Nginx의 내부 IP가 반환된다. 클라이언트 IP가 아니다.

Rate Limiting을 IP 기반으로 하면 — 모든 요청이 같은 Nginx IP로 보인다. Rate Limiting이 동작하지 않는다.

Spring Security의 HTTPS 리다이렉트가 안 먹힌다. request.getScheme()http를 반환하기 때문이다.

해결:

# application.yml
server:
  forward-headers-strategy: framework

이 설정을 하면 Spring이 X-Forwarded-Proto: https를 읽어서 request.getScheme()https를 반환한다. X-Forwarded-For도 올바르게 처리한다.


SSE 서비스를 Nginx 뒤에 두면 이벤트가 왜 안 오나?

가장 많이 겪는 Nginx SSE 문제다. 원인이 두 가지다.

원인 1 — 응답 버퍼링:

Nginx 기본 설정은 upstream 응답을 버퍼에 쌓은 뒤 클라이언트에 보낸다. 응답이 일정 크기가 되거나 연결이 닫혀야 전달한다.

SSE는 서버가 이벤트를 발생시킬 때마다 즉시 전달해야 한다. 버퍼에 가로막히면 실시간성이 깨진다. 클라이언트 입장에서 이벤트가 아예 안 오거나, 뭉텅이로 한 번에 온다.

원인 2 — 타임아웃:

proxy_read_timeout 기본값은 60초다. SSE 연결은 이벤트가 없어도 수 분~수 시간 연결을 유지한다. 60초 동안 데이터가 없으면 Nginx가 연결을 끊어버린다.

location /api/sse {
    proxy_pass http://spring_boot;

    # 버퍼링 비활성화: 즉시 클라이언트에 전달
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header X-Accel-Buffering no;

    # SSE 연결 타임아웃 (60초 → 24시간)
    proxy_read_timeout 86400s;

    # HTTP/1.1 필수 (청크 전송 인코딩 지원)
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    add_header Content-Type "text/event-stream; charset=utf-8";
    add_header Cache-Control "no-cache";
    add_header X-Accel-Buffering "no";
}

Spring Boot 쪽도 맞춰줘야 한다:

@GetMapping(value = "/api/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
    // timeout을 Nginx proxy_read_timeout보다 짧게
    SseEmitter emitter = new SseEmitter(3_600_000L); // 1시간

    // Heartbeat 전송 (30초마다) — 중간 프록시의 타임아웃 방지
    ScheduledFuture<?> heartbeat = scheduler.scheduleAtFixedRate(() -> {
        try {
            emitter.send(SseEmitter.event().comment("heartbeat"));
        } catch (IOException e) {
            emitter.completeWithError(e);
        }
    }, 0, 30, TimeUnit.SECONDS);

    emitter.onCompletion(() -> heartbeat.cancel(true));
    emitter.onTimeout(() -> heartbeat.cancel(true));

    return emitter;
}

comment("heartbeat")는 SSE 스펙에서 :heartbeat 형태의 코멘트로 전송된다. 클라이언트는 이벤트로 인식하지 않지만, 데이터가 흐르므로 Nginx 타임아웃이 리셋된다.

ALB도 함께 확인하라:

Nginx 뒤에 ALB가 있다면 ALB의 Idle Timeout(기본 60초)도 늘려야 한다. ALB 콘솔에서 Attributes → Idle timeout을 3600초로 변경한다.


Nginx에서 Rate Limiting을 설정하면 WAF랑 역할이 겹치지 않나?

겹치는 것처럼 보이지만 레이어와 목적이 다르다.

AWS WAF Rate LimitingNginx Rate Limiting
위치네트워크 엣지 (CloudFront/ALB 앞)Spring Boot 직전
집계 단위IP, Forwarded-IP, Header 등IP, URI 조합 등
정밀도서비스 전체 단위URI 경로별 세밀한 제어
응답즉시 Block (요청이 리전에 오기 전)429 응답 (Nginx까지 온 후)
적합한 용도DDoS, 대규모 스캐닝경로별 API 제한, 로그인 보호

두 가지를 함께 쓰는 게 맞다. WAF가 대규모 공격을 먼저 걸러내고, Nginx가 세밀한 경로별 제한을 한다.

http {
    # IP별 요청 속도 제한 존
    limit_req_zone $binary_remote_addr zone=api_zone:10m rate=100r/s;
    limit_req_zone $binary_remote_addr zone=login_zone:1m  rate=5r/m;

    # IP별 동시 연결 수 제한 존
    limit_conn_zone $binary_remote_addr zone=conn_zone:10m;
}

server {
    # 일반 API
    location /api/ {
        limit_req zone=api_zone burst=200 nodelay;
        limit_conn conn_zone 20;
        limit_req_status 429;
        limit_conn_status 429;
        proxy_pass http://spring_boot;
    }

    # 로그인 — 더 엄격하게
    location /api/auth/login {
        limit_req zone=login_zone burst=10 nodelay;
        limit_req_status 429;
        proxy_pass http://spring_boot;
    }
}

burst=200 nodelay의 의미: 순간적으로 rate를 초과해도 burst 범위(200개)까지는 즉시 처리한다. burst를 초과하면 즉시 429를 반환한다.

$binary_remote_addr는 IP를 바이너리로 저장한다. IPv4는 4바이트, IPv6는 16바이트. 문자열($remote_addr) 대비 메모리 효율이 좋다.


nginx -s reload가 무중단으로 동작하는 원리가 뭔가?

nginx -s reload

설정이 바뀐다. 진행 중인 요청이 끊어지지 않는다.

처음엔 “설정 파일을 바꾸면 기존 연결이 끊기는 거 아닌가?”라는 의문이 생긴다. 어떻게 가능한가?

동작 원리:

1. nginx -s reload 실행
   → Master Process에 SIGHUP 시그널 전송

2. Master Process:
   a. 새 설정 파일 읽기 및 검증 (문법 오류 있으면 여기서 실패)
   b. 새 설정으로 새 Worker Process 생성
   c. 기존 Worker Process에 SIGQUIT(우아한 종료) 신호 전송

3. 기존 Worker Process (SIGQUIT 받은 후):
   → 새 연결 수락 중단
   → 현재 처리 중인 요청은 완료될 때까지 계속 처리
   → 모든 요청 완료 후 종료

4. 새 Worker Process:
   → 즉시 새 연결 수락 시작
   → 새 설정 적용

새 Worker가 새 연결을 받고, 기존 Worker는 진행 중인 요청을 마무리한다. 두 세대가 잠깐 공존한다.

# 반드시 설정 검사 후 reload
nginx -t && nginx -s reload

nginx -t 없이 바로 reload하면 설정 오류가 있어도 일단 시도하다가 실패한다. 기존 Worker는 그대로 계속 돌아가지만, 새 설정이 적용됐다고 착각하기 쉽다. 배포 스크립트에 항상 nginx -t && nginx -s reload 패턴을 써라.


Nginx 로그에서 응답시간을 어떻게 분석하나?

기본 로그 포맷은 사람이 읽기엔 괜찮지만 자동 파싱이 번거롭다. JSON 포맷으로 바꾸면 CloudWatch Logs Insights, Loki에 바로 넣을 수 있다.

http {
    log_format json_main escape=json
        '{'
            '"time":"$time_iso8601",'
            '"remote_addr":"$remote_addr",'
            '"x_forwarded_for":"$http_x_forwarded_for",'
            '"method":"$request_method",'
            '"uri":"$uri",'
            '"query":"$query_string",'
            '"status":$status,'
            '"body_bytes_sent":$body_bytes_sent,'
            '"request_time":$request_time,'
            '"upstream_response_time":"$upstream_response_time",'
            '"upstream_addr":"$upstream_addr",'
            '"upstream_status":"$upstream_status",'
            '"request_id":"$request_id"'
        '}';

    access_log /var/log/nginx/access.log json_main buffer=32k flush=5s;
}

핵심 지표 — request_time vs upstream_response_time의 차이가 뭘 의미하는지:

request_time:
  클라이언트가 첫 바이트 전송 시점
  ~ Nginx가 마지막 바이트 전송 시점
  → 클라이언트 네트워크 속도까지 포함
  → 느린 클라이언트, 대용량 업로드에서 크게 나올 수 있음

upstream_response_time:
  Nginx가 upstream에 요청 전송 완료 시점
  ~ 응답 마지막 바이트 수신 시점
  → 순수 Spring Boot 처리 시간에 가장 가까움

분석 공식:

request_time 크고, upstream_response_time 작음
→ 클라이언트 네트워크가 느리다
  (업로드 속도 문제 또는 모바일 환경)

upstream_response_time 크다
→ Spring Boot 처리가 느리다
  (DB 쿼리 지연, 외부 API 호출 등)

request_time 크고, upstream_addr가 "-" (빈값)
→ upstream에 연결도 못 했다
  (Spring Boot 다운, 연결 거부)

upstream_status가 502, 503, 504
→ upstream 에러. Spring Boot 상태 확인 필요

요청 추적 — Nginx request_id를 Spring Boot까지:

proxy_set_header X-Request-ID $request_id;
// Spring Boot - MDC에 request_id 등록
@Component
public class RequestIdFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String requestId = ((HttpServletRequest) request).getHeader("X-Request-ID");
        if (requestId != null) MDC.put("requestId", requestId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove("requestId");
        }
    }
}

이제 Nginx 로그의 request_id와 Spring Boot 로그의 [requestId]가 같은 값이다. 하나의 느린 요청을 Nginx → Spring Boot → DB까지 ID 하나로 추적할 수 있다.


최적화

gzip 압축:

http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;       # 1(빠름)~9(높은 압축). 6이 균형점
    gzip_min_length 1024;    # 1KB 미만은 압축 안 함 (오히려 커질 수 있음)
    gzip_types
        text/plain
        text/css
        application/json
        application/javascript
        application/xml
        image/svg+xml;
    # JPEG, PNG, WebP, 동영상은 이미 압축됨 — gzip 제외
}

JSON API 응답은 gzip으로 70~80% 줄어든다. gzip_vary on을 빠뜨리면 Vary: Accept-Encoding 헤더가 없어서 CDN이 gzip 버전과 비압축 버전을 같은 캐시로 혼동한다.

자주 요청되는 공개 API 캐시:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m;

location /api/public/ {
    proxy_cache api_cache;
    proxy_cache_valid 200 5m;
    proxy_cache_key "$scheme$request_method$host$request_uri";
    proxy_cache_use_stale error timeout updating;
    add_header X-Cache-Status $upstream_cache_status;
    proxy_pass http://spring_boot;
}

proxy_cache_use_stale error timeout updating은 upstream이 오류를 반환하거나 업데이트 중일 때 오래된 캐시를 반환한다. 배포 중에도 사용자는 캐시된 응답을 받는다.


튜닝

연결 타임아웃 조정:

# 클라이언트 측
client_header_timeout 15s;
client_body_timeout 30s;
send_timeout 30s;
keepalive_timeout 75s;
keepalive_requests 1000;

# upstream 측
proxy_connect_timeout 5s;    # Spring Boot 다운 시 빠른 감지 (기본 60s보다 짧게)
proxy_send_timeout 60s;
proxy_read_timeout 60s;      # SSE라면 86400s

proxy_connect_timeout을 5초로 짧게 잡는 게 중요하다. Spring Boot가 다운됐을 때 Nginx가 60초(기본값) 동안 기다리면 클라이언트도 60초를 기다린다. 5초로 줄이면 빠른 장애 감지와 upstream failover가 가능하다.

Passive Health Check:

upstream spring_boot {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8081 max_fails=3 fail_timeout=30s backup;
}

30초 안에 3번 실패하면 30초간 해당 서버를 제외한다. backup 서버는 primary가 모두 실패할 때만 활성화된다.


운영상 주의사항

nginx -t 없이 reload하지 말 것

배포 스크립트에서 실패를 감지하지 못하면 새 설정이 적용됐다고 착각한다. nginx -t && nginx -s reload 패턴을 항상 쓴다.

로그 로테이션 설정

# /etc/logrotate.d/nginx
/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    sharedscripts
    postrotate
        nginx -s reopen  # 새 로그 파일로 전환 (이게 없으면 old fd에 계속 씀)
    endscript
}

logrotate가 로그 파일을 rotate하면 Nginx는 여전히 old fd를 열고 거기에 쓴다. nginx -s reopen으로 새 파일을 열도록 알려줘야 한다.

디스크 버퍼 설정

access_log /var/log/nginx/access.log json_main buffer=32k flush=5s;

32KB 버퍼에 쌓이거나 5초가 지나면 플러시한다. 요청마다 즉시 파일에 쓰는 기본 설정 대비 디스크 I/O 오버헤드를 줄인다. 단, 서버가 갑자기 죽으면 버퍼의 로그가 유실될 수 있다.

버전 업그레이드 주의

새 버전에서 기본값이 바뀌거나 지시어가 deprecated되는 경우가 있다. 업그레이드 전에 nginx -v와 변경 로그를 확인하고, 스테이징에서 먼저 검증한다.