Nginx Proxy Manager 502 Bad Gateway 해결: Docker 네트워크·포트·HTTP/HTTPS 진단
Nginx Proxy Manager(NPM)에 도메인과 인증서를 연결했는데 브라우저에는 502 Bad Gateway만 보일 때가 있다. 같은 서버의 다른 Proxy Host는 정상이고, 대상 애플리케이션도 호스트 포트로 직접 열면 보이는데 NPM을 거치면 실패하는 경우도 있다.
이때 인증서를 다시 발급하거나 컨테이너 IP를 고정하는 것부터 시작하면 원인이 더 흐려진다. 502는 게이트웨이가 업스트림 응답을 처리하지 못했을 때 나타날 수 있지만, 앞단 CDN·다른 프록시·업스트림 자체가 반환한 응답일 수도 있다. 먼저 해당 NPM Proxy Host의 요청 로그에서 요청 도달을 확인하고, 오류 로그와 함께 실패 구간을 좁힌다.
이 글의 네트워크·서비스명·내부 포트 예시는 NPM과 업스트림이 같은 Docker 호스트에서 사용자 정의 bridge 네트워크를 공유하는 구성을 기준으로 한다. 원격 서버나 호스트에서 직접 실행하는 앱은 그 대상의 실제 주소·포트와 라우팅 조건을 별도로 확인해야 한다.
확인 순서는 다음처럼 바깥에서 안쪽으로 좁힌다.
- NPM 자체가 요청을 받았는지 로그에서 확인한다.
- 업스트림 컨테이너가 실행 중이고 실제 요청에 응답하는지 확인한다.
- NPM과 업스트림이 같은 사용자 정의 Docker 네트워크를 공유하는지 확인한다.
- NPM 컨테이너에서 서비스명이 해석되는지 확인한다.
- Forward Port가 호스트 포트가 아니라 컨테이너 내부 포트인지 확인한다.
- NPM의 Scheme과 업스트림이 실제로 말하는 HTTP·HTTPS가 일치하는지 확인한다.
- 변경 뒤 내부 경로와 외부 HTTPS를 각각 다시 검증한다.
아래 명령의 컨테이너명, Compose 서비스명, 네트워크명, 도메인과 포트는 실제 값으로 바꿔야 한다.
<ACTUAL_...>와 같은 자리표시자는 꺾쇠까지 실제 값으로 교체하고 바깥쪽 작은따옴표는 유지한다. 전체docker inspect, Compose 환경변수, NPM 설정 파일을 공개 질문에 그대로 붙이면 비밀번호·토큰·내부 주소가 노출될 수 있다.
3줄 요약
- 같은 Docker 호스트의 컨테이너끼리 연결한다면 사용자 정의 네트워크를 공유하고, Forward Hostname에는 그 네트워크의 유일한 서비스명 또는 별칭을 사용하는 구성을 권장한다.
- 컨테이너 간 통신에는
HOST_PORT가 아니라 애플리케이션이 실제로 듣는CONTAINER_PORT를 사용한다. - 브라우저 구간의 HTTPS와 업스트림 구간의 Scheme은 별개다. NPM에서 TLS를 종료하고 Ghost로 전달한다면 업스트림은 보통 HTTP이지만, 실제 설정과 수신 포트를 확인해 결정한다.
먼저 구분할 것: 502는 어디까지 도착했다는 뜻인가
브라우저에 NPM 또는 NGINX 형태의 502 페이지가 보여도 그것만으로 요청이 의도한 NPM까지 도달했다고 확정할 수는 없다. 잘못된 DNS 대상이나 앞단 CDN·다른 프록시도 같은 형태의 응답을 반환할 수 있다. 요청 시각·도메인·경로가 해당 Proxy Host의 access log에 기록되는지 확인한 뒤 NPM에서 업스트림으로 넘어가는 두 번째 구간을 확인한다.
브라우저는 HTTPS로 Nginx Proxy Manager에 연결한다. NPM은 설정된 HTTP 또는 HTTPS로 업스트림 앱에 별도 요청을 전달한다.
첫 번째 구간의 인증서가 정상이어도 두 번째 구간의 네트워크, 이름, 포트 또는 Scheme이 틀리면 502가 발생할 수 있다. 반대로 NPM 관리 화면이 열리는 것만으로 모든 Proxy Host의 업스트림 연결이 정상이라는 뜻도 아니다.
NGINX 공식 문서는 proxy_pass 대상 주소에 도메인명 또는 IP와 포트를 지정하고, 그 프록시 서버에서 응답을 받아 클라이언트로 돌려주는 구조를 설명한다. NPM은 이 구성을 UI로 관리하므로 Forward Hostname / IP, Forward Port, Scheme이 결국 업스트림 주소를 결정한다.
0단계: 변경 전에 현재 상태를 기록한다
먼저 문제가 시작된 시각과 현재 컨테이너 상태를 남긴다.
date -Is
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker network ls
각 Compose 프로젝트에서 최종 설정을 검증한다. NPM과 앱이 다른 프로젝트라면 각 프로젝트 디렉터리에서 따로 실행하며, 기존 배포에서 사용한 -f, --env-file, -p 옵션이 있다면 동일하게 적용한다. 아래의 모든 Compose 명령도 이 조건을 유지한다.
docker compose config -q
docker compose config --services
docker compose config --networks
docker compose ps -a
docker compose config 전체 출력에는 환경변수 치환 결과가 포함될 수 있다. 외부에 공유할 때는 비밀번호, 토큰, 이메일, 도메인과 내부 주소를 제거한다.
NPM Proxy Host 화면에서는 다음 값만 별도로 기록한다.
- Domain Names
- Scheme
- Forward Hostname / IP
- Forward Port
- Websockets Support 여부
- Access List 연결 여부
- Advanced 탭의 사용자 정의 설정 유무
무엇을 바꿨는지 설명할 수 있어야 롤백도 가능하다.
1단계: NPM과 업스트림 로그의 같은 시각을 본다
테스트 요청을 한 번 보내고 직후 로그를 제한된 범위로 확인한다.
curl -I --max-time 10 'https://<ACTUAL_DOMAIN>/'
docker logs --since=10m '<ACTUAL_NPM_CONTAINER>'
docker logs --since=10m '<ACTUAL_UPSTREAM_CONTAINER>'
docker logs는 컨테이너의 표준 출력·오류 출력을 보여준다. NPM의 개별 Proxy Host 요청 로그는 별도 파일에 기록되므로 위 명령만 보고 요청 유무를 판정하지 않는다. 대상 Proxy Host ID를 확인하고, 해당 호스트의 access log와 error log를 함께 읽는다. 기본 NPM 템플릿의 경로는 다음과 같으며, 배포 버전이나 사용자 정의 설정으로 경로가 달라졌다면 실제 지정 경로를 따른다.
docker exec '<ACTUAL_NPM_CONTAINER>' \
tail -n 100 '/data/logs/proxy-host-<ACTUAL_PROXY_HOST_ID>_access.log'
docker exec '<ACTUAL_NPM_CONTAINER>' \
tail -n 100 '/data/logs/proxy-host-<ACTUAL_PROXY_HOST_ID>_error.log'
최근 100줄에는 이전 요청도 포함되므로 테스트 요청의 시각·도메인·경로를 맞춰 본다. 로그를 외부에 공유한다면 클라이언트 IP·쿼리 문자열·토큰을 가린다. 파일이 없거나 로그 회전이 진행됐다면 실제 파일 경로와 로깅 설정을 먼저 확인한다.
Compose로 관리한다면 컨테이너명 대신 서비스 단위 로그를 사용할 수 있다. NPM과 업스트림이 다른 프로젝트라면 아래 두 명령을 각각 해당 프로젝트에서 실행한다. 이 명령도 Proxy Host 파일 로그 조회를 대신하지 않는다.
docker compose logs --since=10m '<ACTUAL_NPM_SERVICE>'
docker compose logs --since=10m '<ACTUAL_UPSTREAM_SERVICE>'
해석은 다음처럼 나눈다.
| 관찰 | 우선 확인할 곳 |
|---|---|
| NPM 로그에 이름 해석 실패 | Forward Hostname, 서비스명, 공통 네트워크 |
| 연결 거부 | 업스트림 실행 상태, 수신 포트, 수신 주소 |
| TLS handshake 오류 | NPM Scheme과 업스트림 TLS 설정 |
| 시간 초과 | 네트워크 경로, 방화벽, 업스트림 응답 지연 |
| 업스트림 로그에 요청이 전혀 없음 | access logging 활성 여부와 출력 위치를 확인한 뒤 NPM→업스트림의 이름·네트워크·포트 |
| 같은 요청에 대한 업스트림 4xx·5xx가 있음 | 요청 전달은 됐으므로 앱 설정·의존성과 NPM이 전달한 Host·인증 헤더 등을 함께 확인 |
문구 하나만 보고 즉시 설정을 바꾸기보다 NPM 로그와 업스트림 로그의 같은 시각을 맞추는 것이 중요하다.
2단계: 업스트림이 실제로 실행 중인지 확인한다
docker inspect \
--format '{{.Name}} status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' \
'<ACTUAL_UPSTREAM_CONTAINER>'
running은 프로세스가 살아 있다는 뜻이지 HTTP 요청을 정상 처리한다는 보장은 아니다. 이미지가 HEALTHCHECK를 제공하거나 Compose에 healthcheck를 정의했다면 healthy, unhealthy, starting 상태를 함께 본다.
재시작 반복이면 네트워크를 고치기 전에 앱 로그의 원인을 해결한다.
docker inspect \
--format 'restart={{.RestartCount}} exit={{.State.ExitCode}} error={{.State.Error}}' \
'<ACTUAL_UPSTREAM_CONTAINER>'
DB 연결 실패, 잘못된 환경변수, 파일 권한 오류처럼 업스트림 자체가 준비되지 않은 상태라면 NPM 설정만 바꿔도 해결되지 않는다.
3단계: NPM과 업스트림이 같은 네트워크인지 확인한다
두 컨테이너의 네트워크 이름만 제한해서 본다.
docker inspect \
--format '{{.Name}} {{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}' \
'<ACTUAL_NPM_CONTAINER>' '<ACTUAL_UPSTREAM_CONTAINER>'
공통 네트워크가 하나도 없다면 NPM은 업스트림 서비스명을 Docker DNS로 찾을 수 없다. NPM 공식 문서는 같은 Docker 호스트의 업스트림과 사용자 정의 네트워크를 공유하는 구성을 권장한다. 이 방식은 업스트림 포트를 호스트의 모든 인터페이스에 게시할 필요도 줄인다.
NPM과 앱이 서로 다른 Compose 프로젝트라면 Compose 밖에서 수명주기를 관리하는 외부 네트워크를 사용할 수 있다. 아래는 기존 서비스에 공통 네트워크를 추가하는 부분 예시이며 전체 Compose 파일을 대체하는 설정이 아니다. 앱이 이미 연결된 기본·DB 네트워크를 그대로 유지한 상태에서 edge_proxy를 추가한다. 예시의 default도 현재 사용 중인 실제 네트워크 이름에 맞춘다.
services:
app:
image: '<PINNED_APP_IMAGE>'
networks:
- default
- edge_proxy
networks:
default: {}
edge_proxy:
external: true
name: '<ACTUAL_SHARED_NETWORK>'
external: true 네트워크는 미리 존재해야 하며 Compose가 자동 생성하지 않는다. 다음 조회로 실제 공유 네트워크가 있는지 확인한다. 없다면 이름·드라이버·주소 범위가 운영 구성과 맞는지 검토한 뒤 별도로 생성해야 한다.
docker network inspect --format '{{.Name}} driver={{.Driver}}' '<ACTUAL_SHARED_NETWORK>'
NPM 스택의 NPM 서비스도 기존 네트워크를 보존하면서 같은 <ACTUAL_SHARED_NETWORK>에 연결한다. 데이터베이스까지 무조건 이 네트워크에 붙이지 않는다. 공유 네트워크에서 다른 프로젝트와 서비스명·별칭이 겹치지 않는지도 확인한다. NPM·앱·DB 경계를 나누는 전체 구조는 NPM·Ghost·MySQL을 공용망과 내부망으로 나누는 구성에 정리돼 있다.
docker network connect로 실행 중 컨테이너에 임시 연결하면 원인 확인에는 도움이 될 수 있지만, 재생성 뒤 사라질 수 있다. 최종 상태는 Compose 파일을 설정의 기준으로 삼고 검토·적용한다.
4단계: 고정 IP가 아니라 서비스명 DNS를 확인한다
Docker Compose 공식 문서에 따르면 같은 Compose 네트워크의 서비스는 서비스명으로 발견된다. 컨테이너를 재생성하면 IP는 바뀔 수 있지만 서비스명은 유지된다.
따라서 NPM의 Forward Hostname / IP에는 다음처럼 공통 네트워크에서 유일한 서비스명을 사용한다.
ghost
다음 값은 장기 설정으로 피한다.
172.20.0.7
NPM 컨테이너 안에서 이름 해석 여부를 조회할 수 있다.
docker exec '<ACTUAL_NPM_CONTAINER>' \
getent hosts '<ACTUAL_UPSTREAM_SERVICE>'
getent가 이미지에 없다면 “DNS 실패”로 단정하지 않는다. 실행 중인 컨테이너에 임의 패키지를 설치하지 말고, 승인된 진단 이미지나 기존 도구를 공통 네트워크에 붙여 별도로 확인한다.
서비스명이 실제 Compose 서비스 목록과 일치하는지도 본다.
docker compose config --services
서비스명 DNS 문제의 원리와 ENOTFOUND 구분은 Ghost ENOTFOUND db의 서비스명 DNS 진단과 같다. 다만 이번 경로는 Ghost→MySQL이 아니라 NPM→업스트림이다.
5단계: Forward Port는 컨테이너 내부 포트다
Compose 공식 문서는 서비스 간 통신이 HOST_PORT가 아니라 CONTAINER_PORT를 사용한다고 설명한다.
예를 들어 다음 매핑이 있다고 가정한다.
ports:
- "8080:2368"
- 호스트나 외부 클라이언트가 접근할 때:
호스트주소:8080 - 같은 Docker 네트워크의 NPM이 접근할 때:
ghost:2368
NPM의 Forward Port에 8080을 넣는 실수가 흔하다. NPM과 Ghost가 같은 네트워크라면 2368처럼 앱이 컨테이너 안에서 실제로 듣는 포트를 사용한다.
포트 게시를 추가하기 전에 현재 노출을 확인한다.
docker ps --format 'table {{.Names}}\t{{.Ports}}'
502를 해결하려고 업스트림에 0.0.0.0:<PORT>:<PORT>를 무조건 추가하면 인터넷 노출 범위가 커질 수 있다. 같은 사용자 정의 네트워크의 컨테이너끼리는 호스트 게시 포트가 없어도 통신할 수 있다. 외부 노출을 바꾸기 전에는 외부 노출 포트와 방화벽 상태 확인 절차로 범위를 확인한다.
6단계: Scheme이 실제 업스트림 프로토콜과 맞는지 확인한다
브라우저가 NPM에 HTTPS로 연결한다는 사실과 NPM이 업스트림에 HTTPS로 연결해야 한다는 사실은 같지 않다.
일반적인 TLS 종료 구성에서는 브라우저가 HTTPS로 NPM에 연결하고, NPM이 Ghost:2368로 HTTP 요청을 전달한다.
Ghost 공식 문서도 앞단 프록시가 SSL을 종료하고 Ghost로 HTTP 프록시할 수 있다고 설명한다. 이때 X-Forwarded-Proto 헤더가 잘못되면 HTTPS 리다이렉트 루프가 생길 수 있다.
NPM Proxy Host에서 확인할 값은 다음과 같다.
- 업스트림이 평문 HTTP로 듣는다면 Scheme
http - 업스트림 자체가 TLS를 제공하고 해당 포트에서 HTTPS로 듣는다면 Scheme
https - Forward Port는 선택한 Scheme이 실제로 수신되는 포트
HTTP 포트에 https로 연결하거나 HTTPS 포트에 http로 연결하면 TLS handshake 또는 잘못된 응답 관련 오류가 날 수 있다. 인증서 탭에서 외부 HTTPS를 켰다는 이유만으로 업스트림 Scheme까지 https로 바꾸지 않는다.
7단계: 업스트림이 모든 인터페이스에서 듣는지 확인한다
컨테이너 안의 애플리케이션이 127.0.0.1에만 바인딩돼 있으면 같은 컨테이너 내부에서는 열려도 다른 컨테이너의 NPM에서는 접근할 수 없다. 앱의 공식 설정에서 컨테이너 인터페이스에 맞는 수신 주소를 확인한다.
컨테이너 안에서 수신 소켓을 조회할 수 있다면 다음처럼 제한적으로 본다.
docker exec '<ACTUAL_UPSTREAM_CONTAINER>' ss -lnt
ss가 없다면 패키지를 즉석 설치하지 말고 이미지·애플리케이션의 공식 문서와 healthcheck, 로그로 수신 주소를 확인한다.
127.0.0.1을 NPM의 Forward Hostname으로 쓰는 것도 피한다. NPM 컨테이너 안의 127.0.0.1은 NPM 컨테이너 자신을 가리키며, 다른 앱 컨테이너나 호스트를 뜻하지 않는다.
8단계: NPM 설정을 검증하고 가장 작은 범위로 적용한다
NPM 컨테이너의 NGINX 구문을 읽기 전용으로 검사한다.
docker exec '<ACTUAL_NPM_CONTAINER>' nginx -t
사용자 정의 Advanced 설정이나 /data/nginx/custom 파일을 변경한 직후라면 구문 오류가 없는지 확인한다. NPM 공식 문서는 사용자 정의 설정 파일이 NGINX 구성의 여러 위치에 포함될 수 있음을 안내한다.
원인이 확인되지 않은 상태에서 NPM 데이터 디렉터리, 인증서, 데이터베이스를 삭제하거나 스택을 처음부터 만들지 않는다. 먼저 Proxy Host 한 개의 Hostname·Port·Scheme만 실제 값과 맞추고 저장한 뒤 테스트한다.
Compose 네트워크를 수정했다면 변경 내용을 검토하고 해당 프로젝트의 변경 대상 서비스만 명시해 적용한다. 아래 <ACTUAL_CHANGED_SERVICE>는 현재 프로젝트에서 네트워크 설정을 바꾼 서비스다. NPM과 앱 프로젝트를 모두 수정했다면 각각의 프로젝트에서 따로 실행한다.
docker compose config -q
docker compose config --networks
docker compose up -d '<ACTUAL_CHANGED_SERVICE>'
docker compose ps
up -d는 변경된 서비스의 컨테이너를 재생성할 수 있으며 연결이 잠시 끊길 수 있다. 대상 서비스의 의존 서비스가 이미 정상 실행 중이고 그 설정을 바꿀 필요가 없다고 확인한 경우에만 up -d --no-deps '<ACTUAL_CHANGED_SERVICE>'로 의존 서비스 적용을 제외한다. docker compose restart는 Compose의 네트워크·환경변수 변경을 반영하지 않으므로 이 적용 절차를 대신할 수 없다.
변경 뒤 장애가 커지면 기록해 둔 Proxy Host 값 또는 변경 전 Compose 네트워크 선언을 복원하고, 같은 프로젝트 조건에서 설정 검증 후 해당 서비스에 up -d를 다시 적용한다. 롤백에서도 기존 볼륨과 네트워크를 삭제하지 않고 내부·외부 요청을 다시 확인한다.
상태가 있는 DB 볼륨을 포함한 스택에서 down -v를 사용하지 않는다. 502는 볼륨 삭제로 해결할 문제가 아니다.
9단계: 내부와 외부를 따로 재검증한다
먼저 NPM과 같은 네트워크에서 업스트림 응답을 확인한다. 사용할 진단 이미지와 태그는 조직에서 승인한 값으로 고정한다. 다음은 NPM이 외부 HTTPS를 종료하고 Ghost에 HTTP로 전달하는 구성을 모사한 예시다. 실제 외부 도메인을 Host에 넣고 원래 요청이 HTTPS였음을 X-Forwarded-Proto로 알린다.
docker run --rm \
--network '<ACTUAL_SHARED_NETWORK>' \
'<APPROVED_CURL_IMAGE:PINNED_TAG>' \
-sv --max-time 10 \
-H 'Host: <ACTUAL_DOMAIN>' \
-H 'X-Forwarded-Proto: https' \
'http://<ACTUAL_UPSTREAM_SERVICE>:<ACTUAL_CONTAINER_PORT>/'
실제 업스트림이 HTTPS라면 URL의 Scheme·포트와 인증서 검증 대상 이름도 그 구성에 맞춰야 한다. 위 HTTP 예시를 그대로 사용하거나 인증서 검증을 무조건 끄지 않는다.
이름 해석 실패·연결 거부·TLS 실패라면 내부 이름·포트·Scheme·앱 상태를 계속 본다. 반면 301·302·401·403 등 HTTP 상태가 반환됐다면 HTTP 응답을 받은 것이므로 이를 곧바로 네트워크 연결 실패로 보지 않는다. 리디렉션 위치·인증 정책·기대한 콘텐츠까지 확인해 앱 동작의 정상 여부를 별도로 판단한다. 일회성 진단 컨테이너는 NPM과 같은 네트워크에서의 도달성을 확인하지만 NPM의 모든 헤더·Access List·사용자 정의 설정까지 재현하지는 않는다.
내부 응답을 확인한 뒤 외부 도메인에 일반 GET 요청을 보낸다. curl -I는 HEAD 검사이므로 첫 로그 대조에는 사용할 수 있지만 최종 페이지 검증을 대신하지 않는다.
curl -sS -D - -o /dev/null --max-time 10 'https://<ACTUAL_DOMAIN>/'
기대한 상태 코드와 리디렉션을 확인하고 브라우저에서도 실제 페이지·로그인이 정상인지 확인한다.
마지막으로 상태와 최근 로그를 다시 확인한다.
docker compose ps
docker logs --since=10m '<ACTUAL_NPM_CONTAINER>'
docker logs --since=10m '<ACTUAL_UPSTREAM_CONTAINER>'
docker exec '<ACTUAL_NPM_CONTAINER>' \
tail -n 100 '/data/logs/proxy-host-<ACTUAL_PROXY_HOST_ID>_error.log'
성공 기준은 다음과 같다.
- NPM과 업스트림이 의도한 공통 네트워크를 공유한다.
- Forward Hostname이 고정 IP가 아니라 유효한 서비스명이다.
- Forward Port가 컨테이너 내부 수신 포트와 일치한다.
- Scheme과 업스트림 프로토콜이 일치한다.
- 내부 진단 요청과 외부 HTTPS 요청이 모두 정상 응답한다.
- NPM과 업스트림 로그에 새 이름 해석·연결 거부·TLS 오류가 없다.
- 업스트림 포트를 불필요하게 외부에 새로 게시하지 않았다.
연결이 복구된 뒤에는 인증·접근 제어도 유지되는지 확인한다. 관리 화면과 보호 대상의 검증은 Nginx Proxy Manager와 Authelia 접근 제어 절차를 이어서 적용할 수 있다.
증상별 빠른 분기표
| 증상 | 가능성이 높은 계층 | 다음 확인 |
|---|---|---|
| 컨테이너명으로 연결할 때만 502 | Docker DNS·공통 네트워크 | 두 컨테이너 네트워크 목록, 서비스명 |
| 호스트 포트로는 되지만 서비스명으로 실패 | Compose 프로젝트 분리·네트워크 미공유 | 외부 공유 네트워크 선언 |
| 내부 IP로 잠시 되다가 재배포 후 실패 | 동적 컨테이너 IP 사용 | 서비스명으로 교체 |
| HTTP Scheme은 되고 HTTPS Scheme은 실패 | Scheme·TLS·수신 포트 불일치 가능성 | 실제 프로토콜·포트, TLS 오류와 SNI 설정 |
| NPM 로그는 연결 거부 | 앱 미기동·잘못된 포트·로컬 바인딩 | 컨테이너 상태, 로그, 수신 소켓 |
| 특정 Proxy Host만 실패 | 해당 Hostname·Port·Scheme·Advanced 설정 | 다른 정상 Host와 값 비교 |
| 모든 Proxy Host가 동시에 실패 | NPM 자체 상태·공유 네트워크·공통 설정 | NPM 로그, nginx -t, 네트워크 |
로그에 Address family not supported by protocol |
NPM 호스트의 IPv6 지원 문제 가능성 | NPM 공식 IPv6 설정 문서와 실제 호스트 지원 |
마지막 항목은 로그 문구가 정확히 일치할 때만 본다. NPM 공식 문서는 일부 Docker 호스트에서 IPv6가 활성화되지 않은 경우 DISABLE_IPV6: 'true'를 설정하는 방법을 안내한다. 단순 502만 보고 IPv6를 끄지 않는다.
하면 안 되는 응급조치
컨테이너 IP를 Proxy Host에 고정하기
Compose가 컨테이너를 재생성하면 IP가 바뀔 수 있다. Docker 공식 문서도 서비스명 사용을 권장한다. IP 고정은 증상을 잠시 숨길 수 있지만 다음 배포에서 다시 실패할 수 있다.
502를 해결하려고 모든 포트를 공개하기
같은 사용자 정의 네트워크의 컨테이너끼리는 게시 포트가 없어도 통신할 수 있다. ports를 추가하면 호스트·외부 노출 범위가 달라질 수 있다.
NPM 컨테이너 안에 진단 도구를 즉석 설치하기
실행 중 컨테이너를 수동 변경하면 재현성이 떨어지고 재생성 뒤 사라진다. 기존 명령이나 승인된 일회성 진단 컨테이너를 사용한다.
network_mode: host로 문제를 덮기
Docker Compose 공식 문서는 host 모드가 컨테이너를 호스트 네트워크 스택에 직접 연결하며 서비스명 DNS를 사용할 수 없다고 설명한다. 필요한 이유를 입증하지 못한 상태에서 격리 경계를 없애지 않는다.
docker compose down -v 실행하기
-v는 Compose가 관리하는 볼륨과 선언된 익명 볼륨을 제거할 수 있다. NPM·Ghost·데이터베이스 데이터를 잃을 위험이 있으며 502 네트워크 진단과 무관하다.
운영 체크리스트
- [ ] 502 발생 시각과 NPM·업스트림 로그를 같은 시간대로 확인했다.
- [ ] 업스트림 컨테이너가 running이며 재시작 반복이 아니다.
- [ ] NPM과 업스트림이 의도한 사용자 정의 네트워크를 공유한다.
- [ ] Forward Hostname은 공통 네트워크에서 해석되는 서비스명이다.
- [ ] Forward Port는 호스트 게시 포트가 아니라 컨테이너 내부 포트다.
- [ ] Scheme이 실제 업스트림 HTTP·HTTPS와 일치한다.
- [ ] 앱이 컨테이너 외부 인터페이스에서 수신한다.
- [ ]
nginx -t로 NPM 설정 구문을 확인했다. - [ ] 내부 네트워크 요청과 외부 HTTPS 요청을 각각 검증했다.
- [ ] 고정 IP, 무분별한 포트 공개, host 네트워크로 우회하지 않았다.
- [ ] 데이터·인증서·볼륨을 삭제하지 않았다.
- [ ] 임시 조치가 아니라 Compose 파일에 최종 네트워크 상태를 반영했다.
마무리
Nginx Proxy Manager에서 502가 보이면 먼저 해당 Proxy Host의 로그로 요청 도달과 응답 발생 지점을 확인한다. NPM에서 업스트림으로 넘어가는 구간의 문제라면 이름, 네트워크, 포트, Scheme 또는 애플리케이션 상태를 차례로 분리할 수 있다.
안전한 해결 순서는 다음과 같다.
- 같은 시각의 NPM·업스트림 로그를 본다.
- 업스트림이 실제로 실행되고 요청을 받을 준비가 됐는지 확인한다.
- 공통 Docker 네트워크와 서비스명 DNS를 확인한다.
- Forward Port를 컨테이너 내부 포트와 맞춘다.
- 외부 HTTPS와 업스트림 Scheme을 분리해 확인한다.
- 가장 작은 설정만 변경한다.
- 내부 연결과 외부 도메인을 모두 재검증한다.
이 순서를 지키면 컨테이너 IP 고정이나 포트 전면 공개로 증상을 가리는 대신, 다음 재배포에도 유지되는 Compose 기반 구성으로 복구할 수 있다.
공식 문서
- Nginx Proxy Manager 고급 설정 — 사용자 정의 Docker 네트워크
- Docker Compose 네트워킹과 서비스명 DNS
- Docker 사용자 정의 bridge 네트워크
- Docker Compose 네트워크 정의
- NGINX Reverse Proxy
- Ghost Reverse Proxying
- NPM 공식 Proxy Host 템플릿 — access·error 로그 경로
- Docker 컨테이너 로그와 파일 로그의 차이
- Docker Compose up — 변경된 서비스 재생성
- Docker Compose restart — 설정 변경 미반영