MYMONDAY.ai
서비스스튜디오도구데이터·포맷73CSS 생성기19색상·이미지10인코딩·보안10웹·SEO11개발·데브옵스10파일·내보내기18계산기6가이드

7분 읽기 · 마지막 업데이트

Docker Compose 포트 매핑 완전 정리: 문법, 충돌 조건, "port is already allocated" 해결

Compose의 포트 매핑은 호스트의 소켓 하나를 점유하겠다는 선언입니다. 어떤 선언끼리 겹치는지 정확히 알면 docker compose up이 왜 실패하는지, 어느 줄을 고쳐야 하는지가 보입니다.

포트 매핑이 실제로 하는 일

컨테이너는 자기만의 네트워크 네임스페이스를 가집니다. 컨테이너 안에서 80번 포트를 연 프로세스는 publish하기 전까지 호스트에서 보이지 않습니다. publish는 Docker가 호스트에 소켓을 열고 그 트래픽을 컨테이너 포트로 전달하게 만드는 일이고, 서비스 아래의 ports:가 그 요청서입니다. 항목은 항상 두 쪽으로 이루어집니다. 호스트 쪽은 내 컴퓨터에서 비어 있어야 하고, 컨테이너 쪽은 그 컨테이너 안에서만 비어 있으면 됩니다.

충돌의 대부분은 이 비대칭에서 나옵니다. 두 서비스가 내부에서 똑같이 3000번을 써도 각자 네임스페이스가 다르니 문제가 없습니다. 하지만 둘 다 호스트 8080으로 publish하면 호스트에 같은 주소·프로토콜·포트로 묶인 소켓이 둘 필요해지고, 운영체제는 이를 허용하지 않습니다. 그래서 충돌을 찾을 때는 각 매핑의 왼쪽 값만 보면 됩니다.

짧은 표기: 만나게 되는 형태 전부

짧은 표기는 [호스트IP:][호스트포트:]컨테이너포트[/프로토콜] 형태의 문자열이고, 각 포트 자리에는 범위가 올 수 있습니다. 프로토콜을 생략하면 tcp입니다. IPv6 주소는 대괄호로 감쌉니다. 표기별 의미는 아래와 같습니다.

항목호스트 쪽컨테이너 쪽비고
"8080:80"모든 주소의 TCP 808080가장 흔한 형태
"127.0.0.1:8080:80"루프백만, TCP 808080다른 기기에서는 접속 불가
"8080-8081:80-81"TCP 8080, 808180, 81양쪽 범위 길이가 같아야 하며 순서대로 대응
"80"비어 있는 임의의 호스트 포트80충돌하지 않음. docker compose port로 확인
"127.0.0.1::80"루프백의 임의 포트80호스트 포트를 비우면 Docker가 고름
"53:53/udp"모든 주소의 UDP 5353UDP와 TCP는 별개의 소켓
"[::1]:8080:80"IPv6 루프백, TCP 808080IPv6는 대괄호 필수

긴 표기: 같은 내용을 필드로 풀어 쓰기

Compose 명세는 각 포트를 이름 있는 필드의 맵으로도 쓸 수 있게 합니다. 더 길지만 모호함이 없습니다. 필드는 target(컨테이너 포트), published(호스트 포트 또는 범위), host_ip, protocol(이 검사기에서는 tcp, udp, sctp), mode, name입니다.

mode는 host와 ingress 중 하나입니다. Docker Swarm에서 의미가 있는 값으로, ingress는 클러스터 라우팅 메시를 통해 publish하고 host는 태스크가 도는 노드에서만 엽니다. 한 대에서 docker compose up을 쓸 때는 신경 쓰지 않아도 됩니다. 프로토콜과 주소를 함께 지정할 때 콜론 개수를 세지 않아도 된다는 점이 긴 표기의 실용적인 장점입니다.

ports:
  - target: 53
    published: "5353"
    host_ip: 127.0.0.1
    protocol: udp
# 짧은 표기 "127.0.0.1:5353:53/udp"와 같은 뜻

두 매핑이 실제로 충돌하는 조건

운영체제는 주소 체계, 바인드 주소, 프로토콜, 포트의 조합마다 리스닝 소켓을 하나만 허용합니다. 따라서 두 매핑이 충돌하려면 프로토콜이 같고, 호스트 포트 범위가 겹치고, 바인드 주소가 겹쳐야 합니다. 호스트 IP를 생략한 매핑은 모든 주소에 묶이므로 같은 체계의 어떤 특정 주소와도 겹칩니다. 127.0.0.1과 127.0.0.2는 서로 다른 주소이므로 같은 포트여도 겹치지 않습니다.

이 사이트의 Docker Compose 검사기가 구현한 판정이 바로 이것입니다. 아래는 규칙마다 하나씩 들어 있는 작은 파일이고, 도구의 분석기로 돌리면 충돌 세 건만 나옵니다. web과 api의 8080/tcp, web과 dns의 53/udp, dns의 범위 8000-8001과 cache의 8001:6379가 겹치는 8001/tcp입니다.

충돌이 아니라 오류로 보고하는 표기도 있습니다. YAML 숫자로 쓴 포트 - 3000은 "3000"과 같은 뜻으로, 호스트 포트를 임의로 받으므로 세지 않습니다. 1~65535를 벗어난 숫자(0, 70000), "8080-8081:80-82"처럼 길이가 다른 두 범위, "9100:80-81"처럼 컨테이너 범위 앞의 호스트 포트 하나, 거꾸로 쓴 범위는 오류입니다. 호스트 범위와 컨테이너 포트 하나를 연결한 "8080-8090:80"은 Compose가 범위에서 사용 가능한 호스트 포트 하나를 고릅니다. 다른 고정 포트나 범위와 겹치면 검사기는 확정 충돌이 아니라 충돌 가능성 경고를 냅니다.

  • 8080/tcp: web과 api가 모든 주소의 TCP 8080을 함께 요구합니다. 충돌로 보고됩니다.
  • 53/udp: web과 dns가 UDP 53을 함께 요구합니다. 충돌입니다. api의 "53:53"은 TCP라서 어느 쪽과도 겹치지 않습니다.
  • 8001/tcp: dns의 범위 8000-8001이 cache의 8001과 겹칩니다. 8000만 썼다면 괜찮았을 것이 범위 때문에 걸립니다.
  • 9090: web은 127.0.0.1, api는 127.0.0.2를 씁니다. 주소가 달라 충돌이 아닙니다.
  • api의 긴 표기 8080/udp는 TCP 8080과 무관합니다. 충돌이 아닙니다.
  • cache의 "6379"는 호스트 포트가 없어 시작할 때 Docker가 고릅니다. 충돌이 아닙니다.
services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"
      - "127.0.0.1:9090:90"
      - "53:53/udp"
  api:
    build: .
    ports:
      - "8080:3000"
      - "127.0.0.2:9090:9000"
      - "53:53"
      - target: 3000
        published: 8080
        protocol: udp
  dns:
    image: coredns/coredns:1.11.1
    ports:
      - "53:53/udp"
      - "8000-8001:8000-8001"
  cache:
    image: redis:7
    ports:
      - "8001:6379"
      - "6379"

ports와 expose의 차이

expose는 같은 Compose 네트워크의 다른 서비스가 접근할 수 있는 컨테이너 포트를 적는 키입니다. 호스트로는 아무것도 열지 않으므로 호스트 충돌을 일으키지 않습니다. 사용자 정의 네트워크 안에서는 서비스끼리 프로세스가 리스닝하는 어떤 포트로도 이미 접근할 수 있어서, expose는 대체로 문서 역할에 가깝습니다. 브라우저에서 접속하게 만들어 주지도 않습니다.

실제 사고는 반대 방향에서 납니다. 같은 프로젝트의 API 컨테이너만 쓰는 데이터베이스에 튜토리얼에서 복사한 ports:가 붙어 호스트 포트를 놓고 다투는 경우입니다. 이런 데이터베이스는 ports:를 지우고 db:5432처럼 서비스 이름과 컨테이너 포트로 연결하세요. 내 컴퓨터의 SQL 클라이언트가 접속해야 할 때만 호스트로 열고, 그때도 127.0.0.1에 묶는 편이 안전합니다.

키다른 서비스에서 접근호스트에서 접근호스트 충돌 가능
ports가능가능가능
expose가능 (문서 역할)불가없음
둘 다 없음리스닝 중인 모든 포트로 가능불가없음

따옴표 없는 22:22의 함정

YAML 1.1에는 시간·각도용 60진수 숫자 형식이 있습니다. 22:22는 22 x 60 + 22 = 1342인 정수로 읽힙니다. 따옴표 없이 쓴 - 22:22는 YAML 1.1 파서에게 유효한 60진수이고, 그 결과 서비스의 컨테이너 포트가 엉뚱한 값이 됩니다. 두 숫자가 모두 60 미만일 때만 걸리므로 - 80:80은 문자열로 남지만 - 22:22와 - 59:59는 숫자가 됩니다.

이 사이트가 쓰는 yaml 라이브러리 2.9.0으로 직접 확인했습니다. 기본값인 YAML 1.2 모드에서는 60진수 형식이 사라져서 "22:22", "80:80", "59:59"가 모두 문자열로 나옵니다. YAML 1.1 모드로 바꾸면 첫째 항목은 1342, 셋째는 3599라는 숫자가 되고 둘째 "80:80"만 문자열로 남습니다. 즉 YAML 1.2로 읽는 이 사이트의 검사기는 이 문제를 경고하지 않지만, 작업 흐름 어딘가의 오래되었거나 엄격한 파서가 값을 조용히 바꿀 수 있습니다. Compose 문서가 포트 매핑을 따옴표로 감싸라고 권하는 이유가 이것입니다.

ports:
  - 22:22        # YAML 1.1에서는 정수 1342
  - "22:22"      # 항상 문자열, 의도한 값

여러 파일과 override 병합

-f를 여러 번 주면 Compose가 파일을 병합하고, compose.override.yaml이 있으면 compose.yaml과 자동으로 병합합니다. 대부분의 키는 뒤 파일이 이기지만 ports 같은 시퀀스는 Compose 명세의 병합 규칙에 따라 이어 붙습니다. 기본 파일에 "8080:80", override에 "9090:80"이 있으면 서비스는 두 포트를 모두 publish합니다. override가 매핑을 교체한다고 기대했다가 옛 포트가 계속 점유되어 당황하는 경우가 여기서 나옵니다.

이어 붙이지 않고 교체하려면 최근 Compose v2에서 키에 !override나 !reset YAML 태그를 쓸 수 있습니다. 버전과 상관없이 가장 확실한 방법은 Compose에게 직접 묻는 것입니다. docker compose -f compose.yaml -f compose.dev.yaml config는 병합과 변수 치환이 끝난 모델을 출력하고, 서비스별 최종 ports도 거기에 나옵니다. 이 사이트의 검사기는 붙여넣은 파일 하나만 분석하고 여러 파일을 병합하지 않으므로, 병합 결과를 검사하려면 config 출력을 붙여 넣으세요.

변수도 한 겹 더 얹습니다. "${API_PORT:-9000}:9000"는 기본값이 있어 검사기가 해석하지만, 기본값 없는 ${API_PORT}는 환경이나 .env 파일에 달려 있어 텍스트만으로는 알 수 없습니다. 도구는 이런 항목을 추측하지 않고 해석 불가로 알려 줍니다.

docker compose up에서 만나는 오류

실패는 두 종류이고 문구가 포트의 주인을 알려 줍니다. Bind for 0.0.0.0:8080 failed: port is already allocated는 Docker가 이미 그 포트를 쥐고 있다는 뜻입니다. 이 프로젝트나 다른 프로젝트의 실행 중 컨테이너, 혹은 남아 있는 컨테이너인 경우가 많습니다. listen tcp4 0.0.0.0:8080: bind: address already in use는 Docker 밖의 호스트 프로세스가 쥐고 있다는 뜻입니다. 같은 파일 안의 두 서비스가 같은 호스트 포트를 쓰면 같은 오류가 납니다. 첫 컨테이너는 뜨고 두 번째가 오류를 냅니다.

Windows에서는 비슷해 보이는 세 번째 메시지 An attempt was made to access a socket in a way forbidden by its access permissions가 있습니다. 프로그램이 쓰는 것이 아니라 시스템이 예약한 포트 범위에 걸린 경우가 많고, netsh interface ipv4 show excludedportrange protocol=tcp로 범위를 볼 수 있습니다. 스케일링도 원인입니다. web에 고정 호스트 포트가 있는데 docker compose up --scale web=3을 하면 컨테이너 셋이 호스트 포트 하나를 나눌 수 없어 실패합니다. 호스트 포트 범위를 쓰거나 호스트 포트를 없애고 앞단에 리버스 프록시를 두세요.

포트를 쓰는 프로세스 찾기

가장 흔한 주인은 다른 컨테이너이므로 Docker부터 보고, 그다음에 운영체제에 물어봅니다. 8080은 자기 포트로 바꾸세요.

환경명령읽을 곳
Dockerdocker ps --filter publish=8080해당 포트를 publish하는 컨테이너
Linuxss -ltnp 'sport = :8080'LISTEN 상태와 프로세스 이름·PID (다른 사용자의 프로세스는 sudo 필요)
macOS, Linuxlsof -nP -iTCP:8080 -sTCP:LISTEN리스너의 COMMAND와 PID
Windowsnetstat -ano | findstr :8080마지막 열이 PID. 이어서 tasklist /FI "PID eq 1234"
UDP, Linuxss -lunp 'sport = :53'-t 대신 -u로 UDP 소켓 조회

충돌을 피하는 습관

사람이나 Docker 밖의 도구가 접근해야 하는 것만 publish하고, 개발용 서비스는 127.0.0.1에 묶어 네트워크에 노출되지 않게 하세요. 프로젝트마다 호스트 포트 대역을 정해 문서에 적어 두고, 스택을 올리기 전에 모든 ports: 줄의 왼쪽 값을 읽고, 항목은 따옴표로 감싸 둡니다.

파일을 Docker Compose 검사기에 붙여 넣으면 publish되는 모든 호스트 포트가 충돌 표시와 함께 한 표에 나옵니다. 그다음 docker compose config로 병합 결과를 확인하세요. 이 검사는 파일 안의 서비스끼리의 충돌을 다룹니다. 내 노트북이 이미 5432에서 데이터베이스를 돌리고 있는지는 알 수 없고, 앞 절의 명령이 그 용도입니다.

직접 해보기