Nginx 도커 발행시 502에러가 발생하는 이유

Nginx 도커 발행시 502에러가 발생하는 이유

nginx를 도커 컨테이너로 띄워 다른 컨테이너 앞에 두면, 앱 컨테이너를 다시 빌드한 직후 502 Bad Gateway가 날 수 있습니다. 앱은 정상인데 nginx가 컨테이너의 예전 IP로 연결을 시도하기 때문입니다.

Nginx 도커 발행시 502에러가 발생하는 이유

제 서버에서는 docker compose up -d --build로 앱을 다시 띄울 때마다 Cloudflare의 502 화면이 떴습니다. 앱 로그는 깨끗했고, 서버 안에서 앱에 직접 요청하면 정상 응답이 왔습니다. 이 글은 그 원인과, nginx 설정 세 줄로 다시는 생기지 않게 한 방법을 정리한 것입니다.

원인: nginx는 컨테이너 주소를 시작할 때 한 번만 찾는다

nginx 설정에 proxy_pass http://앱컨테이너이름:8080;처럼 컨테이너 이름을 적으면, nginx는 설정을 읽을 때(시작하거나 reload할 때) 그 이름을 IP로 한 번 바꾼 뒤 계속 그 IP를 씁니다. 이름을 다시 찾아보지 않습니다.

문제는 도커가 컨테이너를 새로 만들 때마다 IP를 다시 배정한다는 점입니다. docker compose up -d --build는 기존 컨테이너를 지우고 새 컨테이너를 만들기 때문에, 앱의 IP가 바뀔 수 있습니다. 그러면 nginx는 이미 없는 예전 IP로 요청을 보내고, 연결에 실패해 502 Bad Gateway를 돌려줍니다.

그동안 멀쩡했던 것은 운이 좋았기 때문입니다. 새 컨테이너가 우연히 예전과 같은 IP를 다시 받으면 문제가 드러나지 않습니다. 제 경우에는 같은 네트워크의 다른 컨테이너들을 정리한 뒤부터 IP 배정 순서가 달라져 매번 502가 났습니다.

확인 방법: 앱에 직접 물어보기

502의 원인이 앱인지 nginx인지는 서버 안에서 앱에 직접 요청해 보면 바로 갈립니다. 앱이 포트를 127.0.0.1:8080으로 열어 두었다면 이렇게 확인합니다.

# 1) 앱 컨테이너가 떠 있는지
sudo docker ps --format '{{.Names}}\t{{.Status}}'

# 2) nginx를 거치지 않고 앱에 직접 요청
curl -s -o /dev/null -w '앱 직접 응답: %{http_code}\n' http://127.0.0.1:8080/

# 3) 앱의 지금 IP와 nginx가 연결하려는 IP 비교
sudo docker inspect 앱컨테이너이름 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}'
sudo docker logs --tail 20 nginx 2>&1 | grep -i upstream

결과는 이렇게 읽습니다.

앱 직접 응답뜻
200, 401 등 숫자앱은 정상입니다. nginx가 예전 IP를 붙잡고 있는 것입니다.
000앱이 응답하지 않습니다. 앱 로그(docker logs)부터 확인합니다.

앱이 정상인데 nginx 로그의 upstream 주소가 3번에서 본 IP와 다르면 이 글의 경우가 맞습니다.

임시 해결: nginx reload

nginx에게 설정을 다시 읽히면 컨테이너 이름을 새로 찾기 때문에 바로 복구됩니다. 접속 중인 사용자를 끊지 않고 적용됩니다.

sudo docker exec nginx nginx -s reload

다만 다음 빌드 때 또 같은 일이 생깁니다. 당장은 빌드 명령 뒤에 reload를 붙여 쓰면 됩니다.

sudo docker compose up -d --build && sudo docker exec nginx nginx -s reload

근본 해결: 도커 내부 DNS에 주기적으로 다시 묻게 하기

nginx가 컨테이너 이름을 정해진 시간마다 다시 찾게 하면, 컨테이너를 새로 만들어도 reload 없이 따라갑니다. 필요한 것은 설정 세 줄입니다.

바꾸기 전:

location / {
    proxy_pass http://app_container:8080;
    ...
}

바꾼 뒤:

location / {
    resolver 127.0.0.11 valid=10s;
    set $app_upstream http://app_container:8080;
    proxy_pass $app_upstream;
    ...
}
  • resolver 127.0.0.11: 도커가 모든 컨테이너 안에 제공하는 내부 DNS 서버 주소입니다. 컨테이너 이름을 지금의 IP로 알려 줍니다.
  • valid=10s: 찾은 IP를 10초 동안만 기억하고, 그 뒤에는 다시 묻습니다.
  • set + 변수로 쓴 proxy_pass: 이 부분이 핵심입니다. proxy_pass에 주소를 직접 적으면 nginx는 resolver를 두어도 시작할 때 한 번만 찾습니다. 주소를 변수에 담아 넘겨야 요청이 올 때마다 resolver로 찾습니다.

적용은 설정 검사를 통과할 때만 reload하도록 합니다.

sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload

이후에는 평소처럼 빌드만 하면 되고, 새 컨테이너가 뜬 뒤 길어야 10초 안에 nginx가 새 IP로 넘어갑니다.

주의할 점

  • 사용자 정의 네트워크에서만 됩니다. 127.0.0.11 DNS는 docker compose가 만드는 네트워크나 docker network create로 만든 네트워크에 붙은 컨테이너에서만 동작합니다. 도커 기본 bridge 네트워크에서는 컨테이너 이름으로 찾을 수 없습니다. nginx와 앱이 같은 네트워크에 붙어 있는지 먼저 확인하세요.
  • 변수 proxy_pass는 주소 뒤 경로를 다르게 다룹니다. proxy_pass http://app:8080/api/;처럼 주소 뒤에 경로를 붙여 쓰던 설정은 변수로 바꾸면 요청 경로가 다르게 전달될 수 있습니다. 이 글처럼 http://이름:포트까지만 쓰는 경우는 차이가 없습니다.
  • 잠깐의 502는 남을 수 있습니다. 새 컨테이너가 뜨는 몇 초 동안과, nginx가 기억한 IP의 유효 시간(valid=10s)이 지나기 전까지는 502가 날 수 있습니다. 계속 이어지지는 않습니다.
  • 다른 방법도 있습니다. 앱 컨테이너에 고정 IP를 주는 방법(ipv4_address)이나, 앱 포트를 서버에 열고 nginx가 host.docker.internal 같은 호스트 주소로 연결하는 방법도 있습니다. 다만 설정이 늘고 관리할 것이 많아져서, 컨테이너가 여럿이라면 resolver 방식이 가장 간단합니다.
  • Cloudflare를 쓴다면 502와 526을 구분하세요. 502는 nginx 뒤의 앱에 연결하지 못한 것이고, 526은 Cloudflare가 서버의 SSL 인증서를 믿지 못한 것입니다. 원인이 전혀 다릅니다.

한눈에 보기

# 1) 원인 확인: 앱 직접 응답이 숫자면 앱은 정상, nginx 문제
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/

# 2) 당장 복구
sudo docker exec nginx nginx -s reload

# 3) 근본 해결: nginx 설정의 proxy_pass를 아래 세 줄로 바꾼 뒤
#    resolver 127.0.0.11 valid=10s;
#    set $app_upstream http://app_container:8080;
#    proxy_pass $app_upstream;
sudo docker exec nginx nginx -t && sudo docker exec nginx nginx -s reload

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다