
nginx 502 Bad Gateway 해결 — 원인별 체크리스트와 로그 보는 법
nginx 502 Bad Gateway는 거의 항상 nginx가 아니라 업스트림(백엔드) 쪽 문제다. error.log부터 켜 두고 앱 다운·proxy_pass 주소·타임아웃·버퍼·SELinux·과부하 6가지 원인을 순서대로 짚는 실전 해결 체크리스트.
배포 버튼을 누르고 브라우저를 새로고침했는데 하얀 화면에 "502 Bad Gateway" 한 줄만 떠 있다. 서버에 SSH로 들어가 보면 nginx는 멀쩡히 돌고 있다. 로그를 켜 둔 터미널은 아무 일도 없었다는 듯 조용하다. 배포하는 사람이라면 한 번쯤 겪는 장면이고, 대부분 여기서 5분에서 30분을 날린다.
502가 유독 헷갈리는 이유는 하나다. 에러 화면을 뱉은 건 nginx인데, 정작 사고를 친 건 nginx가 아니기 때문이다.
502는 "내 잘못이 아니라 뒤쪽 잘못"이라는 뜻
nginx는 대부분 리버스 프록시로 쓴다. 브라우저 요청을 받아 뒤에 있는 실제 애플리케이션(Node, Spring, Django, PHP-FPM 등)으로 넘기고, 그 응답을 다시 브라우저로 전달하는 중개자다. 이 "뒤에 있는 녀석"을 nginx 용어로 업스트림(upstream)이라 부른다.
502 Bad Gateway는 nginx가 업스트림에 요청을 넘겼는데 돌아온 응답이 없거나 말이 안 되는 응답이었다는 신호다. 연결 자체가 거부됐거나, 연결은 됐는데 제한 시간 안에 답이 안 왔거나, 답은 왔는데 nginx가 소화하지 못한 형태였거나. 크게 이 셋 중 하나다.
그래서 502를 만났을 때 nginx 설정 파일만 노려보는 건 보통 헛수고다. 범인은 거의 언제나 업스트림 쪽에 있다. 순서를 지켜 좁혀 나가면 대개 몇 분 안에 잡힌다.

설정 파일 열기 전에 로그부터 본다
가장 많이 하는 실수가 추측부터 하는 것이다. error.log 한 줄이면 끝날 일을 설정 파일을 뜯어고치며 30분을 쓴다. 502를 봤다면 제일 먼저 할 일은 nginx 에러 로그를 실시간으로 켜 두고 요청을 한 번 더 보내는 것이다.
sudo tail -f /var/log/nginx/error.log
로그 경로는 배포판마다 다르다. 우분투·데비안은 /var/log/nginx/error.log, 직접 빌드했거나 도커라면 설정의 error_log 지시어가 가리키는 곳을 본다. 이 상태에서 502가 뜨는 요청을 다시 날리면, 그 순간 찍히는 한 줄이 원인을 거의 다 말해 준다. 아래 키워드로 범인이 갈린다.
connect() failed (111: Connection refused)→ 업스트림이 안 떠 있다 (원인 ①)upstream timed out→ 앱이 느려 타임아웃 (원인 ③)no live upstreams→ 업스트림 전체가 죽었거나 과부하 (원인 ⑥)connect() ... failed (13: Permission denied)→ 권한·SELinux (원인 ⑤)upstream sent too big header→ 응답 헤더가 버퍼보다 큼 (원인 ④)
로그를 먼저 본 사람과 안 본 사람의 해결 속도는 열 배쯤 차이 난다. 아래 체크리스트도 결국 이 로그 한 줄을 해석하는 이야기다.
원인별 해결 체크리스트
① 업스트림(앱)이 죽어 있다 — 가장 흔한 원인
로그에 Connection refused가 보이면 십중팔구 이거다. nginx는 살아 있는데 뒤에 있는 앱이 죽었거나, 아직 안 떴거나, 다른 포트로 떠 있다.
확인은 간단하다. 앱이 정말 그 포트에서 듣고 있는지 직접 본다.
sudo ss -ltnp | grep 8080
curl -v http://127.0.0.1:8080/
앞 명령에 아무것도 안 나오면 앱이 그 포트를 안 열고 있다는 뜻이다. 프로세스 매니저(systemd·pm2·docker)로 앱 상태를 확인하고 다시 띄운다. 유닉스 소켓을 쓰는 구성이라면 소켓 파일 경로(/run/app.sock 등)가 실제로 존재하는지, nginx 설정의 경로와 글자 하나까지 같은지 본다. 배포 스크립트가 앱을 재시작하는 그 몇 초 사이에 502가 뜨는 경우도 흔하다.
② proxy_pass 주소가 틀렸다
앱은 멀쩡히 떠 있는데 nginx가 엉뚱한 곳을 두드리는 경우다. 포트를 바꿨거나, 도커 컴포즈에서 서비스 이름을 바꿨거나, localhost와 127.0.0.1을 혼동한 상황.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
도커 네트워크 안에서는 127.0.0.1이 "nginx 컨테이너 자기 자신"을 가리킨다. 다른 컨테이너를 가리켜야 한다면 localhost가 아니라 컴포즈 서비스 이름(proxy_pass http://backend:8080;)을 써야 한다. 이 함정에 걸리는 사람이 정말 많다. 설정을 고쳤으면 sudo nginx -t로 문법을 검사하고 sudo nginx -s reload로 반영한다. reload는 무중단이라 기존 커넥션을 끊지 않는다.
③ 타임아웃 — 앱이 느려서 nginx가 먼저 포기
nginx의 프록시 타임아웃은 기본값이 전부 60초다. 앱이 무거운 쿼리나 리포트 생성으로 60초를 넘기면, 앱은 아직 일하는 중인데 nginx가 먼저 연결을 끊고 502(상황에 따라 504)를 뱉는다.
location /report {
proxy_pass http://127.0.0.1:8080;
proxy_connect_timeout 75s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
proxy_read_timeout이 응답을 기다리는 시간이라 가장 자주 손대는 값이다. 다만 타임아웃을 늘리는 건 증상 완화지 치료가 아니다. 응답이 5분 걸리는 API가 정상일 리 없다. 느린 쿼리를 잡거나, 오래 걸리는 작업은 비동기 큐로 빼는 게 맞다. 타임아웃 상향은 그 시간을 버는 임시방편으로만 쓴다.
④ 응답 헤더가 버퍼보다 크다
로그에 upstream sent too big header가 보이면 이거다. 앱이 쿠키를 잔뜩 심거나 커스텀 헤더를 많이 붙이면, 응답 헤더가 nginx의 기본 버퍼(플랫폼에 따라 4k 또는 8k)를 넘겨 502가 난다. SSO나 인증 토큰을 헤더로 주고받는 구성에서 잘 터진다.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
proxy_buffer_size는 응답의 첫 부분(헤더)을 담는 버퍼라, 이 값을 키우면 대부분 해결된다.

⑤ 권한 / SELinux — RHEL 계열의 단골
로그에 (13: Permission denied)가 찍혔는데 앱은 분명히 떠 있다면, 리눅스 파일 권한이거나 SELinux다. CentOS·RHEL·Rocky 계열에서 자주 나온다. nginx가 네트워크로 업스트림에 연결하는 것 자체를 SELinux가 막는 경우다.
getsebool httpd_can_network_connect
sudo setsebool -P httpd_can_network_connect 1
-P는 재부팅 후에도 유지하라는 뜻이다. 진짜 원인이 SELinux인지 확신이 안 서면 먼저 감사 로그를 본다.
sudo ausearch -m AVC,USER_AVC -ts recent
여기에 nginx 관련 거부(denied) 기록이 찍히면 SELinux가 확정이다. 유닉스 소켓을 쓰는 PHP-FPM 구성이라면 소켓 파일의 소유자·권한, 그리고 nginx 워커가 도는 유저가 그 소켓에 접근할 수 있는지도 같이 본다.
⑥ 업스트림 과부하 — 피크 때만 뜨는 502
트래픽이 몰려 앱의 워커나 커넥션 풀이 꽉 차면, nginx 입장에선 연결이 거부되거나 지연돼 502가 산발적으로 뜬다. 평소엔 멀쩡하다 피크 때만 뜨는 502가 이 유형이다. 앱의 워커 수와 커넥션 풀을 늘리고, nginx 쪽은 업스트림에 keepalive 커넥션을 두면 매 요청마다 새 연결을 맺는 비용을 줄일 수 있다.
upstream app {
server 127.0.0.1:8080;
keepalive 32;
}
여기서 중요한 건, 과부하 502는 설정이 아니라 용량 문제라는 점이다. 워커를 늘려도 계속 터지면 서버를 키우거나 인스턴스를 늘려야 한다는 신호다.
재발 방지 — 로그 한 줄로 끝내는 습관
502 디버깅의 8할은 "로그를 먼저 봤는가"에서 갈린다. 추측으로 설정을 건드리면 멀쩡하던 부분까지 망가뜨리기 쉽다. error.log를 켜고 → 키워드를 읽고 → 해당 원인 하나만 손대는 순서를 몸에 붙여 두면, 다음 502는 패닉이 아니라 루틴이 된다.
운영 서버를 직접 굴리며 느낀 건, 한국에서 502가 가장 많이 터지는 지점이 두 군데라는 점이다. 하나는 무중단 배포(블루-그린)를 어설프게 구성했을 때 전환 순간 업스트림이 잠깐 비는 경우고, 다른 하나는 도커 컴포즈에서 127.0.0.1과 서비스 이름을 헷갈리는 경우다. 둘 다 코드 문제가 아니라 "nginx가 뒤를 어떻게 찾는가"의 문제라, 설정 파일이 아니라 네트워크 구성을 의심해야 빨리 잡힌다. 배포를 자동화한다면 앱의 헬스체크가 통과한 뒤에야 nginx가 트래픽을 넘기도록 순서를 묶어 두는 것만으로 전환-순간 502는 거의 사라진다. 이건 거창한 인프라가 아니라 배포 스크립트 한 줄 순서를 바꾸는 수준의 이야기라, 작은 팀일수록 효과가 크다.
502를 만나면 당황하지 말고 순서대로 가면 된다. error.log를 켜고, 키워드를 읽고, 위 여섯 중 하나를 고친다. 설정을 난도질하기 전에 로그 한 줄을 먼저 보는 습관, 그거 하나면 대부분 끝난다.
여러분은 502를 주로 어디서 만나나요? 배포 순간인가요, 아니면 트래픽 피크 때인가요? 각자의 "502 단골 원인"을 댓글로 공유해 주시면 다음에 올 사람들에게 큰 도움이 될 것 같다.
댓글 0
댓글을 불러오는 중…