Nginx 502 Bad Gateway: 같은 Connection refused, 다른 두 원인
Nginx 리버스 프록시가 502 Bad Gateway를 낼 때, 백엔드가 없는 경우와 백엔드가 다른 포트에서 기다리는 경우가 이번 실행에서 똑같은 연결 거부 메시지를 남겼습니다. 프록시는 멀쩡히 떠 있는데 서비스 요청만 실패하는 상황은 흔하고, 메시지만으로는 무엇을 고쳐야 하는지 가려지지 않습니다. 그래서 운영 중인 서버에서 생길 이 두 경우를 Debian 13 서버에서 재현·해결·점검 스크립트로 다시 만들고, 복구 뒤의 응답까지 기록했습니다. 에러 로그에 찍힌 접속 목적지를 실제 LISTEN 주소와 맞대어 보면, 백엔드를 띄울지 proxy_pass를 바꿀지를 근거를 갖고 고를 수 있습니다.
요청을 받는 주소와 Nginx가 넘기는 주소는 다르다
실습에서 요청을 받는 Nginx 주소는 127.0.0.1:18080으로 고정했습니다. Nginx가 요청을 넘기는 백엔드는 127.0.0.1:18081 또는 127.0.0.1:18082에서 돌립니다. 브라우저나 curl이 붙는 주소와 에러 로그의 upstream 주소를 구별하는 데서 진단이 시작됩니다. 아래 표의 실행 환경과 설정 위치를 기준으로 뒤의 출력에 나오는 주소와 경로를 읽으면 됩니다.
| 항목 | 테스트 환경 |
|---|---|
| OS | Debian GNU/Linux 13 (trixie) |
| Nginx | nginx/1.26.3 · apt 패키지 nginx · /usr/sbin/nginx |
| curl | 8.14.1 |
| 백엔드 | Python 3.13.5 · python3 -m http.server |
| 실습 경로와 접속 주소 | 전용 prefix ng · 전용 설정 ng/nginx.conf · 127.0.0.1:18080 |
| 확인일 | 2026-10-15 KST |
운영 Nginx와 섞이지 않도록 전용 prefix와 설정 파일을 썼고, 루트 권한이나 80 포트는 쓰지 않습니다. 기록에 나오는 NG는 /usr/sbin/nginx를, R은 실습 스크립트가 있는 run 디렉터리의 절대 경로를 가리킵니다. 그러니 ng/nginx.conf라는 상대 경로도 그 실습 디렉터리를 기준으로 읽어야 합니다. 명령을 다른 작업 경로로 옮겨 쓸 때는 이 경로 관계부터 맞춰 두세요.
같은 연결 거부, 원인은 백엔드 미기동과 포트 불일치
백엔드 없이 Nginx만 띄워 오류 만들기
common.sh의 mkconf는 받은 포트로 proxy_pass를 만들어 줍니다. 백엔드가 없는 상태에서 목적지를 18081로 지정하고 Nginx를 띄웁니다. 재현에 쓴 설정 생성 함수는 아래와 같고, 임시 경로와 로그 경로도 R 아래에 둬서 실습 설정을 따로 묶었습니다.
R=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd); NG=/usr/sbin/nginx
mkconf(){ mkdir -p $R/ng/logs $R/ng/tmp; cat > $R/ng/nginx.conf <<C
pid $R/ng/nginx.pid; error_log $R/ng/logs/error.log;
events {}
http { access_log $R/ng/logs/access.log; client_body_temp_path $R/ng/tmp; proxy_temp_path $R/ng/tmp; fastcgi_temp_path $R/ng/tmp; uwsgi_temp_path $R/ng/tmp; scgi_temp_path $R/ng/tmp;
server { listen 127.0.0.1:18080;
location / { proxy_pass http://127.0.0.1:$1; } } }
C
}
재현 스크립트는 설정의 proxy_pass 값을 확인한 뒤 프록시 주소로 요청을 보냅니다. 출력의 nginx_started는 Nginx가 제대로 떴다는 뜻인데, 바로 뒤의 요청은 HTTP 502를 돌려줍니다. 로그에는 프록시가 127.0.0.1:18081로 연결하다 거부당했다고 남습니다. 기동 성공 메시지를 서비스 복구의 기준으로 삼으면 백엔드 연결 실패를 그대로 놓친다는 이야기입니다.
$ $NG -v
nginx version: nginx/1.26.3
$ grep -n proxy_pass ng/nginx.conf
5: location / { proxy_pass http://127.0.0.1:18081; } } }
$ $NG -p $R/ng -c $R/ng/nginx.conf && echo nginx_started
nginx_started
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18080/
HTTP 502
$ tail -1 ng/logs/error.log
2026/10/15 23:18:22 [error] 1418808#1418808: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:18081/", host: "127.0.0.1:18080"

캡처에서는 요청을 받은 주소의 18080과 로그에 남은 목적지 18081을 같이 봐야 합니다. HTTP 502는 사용자 눈에 보이는 결과이고, connect() failed (111)은 그 요청이 업스트림 연결 단계에서 막혔다는 단서입니다. 프록시 프로세스를 다시 띄우기 전에 이 목적지에서 백엔드가 기다리고 있는지부터 보면 고칠 곳이 금방 좁혀집니다. 다음 단계에서는 설정은 그대로 두고 백엔드만 띄워 응답이 어떻게 바뀌는지 봅니다.
에러 로그의 upstream 주소가 가리키는 곳
읽어야 할 구간은 connect() failed (111: Connection refused) while connecting to upstream입니다. 이 실습에서 111은 리눅스 errno ECONNREFUSED이고, 접속하려는 포트에 LISTEN 중인 프로세스가 없을 때 나는 값입니다. 백엔드 상태는 로그에 적힌 upstream: “http://127.0.0.1:18081/”을 기준으로 조사합니다. 요청을 받은 host의 18080과 실제로 연결을 시도한 upstream의 18081을 헷갈리면 엉뚱한 곳을 고치게 됩니다.
HTTP 502라는 상태 하나로 고칠 대상을 정하지는 않습니다. 이번 기록에서 쓸모 있었던 것은 에러 로그가 백엔드 연결 단계의 실패와 접속 시도 주소를 함께 보여 준다는 점이었습니다. 그래서 그 주소와 포트에서 백엔드가 기다리는지부터 봤습니다. 다른 요청의 로그가 섞이면 목적지를 잘못 짚을 수 있으니, 실패한 요청의 시각과 요청 경로를 같이 맞춰 봅니다.
proxy_pass가 맞다면 그 포트에 백엔드를 띄운다
백엔드를 18081에서 돌리는 구성이 원래 의도였다면, Nginx 설정은 건드리지 말고 백엔드부터 띄웁니다. 해결 스크립트는 python3 -m http.server에 18081과 –bind 127.0.0.1을 줍니다. 이어서 ss -ltn으로 프록시와 백엔드의 LISTEN을 보고, 같은 프록시 주소로 다시 요청합니다. 실행 명령, 응답, 접근 로그가 한 흐름으로 이어집니다.
$ (cd /tmp && setsid python3 -m http.server 18081 --bind 127.0.0.1 >/dev/null 2>&1 &) ; sleep 1; ss -ltn | grep -E "1808[01]"
LISTEN 0 5 127.0.0.1:18081 0.0.0.0:*
LISTEN 0 511 127.0.0.1:18080 0.0.0.0:*
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18080/
HTTP 200
$ tail -1 ng/logs/access.log | cut -c1-90
127.0.0.1 - - [15/Oct/2026:23:18:26 +0900] "GET / HTTP/1.1" 200 260467 "-" "curl/8.14.1"

LISTEN 목록에 127.0.0.1:18081이 나타난 다음 HTTP 200이 돌아오고, access.log에도 같은 요청의 200 응답이 남습니다. 이 경우는 proxy_pass가 처음부터 의도한 백엔드 포트를 가리키고 있었으니, 백엔드를 띄우는 것만으로 연결 관계가 맞춰졌습니다. Python 백엔드의 실행 형식은 Python http.server 문서 (확인일 2026-10-15)에 나와 있습니다.
proxy_pass가 의도한 주소와 맞을 때는 백엔드 기동에만 집중하면 되고, 설정을 고치지 않고도 LISTEN과 HTTP 응답으로 결과가 확인됩니다. 다만 실제 백엔드를 다른 포트에서 돌리려는 상황이라면 이 방법은 답이 아닙니다. 기동 명령을 실행했다는 사실로 끝내지 말고, 그 주소의 LISTEN과 프록시 응답까지 봐야 합니다.
백엔드가 다른 포트에 있다면 proxy_pass를 옮긴다
이번에는 백엔드를 18082에서 돌리고 proxy_pass는 18081로 둡니다. 점검 스크립트의 LISTEN 목록에 백엔드는 보이지만, Nginx가 연결하려는 포트와는 맞지 않습니다. 백엔드 프로세스가 떠 있다는 것만 보고 백엔드 점검을 끝내면 놓치기 쉬운 상황입니다. 기록에서는 오류를 확인한 뒤 sed로 목적지 포트를 바꾸고, nginx -t 검사와 reload를 거쳐 다시 요청합니다.
$ (cd /tmp && setsid python3 -m http.server 18082 --bind 127.0.0.1 >/dev/null 2>&1 &) ; sleep 1; ss -ltn | grep -E "1808[0-2]"
LISTEN 0 511 127.0.0.1:18080 0.0.0.0:*
LISTEN 0 5 127.0.0.1:18082 0.0.0.0:*
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18080/
HTTP 502
$ tail -1 ng/logs/error.log | grep -o "connect() failed.*upstream: \"[^\"]*\""
connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:18081/"
$ sed -i "s#127.0.0.1:18081#127.0.0.1:18082#" ng/nginx.conf && $NG -t -p $R/ng -c $R/ng/nginx.conf
nginx: the configuration file /home/user/nginx-502/ng/nginx.conf syntax is ok
nginx: configuration file /home/user/nginx-502/ng/nginx.conf test is successful
$ $NG -p $R/ng -c $R/ng/nginx.conf -s reload; sleep 1; curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18080/
2026/10/15 23:18:32 [notice] 1419299#1419299: signal process started
HTTP 200
Terminated

nginx -t 캡처의 설정 파일 경로는 실제 작업 폴더이고, 본문에서는 /home/user/nginx-502로 줄였습니다. 문법 검사 결과는 같습니다.
수정 전에는 백엔드가 18082에서 LISTEN하고 있는데도 upstream 주소가 18081이라 HTTP 502가 납니다. 설정을 18082로 바꾸고 나면 syntax is ok와 test is successful이 나오고 reload로 넘어갑니다. 그 뒤 같은 18080 주소로 보낸 요청은 HTTP 200을 돌려줍니다. 목적지를 바꾼다는 것이 무슨 뜻인지는 Nginx proxy_pass 문서 (확인일 2026-10-15)에 설명돼 있습니다.
백엔드가 의도한 새 포트에서 이미 기다리고 있다면, Nginx의 전달 목적지만 맞춰 지금 구성 그대로 복구할 수 있고 수정 전후의 upstream 포트와 HTTP 응답을 비교하기도 쉽습니다. 대신 설정 수정, 검사, reload까지 거쳐야 합니다. 고친 파일의 경로가 실행 중인 실습 설정과 같은지도 맞춰 봐야 하고, 문법 검사가 통과한 뒤에도 실제 요청 결과는 따로 확인해야 합니다.
upstream 주소, LISTEN, 설정 경로를 한 줄로 맞추는 순서
로그의 upstream 주소로 LISTEN을 찾기
이 사례에서 먼저 맞춰 보는 것은 에러 로그의 upstream 값, ss -ltn의 LISTEN 주소와 포트, 설정 파일의 proxy_pass 값 세 가지입니다. 프록시의 18080이 LISTEN하는 것과 백엔드의 18081 또는 18082가 LISTEN하는 것은 따로 확인합니다. 점검 스크립트처럼 백엔드가 다른 포트에서 기다리는 경우라면, 프로세스가 떴는지보다 접속 목적지가 맞는지가 먼저입니다.
순서로 적으면 이렇습니다. 실패한 요청의 에러 로그를 읽고, 그 로그의 주소를 LISTEN 목록에서 찾고, 설정의 목적지와 비교합니다. 백엔드가 없으면 의도한 포트에 띄우고, 백엔드가 다른 포트에 있으면 어느 포트를 쓰는 구성인지 정한 뒤 proxy_pass를 맞춥니다. 같은 연결 거부 메시지를 보고도 서로 다른 수정 지점을 가려낼 수 있는 이유가 이 순서에 있습니다.
검사와 reload도 같은 -p, -c로
전용 설정으로 띄운 실습이라면 설정 검사에도 같은 -p와 -c 인자를 줘야 합니다. 고친 파일과 검사한 파일을 일치시키는 단계입니다. 아래 명령은 R과 NG가 잡혀 있는 실습 셸에서 돌립니다. 검사가 성공하면 reload로 이어지고, 응답 확인은 앞의 실행 기록처럼 프록시 주소로 다시 요청해서 합니다.
$NG -t -p "$R/ng" -c "$R/ng/nginx.conf" &&
$NG -p "$R/ng" -c "$R/ng/nginx.conf" -s reload
nginx -t가 성공한 것과 HTTP 200이 돌아온 것은 서로 다른 점검입니다. 앞의 기록도 문법 검사 메시지 다음에 reload를 하고, 마지막 요청으로 복구를 확인했습니다. 설정을 다 바꿨다고 응답 점검을 빼면 안 됩니다. 설정을 적용하는 흐름은 Nginx 시작 안내 (확인일 2026-10-15)에 정리돼 있습니다.
프록시가 떠 있어도 남는 백엔드 연결 거부
패키지를 갱신한 뒤 연결 거부가 났다면, 바뀐 환경에서도 실행 명령과 설정 파일이 같은 대상을 가리키는지부터 맞춰 봅니다. 이번 복구 기록에서 원인을 가른 근거는 백엔드가 떴는지와 포트가 맞는지였으니, 버전 호환성 문제로 몰아갈 이유는 없습니다. 옵션이나 설정 위치를 바꾸는 작업이라면 기동에 쓰는 경로와 검사·reload에 쓰는 경로를 함께 바꿔야 합니다. 이 실습처럼 전용 설정을 쓴다면 -p와 -c를 작업 기록에 남겨, 어떤 파일을 검사하고 적용했는지 나중에 따라갈 수 있게 합니다.
헬스체크도 프로세스 기동 여부와 실제 서비스 요청을 나눠서 설계해야 합니다. 이번 출력에서 Nginx가 뜬 뒤에도 요청은 실패했으니, 프록시가 살아 있다는 신호만으로 백엔드까지 준비됐다고 보기는 어렵습니다. 운영 환경에서 백엔드 주소나 서비스 경로를 바꿀 때는 프록시 설정과 헬스체크 대상을 같이 맞춰 보고, 실제 요청이 백엔드까지 닿는지 확인하기를 권장합니다. 쿠버네티스의 준비 상태 검사에 쓸 때도 애플리케이션이 실제로 준비됐는지 드러나는 경로를 고르고, 경로가 바뀌면 배포 설정에도 같이 반영해야 합니다.
upstream 주소와 LISTEN 주소가 같은지부터 맞추는 복구
Debian 13에서 백엔드 미기동과 proxy_pass 포트 불일치를 각각 재현했고, 의도한 연결 관계를 맞춘 뒤 HTTP 200을 얻었습니다. 같은 연결 거부 로그라도 실제 LISTEN 상태에 따라 손댈 곳이 달라집니다. 장애 대응 중 백엔드를 띄울지 프록시 설정을 고칠지는 아래 표로 판단하면 됩니다.
| 상황 | 판단 근거 | 해결 방법 | 복구 확인 |
|---|---|---|---|
| 백엔드 미기동 | upstream은 18081이며 해당 백엔드가 없는 상태로 재현합니다. | 127.0.0.1:18081에서 백엔드를 기동합니다. | LISTEN, HTTP 200, access.log의 200을 확인합니다. |
| 백엔드 포트 불일치 | LISTEN은 18082이고 upstream은 18081로 기록합니다. | proxy_pass를 18082로 수정한 뒤 nginx -t와 reload를 수행합니다. | 같은 프록시 주소로 재요청하여 HTTP 200을 확인합니다. |
세 실행 기록에서 가장 쓸모 있었던 진단 기준은 실패한 요청의 upstream 주소와 실제 LISTEN 주소가 같으냐였습니다. 설정이 의도한 포트를 가리키고 있다면 백엔드를 띄우고, 백엔드가 의도한 다른 포트에서 기다리고 있다면 proxy_pass를 맞추는 방식을 추천합니다. 운영 구성에서는 프록시 설정과 백엔드 기동 설정이 쓰는 주소를 한곳에서 관리하고, 바꿀 때마다 검사·reload·실제 요청 확인을 한 묶음으로 돌립니다. 복구 기록에는 고친 파일의 경로와 변경 전후 목적지를 남겨, 다음 담당자가 같은 오류를 만났을 때 판단 근거를 그대로 이어받게 합니다.
실시간 서비스에 적용한다면 재현용 Python 서버 구성에 머물지 말고, 서비스에 맞는 백엔드 실행 방식과 준비 상태 검사를 갖추는 구성을 권장합니다. 필요한 서버 사양은 이 연결 거부 기록으로 정할 수 있는 값이 아니니 실제 서비스 부하와 응답 요구를 기준으로 고릅니다. 장애 대응에서는 서버 자원을 늘리기 전에 요청이 어느 주소로 넘어가고 어디서 기다리는지를 맞추는 일이 먼저입니다. 저는 설정 검사가 성공한 뒤에도 프록시 주소로 다시 요청해 응답과 로그를 맞대어 보는 단계를 복구 완료 조건으로 두겠습니다.






