/인프라/nginx 502 Bad Gateway: Diagnostic Chart and Copy-Paste Commands to Fix It in 5 Minutes
Infrastructure웹서버트러블슈팅서버에러해결

nginx 502 Bad Gateway: Diagnostic Chart and Copy-Paste Commands to Fix It in 5 Minutes

Don't blindly restart nginx for a 502 Bad Gateway. Match error.log keywords against a cause-by-cause diagnostic chart, then use copy-paste commands for php-fpm, gunicorn, and uvicorn upstreams to pinpoint the issue—including SELinux and soc

nginx 502 Bad Gateway: Diagnostic Chart and Copy-Paste Commands to Fix It in 5 Minutes

nginx 502 Bad Gateway: Diagnostic Chart and Copy-Paste Commands to Fix It in 5 Minutes

Why a 502 When the Frontend Looks Fine?

An alert fires and the browser shows 502 Bad Gateway. The first thing your hands reach for is systemctl restart nginx—but pause. A 502 is far more likely not nginx's fault.

The critical difference between a 502 and a 4xx is that the request itself is valid. A 4xx is a client (request) problem; a 502 means nginx accepted the client's request and tried to pass it to an upstream (php-fpm, gunicorn, uvicorn, node, etc.) but got an invalid response from the upstream, or the connection itself dropped. nginx is just a healthy middleman—the real culprit is almost always the upstream.

That's why blindly restarting nginx is risky. If the cause is an upstream OOM or timeout, restarting nginx will never fix it—and you'll only wipe connection state and log context, making root-cause analysis harder. The sequence is clear: check error.log keywords → match the diagnostic chart → verify with copy-paste commands → fix the right place.

502 Diagnostic Chart (8 Common Cases)

First open /var/log/nginx/error.log, grab the keyword, and match it against the table below.

Causeerror.log keywordCheck commandOne-line fix
Upstream downconnect() failed (111: Connection refused)sudo systemctl status php8.2-fpmStart/restart the upstream process
Connect timeoutupstream timed out ... while connectingcurl -v http://127.0.0.1:8000/healthCheck firewall/network and proxy_connect_timeout
Read timeoutupstream timed out ... while reading response headerjournalctl -u gunicorn -n 50Align proxy_read_timeout with the app --timeout
Response buffer too smallupstream sent too big headernginx -T | grep bufferIncrease proxy_buffer_size/fastcgi_buffer_size
Unix socket permissionsconnect() ... Permission deniedls -l /run/php/php-fpm.sockFix listen.owner/group/mode
SELinux blockingconnect() failed (13: Permission denied)sudo getsebool httpd_can_network_connectsetsebool -P httpd_can_network_connect 1
Health failure / no nodesno live upstreams while connectingss -tlnp | grep 9000Restore upstream nodes; proxy_next_upstream
Premature connection closeupstream prematurely closed connectionjournalctl -u uvicorn -n 100Check gateway timeout/OOM/keepalive

Narrow the Cause by Reading error.log Keywords

If the table isn't enough, copy-paste the commands below in order to narrow it down. Each command notes what it checks.

Bash
# 1) 업스트림 프로세스가 살아있나? (Active: running 확인)
sudo systemctl status php8.2-fpm

# 2) 업스트림이 기대한 포트에서 실제로 리스닝 중인가?
ss -tlnp | grep 9000

# 3) nginx를 건너뛰고 업스트림을 직접 호출 — 여기서 200이면 nginx 설정 문제
curl -v http://127.0.0.1:8000/health

# 4) 유닉스 소켓 방식이면 소켓으로 직접 호출
curl --unix-socket /run/php/php-fpm.sock http://localhost/

# 5) nginx 설정 문법 검사 후 무중단 reload
sudo nginx -t && sudo systemctl reload nginx

# 6) gunicorn/uvicorn 게이트웨이 로그에서 OOM·timeout 흔적 찾기
journalctl -u gunicorn -n 50

The key is step 3. If you skip nginx and hit the upstream directly and get a healthy response, the problem is in nginx config (port, socket, timeout). If the direct call also fails, the culprit is 100% the upstream. That one line cuts the blame in half.

Copy-Paste Fix Recipes by Cause

proxy_pass (gunicorn/uvicorn/node) Timeout and Buffer Tuning

Nginx
# Before — 기본값이라 느린 응답·큰 헤더에서 502
location / {
    proxy_pass http://127.0.0.1:8000;
}

# After — 타임아웃과 버퍼를 현실에 맞게
location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_connect_timeout 5s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
    proxy_buffer_size 32k;
    proxy_buffers 8 16k;
    proxy_busy_buffers_size 64k;
}

If you see upstream sent too big header, increasing proxy_buffer_size is the right fix. Large cookies or JWT headers overflowing the default buffer is common.

fastcgi_pass (php-fpm) Socket vs Port

Nginx
# 유닉스 소켓 방식 — 같은 호스트면 빠르지만 권한 함정 주의
fastcgi_pass unix:/run/php/php-fpm.sock;

# TCP 포트 방식 — 컨테이너/원격 분리 시 권장
fastcgi_pass 127.0.0.1:9000;

# 공통: 버퍼/타임아웃도 fastcgi_ 접두어로 따로 설정
fastcgi_read_timeout 60s;
fastcgi_buffer_size 32k;
fastcgi_buffers 8 16k;

A mistyped socket path or port mismatch is a classic cause of connect() failed. Always compare the actual listening address from ss -tlnp | grep 9000 against your config.

SELinux and Socket Permission Traps

On RHEL/Rocky/AlmaLinux, SELinux defaults to enforcing, so nginx is blocked when it tries to open a network connection to the upstream. The symptom is connect() failed (13: Permission denied)—if the port is open but you get permission denied, suspect SELinux.

Bash
# 현재 정책 상태 확인 (off면 이게 범인)
sudo getsebool httpd_can_network_connect

# 영구 허용 (-P 옵션이 재부팅 후에도 유지)
sudo setsebool -P httpd_can_network_connect 1

If you use a Unix socket, the php-fpm pool owner/group/permissions must match the nginx runtime user. /etc/php/8.2/fpm/pool.d/www.conf:

INI
listen = /run/php/php-fpm.sock
listen.owner = www-data   ; nginx 실행 유저 (RHEL 계열은 nginx)
listen.group = www-data
listen.mode = 0660

Permission denied is common when sharing a socket via a volume between containers—if UIDs/GIDs differ across containers, you can't put them in the same group. In that case, TCP (127.0.0.1:9000) is simply less painful.

upstream prematurely closed connection — Tweaking nginx Alone Won't Fix It

This is the most confusing case. upstream prematurely closed connection while reading response header means the upstream closed the connection first while nginx was waiting for a response. That's why bumping nginx timeouts alone doesn't fix it. The real cause is on the gateway side.

  • gunicorn/uvicorn's own timeout: the worker is killed because it hit gunicorn's default --timeout 30
  • Worker OOM: the worker dies from memory exhaustion and the connection drops (look for Worker ... was sent SIGKILL! in journalctl -u gunicorn)
  • keepalive mismatch: nginx tries to reuse the connection but the upstream closes keepalive

A real-world case: we raised nginx proxy_read_timeout to 120s, but still got 502s around 60s. Turns out gunicorn --timeout was set to 60s, so gunicorn killed the worker first no matter how long nginx was willing to wait. Without the habit of looking at both timeouts together, you'll wander this trap for a long time.

Recurrence Prevention Checklist

Nginx
upstream app {
    server 127.0.0.1:8000;
    keepalive 32;            # 연결 재사용으로 부하·지연 감소
}

server {
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";   # keepalive 사용 시 필수
        proxy_read_timeout 90s;           # 아래 앱 timeout보다 살짝 크게
        proxy_next_upstream error timeout http_502;
    }
}
  • Align timeouts: if gunicorn --timeout 60, set nginx proxy_read_timeout higher (e.g. 90s) so the gateway doesn't die first.
  • Health-check endpoint: expose /health for the load balancer/monitoring to poll, and use proxy_next_upstream to skip dead nodes automatically.
  • Habit of graceful reload: apply config changes with sudo nginx -t && sudo systemctl reload nginx. Use reload instead of restart for zero-downtime.

When 502 shows up again, just remember this sequence: check error.log keywords → curl the upstream directly → match the diagnostic chart → fix the one right place → reload. Recovery speed is a world apart from the days of restarting on a guess.

References: Official Docs

The primary source for the behavior, settings, and errors covered in this post is the official documentation below. Check there for version-specific options and exact behavior.

FAQ

Q. Restarting nginx doesn't clear the 502. Why? A. 502 is usually an upstream (php-fpm/gunicorn/uvicorn) problem, so restarting nginx won't fix it. Hit the upstream directly with curl -v http://127.0.0.1:포트/health. If that fails too, the culprit is 100% the upstream.

Q. How do I catch upstream prematurely closed connection? A. Don't blame the nginx timeout—suspect the gateway's own timeout (gunicorn --timeout), worker OOM, or a keepalive mismatch. In journalctl -u gunicorn -n 100, look for SIGKILL or timeout traces, then set the app timeout smaller than nginx proxy_read_timeout.

Q. The port is open but I get connect() failed (13: Permission denied). A. SELinux on RHEL/Rocky is the likely blocker. Check with sudo getsebool httpd_can_network_connect, then allow it with sudo setsebool -P httpd_can_network_connect 1. If you use a Unix socket, listen.owner/group/mode must also match the nginx user.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·

Comments

Be the first to comment.