본문 바로가기
빌드노트 검색

빌드노트

Docker 포트를 publish했는데 서버가 응답하지 않는다

호스트에 포트를 publish해도 서버가 컨테이너의 127.0.0.1에서만 들으면 요청을 받지 못한다. 같은 호스트 포트에서 bind 주소만 바꿔 비교하고, 포트 매핑과 실제 listen 주소를 구분하는 확인 방법을 설명한다.

Docker로 포트를 publish했는데 호스트에서 서버에 접속되지 않는다면, 컨테이너 안 서버가 어느 주소에서 요청을 받는지 확인해야 한다. 컨테이너가 실행 중이고 내부 요청에 200을 반환해도, 서버가 컨테이너의 127.0.0.1에서만 듣고 있으면 호스트의 publish 포트를 통해 들어온 요청을 받지 못한다.

예를 들어 -p 127.0.0.1:28995:8000에서 첫 주소는 호스트 쪽 공개 주소다. 호스트의 loopback 28995 포트를 컨테이너의 8000 포트에 연결한다. 서버 실행 옵션의 --bind 127.0.0.1은 컨테이너 안에서 자기 loopback에만 수신 소켓을 여는 설정이다. 같은 숫자여도 설정하는 위치가 다르다. Docker의 포트 공개 설명 (새 탭에서 열림)

일반적인 bridge 포트 매핑은 컨테이너의 네트워크 주소로 연결을 전달한다. 컨테이너 안 loopback에서만 듣는 프로세스는 그 인터페이스 주소로 들어온 연결을 받지 않는다. publish 설정이 프로세스의 listen 주소까지 바꿔 주지는 않는다. 컨테이너가 자체 인터페이스와 IP를 가진다는 조건은 Docker 네트워킹 문서 (새 탭에서 열림)에서도 확인할 수 있다.

내부 요청은 둘 다 성공했지만 호스트 요청은 달랐다

이 차이를 확인하려고 합성 파일 하나를 제공하는 전용 이미지를 만들었다. Windows의 Docker Desktop 4.92.0, Linux 컨테이너용 Engine 29.8.0, Python 3.12.14 환경이다. 전용 bridge에서 같은 이미지를 순서대로 실행했고, 호스트 매핑은 두 번 모두 127.0.0.1:28995:8000으로 유지했다. 서버 프로세스의 인자는 다음처럼 bind 주소만 바꿨다.

python -u -m http.server 8000 --bind 127.0.0.1
python -u -m http.server 8000 --bind 0.0.0.0

--bind는 Python 서버의 수신 주소를 정한다. 여기서 0.0.0.0은 컨테이너의 모든 IPv4 인터페이스에서 받겠다는 설정이다. 이 HTTP 서버는 비교용으로만 사용했다. Python http.server의 bind 옵션 (새 탭에서 열림)

각 서버가 내부 HTTP 요청에 응답하기 시작한 뒤 다음 결과를 얻었다. 두 컨테이너의 상태는 모두 running이었다.

  • 127.0.0.1 bind: 컨테이너 안의 127.0.0.1:8000 요청은 200이었다. 같은 컨테이너 안에서 자기 bridge 주소인 172.27.0.2:8000으로 요청하면 ConnectionRefusedError: [Errno 111] Connection refused가 나왔다. 호스트의 127.0.0.1:28995 요청은 세 번 모두 curl 종료 코드 52로 끝났다.
  • 0.0.0.0 bind: 컨테이너 안의 loopback과 bridge 주소 모두 200이었다. 호스트의 같은 127.0.0.1:28995 요청도 세 번 모두 200과 본문 published-bind-lab을 반환했다.

컨테이너의 TCP listen 기록도 각각 127.0.0.1:8000, 0.0.0.0:8000이었다. 172.27.0.2는 이 실행에서 배정된 주소이며, 첫 컨테이너를 제거한 뒤 다음 컨테이너에 같은 주소가 배정됐다. 이 IP를 설정에 고정할 필요는 없다.

컨테이너 안에서 localhost 요청이 성공했다는 사실만으로 publish 경로까지 정상이라고 판단할 수 없다. 이 비교에서는 서버가 살아 있다는 점과 호스트 매핑이 같다는 점을 확인한 뒤, 수신 인터페이스를 바꿨을 때 호스트 응답도 바뀌었다.

내 컨테이너에서는 매핑과 서버 로그를 함께 본다

아래 명령은 Windows PowerShell과 Docker CLI를 사용한다. $server에는 진단할 서버 컨테이너의 실제 이름을 넣는다. 8000도 그 서버의 컨테이너 내부 포트로 바꾼다.

$server = '진단할-컨테이너'
docker inspect --format '{{.State.Status}}' $server
docker port $server 8000/tcp
docker logs --tail 5 $server

실험의 문제 조건에서는 차례로 running, 127.0.0.1:28995, 그리고 Serving HTTP on 127.0.0.1 port 8000 ...이 나왔다. 수정 조건에서는 매핑이 그대로인 채 서버 로그의 주소가 0.0.0.0으로 바뀌었다. docker port 명령 (새 탭에서 열림)은 포트 매핑을 보여 준다. 그 출력 자체가 HTTP 응답 성공을 뜻하지는 않는다. 서버가 시작 주소를 로그에 남기지 않거나 오래전에 남겼다면, 해당 서버의 시작 설정이나 사용 가능한 소켓 조회 도구에서 실제 listen 주소를 확인한다.

호스트 요청은 docker port에 나온 호스트 포트로 보낸다. 다음은 위 실험의 포트에 요청한 명령이다. 자신의 출력이 다르면 $port를 바꾼다. curl.exe가 필요하며, 프록시를 우회하고 요청 전체를 3초로 제한한다.

$port = 28995
curl.exe --noproxy '*' --connect-timeout 2 --max-time 3 -sS -i "http://127.0.0.1:$port/"
Write-Output "curl_exit=$LASTEXITCODE"

문제 조건의 오류 원문은 curl: (52) Empty reply from server였고 HTTP 상태 코드는 없었다. 수정 뒤에는 HTTP/1.0 200 OK와 합성 본문이 왔으며 curl_exit=0이었다. 오류 문구는 Docker와 호스트 환경에 따라 달라질 수 있다. curl 52라는 숫자 하나만으로 bind 문제를 확정하지 않는다.

컨테이너에 Python 3가 있다면 내부 요청도 같은 경로로 확인할 수 있다. 이때 8000은 컨테이너 내부 포트다. 실제 앱의 응답 경로가 다르면 /도 바꾼다.

docker exec $server python -c "import http.client; c=http.client.HTTPConnection('127.0.0.1',8000,timeout=2); c.request('GET','/'); r=c.getresponse(); print(r.status, r.read(256).decode().strip()); c.close()"

실험에서는 두 조건 모두 200 published-bind-lab이었다. 내부 요청도 실패한다면 서버의 시작 상태와 내부 포트부터 확인해야 한다. Python 실행 파일을 찾지 못하는 오류는 진단 도구가 없다는 뜻이므로 연결 실패의 증거로 세지 않는다.

고칠 곳은 서버의 수신 설정이다

호스트 매핑은 맞고 서버 로그가 컨테이너 안 127.0.0.1만 가리킨다면, 컨테이너의 네트워크 인터페이스에서도 요청을 받도록 서버의 listen 또는 bind 설정을 바꾼다. 위 Python 비교에서는 두 번째 명령의 --bind 0.0.0.0이 그 변경이었다. 실제 앱은 해당 서버가 제공하는 설정을 적용하고, 다시 뜬 프로세스의 주소와 호스트 HTTP 응답을 확인한다.

호스트 쪽 공개 범위는 계속 -p 127.0.0.1:28995:8000으로 둘 수 있다. 컨테이너 안의 0.0.0.0은 서버 수신 설정에 쓰고, 호스트의 접속 URL에는 http://127.0.0.1:28995를 쓴다. 수신 범위가 넓어지면 같은 bridge의 다른 컨테이너에서도 그 서버 주소로 접근할 수 있으므로, 호스트 loopback 공개와 컨테이너 간 접근 범위는 따로 판단한다.

이미 컨테이너 인터페이스에서 듣고 있다면 이 원인을 그대로 적용하지 않는다. 컨테이너 내부 포트와 publish 대상 포트가 같은지, 서버가 준비됐는지, 호스트 요청이 실제로 그 매핑을 향하는지 확인한다. 실제 실험에서도 시작 직후에는 내부 요청이 연결 거절됐고, 내부 HTTP 준비를 확인한 뒤에 두 조건을 비교했다.

이번 관찰은 Linux 컨테이너의 IPv4 bridge와 Windows Docker Desktop의 호스트 공개 경로까지다. 패킷 캡처, IPv6, host 모드, 네트워크 공간 공유, 다른 컴퓨터에서의 접근은 확인하지 않았다. 실제 업무 서버의 인증이나 응답 내용도 검증 범위에 포함하지 않았다.

← 전체 글 보기