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

빌드노트

Compose에 ports가 있는데 compose run으로 띄우면 접속되지 않는다

compose run은 서비스의 ports를 기본으로 생략한다. 실제 PortBindings와 내부·호스트 HTTP 응답을 비교하고, 호스트 접속이 필요한 작업에서만 --service-ports 또는 명시적 -p를 선택하는 방법을 설명한다.

Compose 파일에 ports가 있는데 docker compose run으로 만든 컨테이너에 호스트에서 접속되지 않는다면, 그 컨테이너의 실제 포트 매핑부터 확인한다. compose run은 서비스에 선언한 ports를 기본으로 적용하지 않는다. 서버가 컨테이너 안에서 정상 실행 중이어도 호스트 포트는 열리지 않을 수 있다.

run은 서비스 설정을 바탕으로 일회성 작업용 새 컨테이너를 만든다. Docker의 Compose run 문서 (새 탭에서 열림)는 이미 사용 중인 포트와 충돌하지 않도록 서비스 포트를 생략한다고 설명한다. 호스트에서 접근할 필요가 있는 작업이라면 생성할 때 --service-ports 또는 -p를 선택한다.

Compose 파일보다 생성된 컨테이너를 먼저 본다

아래는 PowerShell 7에서 실행하는 조회다. $container에 문제가 생긴 one-off 컨테이너의 실제 ID나 이름을 넣는다. run -d가 반환한 ID나 docker ps -a에서 해당 실행을 찾아 사용한다. 같은 서비스의 기존 up 컨테이너를 조회하면 다른 결과를 보고 판단하게 된다.

$container = '진단할-컨테이너-ID'
docker inspect --type container --format '{{.State.Status}}' $container
docker inspect --type container --format '{{json .HostConfig.PortBindings}}' $container
docker inspect --type container --format '{{json .NetworkSettings.Ports}}' $container
docker port $container

이번 비교 실험에서 기본 run 컨테이너는 차례로 running, {}, {}를 출력했고, docker port에는 출력이 없었다. 컨테이너는 실행 중이지만 호스트에 연결한 포트는 없었다. HostConfig.PortBindings에서 생성 시 바인딩을, NetworkSettings.Ports와 docker port에서 실행 중 매핑을 함께 확인한 것이다. inspect의 JSON 출력 (새 탭에서 열림), docker port의 조회 대상 (새 탭에서 열림)

반대로 HostIp와 HostPort가 들어 있는 매핑이 나온다면, 포트 생략만으로 접속 실패를 설명할 수 없다. 서버가 준비됐는지, 실제 수신 포트가 매핑 대상과 같은지, 요청이 출력에 나온 호스트 주소·포트를 향하는지 확인한다. No such object는 바인딩이 비었다는 뜻이 아니라 조회할 컨테이너를 찾지 못했다는 오류다.

같은 서버인데 호스트 요청만 달라졌다

Windows 11, PowerShell 7.6.6, Docker Desktop 4.92.0의 Linux Engine 29.8.0, Compose v5.5.1에서 별도 프로젝트로 비교했다. 기존 로컬 python:3.12-alpine 이미지를 사용했고, 컨테이너의 Python은 3.12.14였다. 실제 업무 장애 기록이 아니라 이 동작을 확인하기 위한 실습이다.

probe 서비스의 서버는 모든 비교에서 컨테이너 안 0.0.0.0:8000으로 수신했다. 서비스에 선언한 포트 부분은 다음과 같다. 기존 서비스에 적용한다면 ports 아래 값을 자신의 호스트·컨테이너 포트로 바꾼다. 이것만으로 실행 가능한 Compose 파일 전체가 되는 것은 아니다.

ports:
  - "127.0.0.1:27690:8000"

27690은 호스트 포트, 8000은 컨테이너 안 서버 포트다. 앞의 127.0.0.1은 호스트의 loopback 공개 주소다. 서버의 컨테이너 내부 수신 주소와는 설정 위치가 다르다. Compose ports 문법 (새 탭에서 열림)

기준값인 up 컨테이너를 먼저 확인한 뒤 제거하고, 같은 서비스를 세 가지 run 방식으로 순차 생성했다. 매번 앞 컨테이너를 정리해 호스트 포트를 함께 점유하지 않게 했다. 네 컨테이너 모두 내부의 127.0.0.1:8000/inside 요청에는 200 compose-run-probe를 반환했다.

  • up: 8000/tcp가 127.0.0.1:27690에 매핑됐다. 호스트 요청도 200이었고 서버에 /from-host 수신 로그가 남았다.
  • 기본 run: PortBindings는 {}였다. 호스트의 127.0.0.1:27690 요청은 실패했고 /from-host 수신 로그도 없었다.
  • run --service-ports: up과 같은 127.0.0.1:27690 매핑이 생겼다. 호스트 요청은 200이었고 수신 로그도 남았다.
  • run -p 127.0.0.1:27691:8000: 명시한 27691만 매핑됐다. 서비스에 적힌 27690 매핑이 함께 생기지 않았다. 27691 요청은 200이었고 수신 로그도 남았다.

이 비교에서는 서버 명령과 내부 수신 주소를 바꾸지 않았다. 내부 응답이 가능한 상태에서 포트 공개 옵션을 바꾸자 호스트 매핑과 수신 결과가 함께 달라졌다. 따라서 이 관찰은 서버의 listen 주소 오류와 구별된다.

호스트 접속이 필요한 작업에서만 매핑을 선택한다

서비스의 포트 설정을 그대로 쓸 때는 --service-ports를 붙인다. 아래 명령은 자신의 Compose 프로젝트 디렉터리에서 실행하며, probe를 해당 서비스 이름으로 바꾼다. 평소 -f나 프로젝트 이름 옵션을 썼다면 동일하게 유지한다. 서비스의 기본 명령이 실행할 작업이라는 전제이며, -d는 새 컨테이너를 백그라운드에서 실행하고 ID를 반환한다.

docker compose run -d --no-deps --service-ports probe

이 옵션은 서비스의 포트 설정 전체를 사용한다. 위 실습의 PortBindings에는 {"8000/tcp":[{"HostIp":"127.0.0.1","HostPort":"27690"}]}가 나왔다. 기존 서비스가 그 호스트 포트를 사용 중이라면 포트를 그대로 재사용하려 하지 말고, 별도 포트가 필요한지 판단한다. 이 충돌 가능성은 공식 문서의 설명이며, 이번 실습에서는 동시 점유 오류를 재현하지 않았다.

필요한 포트 하나를 다른 호스트 포트로 연결하려면 --service-ports 대신 명시적 -p를 선택한다. 다음은 위 명령의 대안이며 함께 실행할 필요는 없다. 27691이 비어 있고 작업 프로세스가 컨테이너의 8000에서 수신한다는 조건이다.

docker compose run -d --no-deps -p 127.0.0.1:27691:8000 probe

여기서 -p는 run 뒤에 오는 포트 공개 옵션이다. 127.0.0.1을 생략하면 공개 범위가 넓어질 수 있으므로 로컬 확인에는 주소까지 적는다. --no-deps는 의존 서비스를 함께 시작하지 않게 하는 옵션이지 포트를 공개하는 옵션은 아니다. 두 생성 방식 모두 Compose run 공식 옵션 (새 탭에서 열림)에 설명돼 있다.

생성 뒤에는 반환된 ID로 앞의 조회를 다시 실행한다. 이어서 자신이 선택한 호스트 포트로 요청을 보내 실제 응답도 확인한다. 다음은 curl.exe가 있는 Windows 호스트에서 프록시를 우회해 실습 서버의 /from-host로 요청하는 명령이다. 실제 앱에서는 경로도 유효한 확인 주소로 바꾼다.

$port = 27691
curl.exe --noproxy '*' --connect-timeout 1 --max-time 2 -sS -i "http://127.0.0.1:$port/from-host"
$curlExit = $LASTEXITCODE
Write-Output "curl_exit=$curlExit"

명시적 -p를 쓴 실습에서는 HTTP/1.0 200 OK, 본문 compose-run-probe, curl_exit=0이 나왔다. 기본 run의 27690 요청은 curl: (28) Connection timed out after 1011 milliseconds와 curl_exit=28로 끝났다. 오류 문구와 대기 시간은 환경에 따라 달라질 수 있다. curl 오류 번호 하나로 매핑 누락을 단정하지 않고, 바인딩 조회와 서버의 실제 수신 여부를 함께 본다.

DB 마이그레이션이나 배치처럼 호스트에서 들어올 요청이 필요 없는 작업에는 포트를 추가할 이유가 없다. 위 두 명령은 필요한 매핑을 가진 새 one-off 컨테이너를 만든다. 작업이 끝난 컨테이너는 반환된 ID를 대상으로 docker stop 후 docker rm으로 정리한다.

직접 확인한 범위는 위 버전의 Linux bridge 네트워크, 단일 TCP 포트와 호스트 loopback HTTP 요청이다. IPv6, host 네트워크, 원격 Docker 엔진, 다른 컴퓨터에서의 접속과 여러 서비스 포트의 동시 공개는 검증하지 않았다.

← 전체 글 보기