BlueByte
502 Bad GatewayFixed

nginx: upstream에서 오는 502 Bad Gateway

작성 Haneul Seo2026년 9월 4일 업데이트7 min

안녕하세요, BlueByte입니다. nginx의 502 Bad Gateway는 nginx 자체는 멀쩡하다는 뜻입니다 — 뒤에 있는 upstream이 응답하지 않았거나, nginx가 쓸 수 없는 응답을 보낸 것입니다. 증상은 nginx -t는 통과하는데도 nginx가 502를 돌려주는 것입니다. 오늘은 502가 실제로 뜻하는 것, error log를 읽어 정확한 실패를 찾는 법, 원인별 해결, 그리고 예방까지 하나씩 짚어보겠습니다.

nginx의 502가 실제로 뜻하는 것

브라우저에는 짧은 페이지가 뜨고 access log에는 상태 코드가 기록됩니다:

203.0.113.7 - - [04/Sep/2026:11:20:03 +0900] "GET /api HTTP/1.1" 502 552 "-" "curl/8.4.0"

nginx는 요청을 받아 proxy_pass에 지정된 서버로 프록시하려 했지만, 연결하지 못했거나 유효하지 않다고 판단한 응답을 받았습니다. 이것은 nginx가 죽은 것이 아닙니다 — nginx는 살아서 응답하고 있고, 그 뒤의 게이트웨이가 문제입니다. 그래서 nginx를 재시작해도 대개 도움이 안 됩니다.

upstream을 못 쓰게 만드는 네 가지

502는 다음 중 하나에서 옵니다:

  • upstream이 실행 중이 아님 또는 proxy_pass의 주소에서 리슨하지 않음 — 연결이 거부됨.
  • proxy_pass가 틀린 호스트·포트·소켓을 가리킴 — nginx가 아무것도 없는 곳에 연결.
  • upstream이 응답 도중 크래시하거나 연결을 닫음 — nginx가 비었거나 잘린 응답을 읽음.
  • upstream의 응답 헤더가 proxy_buffer_size(기본 4k/8k)보다 큼 — nginx가 유효하지 않다고 거부.

살아 있지만 느린 upstream은 별개의 오류입니다 — 그건 504이며, 끝에서 다룹니다.

error log 읽기 — 정확한 실패를 알려준다

access log는 502를 보여주고, error log는 이유를 말해줍니다. 요청을 재현하면서 지켜보세요:

sudo tail -f /var/log/nginx/error.log
2026/09/04 11:20:03 [error] 812#812: *5 connect() failed (111: Connection refused)
  while connecting to upstream, client: 203.0.113.7, upstream: "http://127.0.0.1:8000/api"

괄호 안을 읽으세요. (111: Connection refused)는 리슨하는 게 없다는 뜻, upstream prematurely closed connection은 앱이 응답 도중 죽었다는 뜻, upstream sent too big header는 헤더가 버퍼를 넘쳤다는 뜻, (13: Permission denied)는 RHEL에서 보통 SELinux가 아웃바운드 연결을 막는다는 뜻입니다. upstream: 필드는 nginx가 시도한 정확한 주소를 출력하니, 앱이 실제로 리슨하는 곳과 맞는지 확인하세요.

원인별로 고치기: 죽은 upstream·틀린 주소·큰 헤더

  1. upstream이 리슨하지 않음 — 포트에 무엇이 있는지 확인하고 앱을 시작합니다:
ss -ltnp | grep :8000
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/api

curl도 실패하면 nginx는 죄가 없습니다 — 앱을 고치세요. 정상 upstream은 200을 출력합니다.

  1. 틀린 proxy_pass — 실제 주소로 가리키고 reload합니다:
location /api {
    proxy_pass http://127.0.0.1:8000;
}
  1. 헤더가 너무 큼 — 해당 location의 버퍼를 키웁니다:
proxy_buffer_size 16k;
proxy_buffers 8 16k;

실제 사례: 앱이 죽어 소켓이 끊긴 경우

nginx 뒤의 Node 앱이 502를 내기 시작합니다. nginx -t는 정상이라 tail -f /var/log/nginx/error.log를 실행하니 connect() failed (111: Connection refused) while connecting to upstream: "http://127.0.0.1:3000/"가 보입니다. ss -ltnp | grep :3000을 실행하면 리슨하는 게 없습니다 — 앱 프로세스가 밤사이 종료돼 포트가 비었습니다. systemctl restart myapp으로 서비스를 재시작하고, curl http://127.0.0.1:3000/이 200을 돌려주자 502가 즉시 멈춥니다. nginx는 처음부터 사실을 보고하고 있었습니다: 닿을 게이트웨이가 없었던 것입니다.

프록시 경로를 끝까지 확인하기

설정을 검사하고 reload한 뒤 nginx를 통해 요청합니다:

sudo nginx -t && sudo nginx -s reload
curl -I https://example.com/api
HTTP/1.1 200 OK

공개 URL을 통해 200(또는 앱의 정상 상태)이 나오면 nginx가 upstream에 닿아 응답을 되돌려준 것입니다. 테스트하는 동안 error log를 열어 두세요 — 정상 요청은 새 [error] 줄을 남기지 않습니다.

502가 다시 생기지 않게 하기

upstream을 supervisor 아래에서 실행하세요 — systemd, 컨테이너 재시작 정책, 또는 프로세스 매니저 — 그러면 크래시가 나도 죽은 포트를 남기지 않고 재시작합니다. 헬스 체크를 추가해, 준비되지 않은 백엔드가 502를 내기 전에 로테이션에서 빠지게 하세요. RHEL/SELinux 호스트에서는 sudo setsebool -P httpd_can_network_connect 1로 nginx의 아웃바운드 연결을 한 번 허용하세요. proxy_buffer_size는 앱이 보내는 가장 큰 헤더에 맞춰 잡으세요 — 지나치게 큰 auth 토큰과 쿠키가 흔한 오버플로 원인입니다. 502가 간헐적이고 로그에 upstream prematurely closed connection이 보이면, 백엔드가 nginx가 재사용하기 전에 유휴 keepalive 연결을 닫고 있을 가능성이 큽니다 — proxy_http_version 1.1과 upstream의 keepalive 개수를 설정하거나, nginx가 오래된 연결을 고르지 않도록 백엔드의 유휴 타임아웃을 줄이세요.

502는 504와 무엇이 다른가

502 Bad Gateway는 upstream에 닿지 못했거나 유효하지 않은 응답을 받았다는 뜻입니다 — 연결이 실패했거나 응답이 깨진 것입니다. 504 Gateway Timeout은 upstream에 닿기는 했지만 너무 느렸다는 뜻입니다: proxy_connect_timeout·proxy_send_timeout·proxy_read_timeout(각 기본 60s) 안에 응답하지 않은 것입니다. error log가 upstream timed out이라고 하면 해당 타임아웃을 늘리거나 백엔드를 빠르게 만드세요 — 그건 504이지 502가 아닙니다.

관련 질문

nginx -t는 통과하는데 여전히 502가 뜹니다.

nginx -t는 설정 문법만 검사하지 upstream이 살아 있는지는 보지 않습니다. 죽은 백엔드를 가리키는 유효한 설정도 502를 냅니다. error log를 읽고 ss -ltnp로 upstream 포트를 확인하세요.

error log에 'upstream prematurely closed connection'이 뜹니다.

백엔드가 연결을 받은 뒤 응답을 끝내기 전에 죽거나 리셋한 것입니다 — 앱 크래시나 out-of-memory kill이 흔합니다. 앱 자체 로그를 확인하고 supervisor 아래에서 재시작하세요.

'upstream sent too big header'가 뜹니다.

응답 헤더가 proxy_buffer_size를 넘었습니다. 해당 location의 proxy_buffer_size와 proxy_buffers를 키우세요; 큰 auth 쿠키나 토큰이 보통 원인입니다.

앱이 살아 있고 로컬 curl도 되는데 RHEL에서 502가 뜹니다.

SELinux가 nginx의 아웃바운드 연결을 막고 있을 가능성이 큽니다 — error log에서 '(13: Permission denied)'를 찾아보세요. sudo setsebool -P httpd_can_network_connect 1로 허용하세요.

502는 504와 같은 건가요?

아닙니다. 502는 upstream에 닿지 못했거나 유효하지 않은 응답을 받은 것이고, 504는 닿기는 했지만 프록시 타임아웃(기본 60s) 안에 응답하지 않은 것입니다. error log 줄이 둘을 구분해 줍니다.

참고 자료

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

같은 카테고리 다른 글

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)

V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트

컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.

Docker