SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-10-13

Docker Compose 네트워크 설정 및 서비스 통신 완벽 가이드

Docker Compose의 기본 브리지 네트워크와 서비스 이름 기반 DNS 통신 원리를 설명합니다. 프로젝트 간 네트워크 공유 방법, 호스트 모드 사용 시점, 그리고 UFW 방화벽을 우회하는 포트 노출 문제 등 실무에서 반드시 알아야 할 핵심 네트워크 구조를 정리했습니다.

애플리케이션 시작 전 Docker Compose가 빌드하는 것

Docker Compose 네트워킹은 한 가지 규칙으로 시작합니다. docker compose up은 프로젝트를 위한 전용 네트워크를 생성하고, 모든 서비스를 이 네트워크에 연결하며, 서비스 이름으로 서로 통신할 수 있게 합니다. 이를 위해 networks: 줄을 작성할 필요는 없습니다. Compose 네트워킹과 관련한 혼란의 대부분은 이 기본 설정이 이미 존재한다는 사실을 알지 못하는 데서 비롯됩니다.

다음은 작은 파일 예시입니다. 이 내용을 shop이라는 디렉터리에 compose.yaml라는 이름으로 저장하십시오.

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example

서비스를 실행하고 Docker가 무엇을 생성했는지 확인하십시오:

docker compose up -d
docker network ls

목록에 shop_default이라는 네트워크가 포함되어 있습니다. Compose는 이를 <project>_default로 명명하며, 프로젝트 이름은 기본적으로 소문자로 된 디렉터리 이름을 사용합니다. docker compose -p myproject up -d를 사용하거나 파일 최상위 수준의 name: myproject을 통해 이를 재정의할 수 있습니다. 네트워크 드라이버는 호스트 내부의 가상 스위치인 bridge입니다. 각 컨테이너는 전용 서브넷의 주소를 할당받으며, 외부로 나가는 트래픽은 호스트 주소로 변환(NAT)됩니다.

docker compose down는 해당 네트워크를 다시 삭제합니다. 이전 프로젝트의 오래된 컨테이너가 네트워크를 점유하고 있으면 Docker가 error while removing network: network shop_default has active endpoints 오류를 반환하며, 이 경우 네트워크에 연결된 컨테이너를 중지하거나 제거해야 문제가 해결됩니다.

Compose가 처음이라면 Compose 파일 구조 및 생명주기 명령어를 먼저 읽어보는 것이 좋습니다. 아래의 모든 내용은 프로젝트를 시작하고 중지할 수 있다는 전제하에 작성되었습니다.

서비스 이름으로 DNS를 확인하는 것은 초보자가 자주 놓치는 부분입니다

사용자 정의 네트워크에서 Docker는 각 컨테이너가 127.0.0.11에서 접근할 수 있는 내장 DNS 서버를 실행합니다. 이 서버는 서비스 이름을 현재 컨테이너 주소로 해석합니다. 따라서 web은 별도의 설정 없이도 호스트 이름 db, 포트 5432를 통해 데이터베이스에 도달할 수 있습니다.

docker compose exec web getent hosts db

이 명령을 실행하면 172.18.0.2 db과 같은 줄이 출력됩니다. 아무것도 출력되지 않는다면 두 서비스가 같은 네트워크에 있지 않은 것입니다.

거의 모든 사용자가 한 번쯤 저지르는 실수는 애플리케이션 설정에 localhost을 사용하는 것입니다. 컨테이너 내부에서 localhost는 호스트나 다른 서비스가 아닌 바로 그 컨테이너 자신을 의미합니다. Postgres 클라이언트는 이를 명확하게 보고합니다.

could not connect to server: Connection refused
	Is the server running on host "localhost" (127.0.0.1) and accepting
	TCP connections on port 5432?

연결 문자열은 postgresql://postgres:example@db:5432/postgres이어야 합니다. 호스트 부분은 서비스 이름입니다.

나중에 시간을 절약해 줄 두 가지 세부 사항이 있습니다. 이름은 현재 실행 중인 컨테이너로 해석되므로, docker compose up -d --scale web=3는 세 개의 주소를 가진 하나의 이름을 반환합니다. DNS를 영구적으로 캐싱하는 클라이언트는 죽은 컨테이너에 고정될 수 있습니다. 또한 --network 없이 일반 docker run을 사용할 때 적용되는 레거시 bridge 네트워크는 컨테이너 이름을 전혀 해석할 수 없습니다. 해당 네트워크의 컨테이너는 IP 주소로만 서로를 찾을 수 있으며, 외부 호스트 이름은 호스트의 /etc/resolv.conf 사본을 통해 조회합니다. 이것이 2016년의 컨테이너 링크 관련 조언이 현재 상황과 맞지 않는 이유입니다.

두 서비스를 연결할 때 ports:은 필요하지 않습니다

ports:은 컨테이너 포트를 호스트에 공개합니다. 이는 Docker 외부에서 들어오는 트래픽을 위한 설정입니다. 프로젝트 네트워크 내의 전체 포트 범위에서 이미 작동 중인 서비스 간 통신과는 아무런 관련이 없습니다.

따라서 많은 사용자가 데이터베이스 서비스에 추가하는 ports: - "5432:5432"은 아무런 도움이 되지 않으며 오히려 서버의 공용 인터페이스에 Postgres를 노출하는 보안 위험을 초래합니다. 해당 설정을 삭제하십시오. 마이그레이션을 위해 노트북에서 데이터베이스에 접근해야 한다면, "127.0.0.1:5432:5432"를 사용하여 루프백 인터페이스에 바인딩한 뒤 SSH 터널을 통해 접속하십시오. 리스닝 소켓, 공개된 포트, 방화벽 규칙의 차이점은 Linux에서 포트와 리스닝 서비스가 작동하는 방식에서 다룹니다.

expose:은 Compose에서 문서화 목적으로만 사용됩니다. 동일한 네트워크에 있는 컨테이너 사이에는 차단된 포트가 없으므로, 이 설정이 실제로 포트를 여는 작업은 수행하지 않습니다.

network_mode host가 적절한 경우와 그 대가

호스트 모드는 컨테이너 고유의 네트워크 네임스페이스를 제거하고 프로세스가 호스트의 인터페이스를 직접 사용하도록 합니다.

services:
  probe:
    image: alpine:3.20
    network_mode: host
    command: sleep infinity

이 방식을 사용해야 하는 타당한 이유가 있습니다. 미디어 서버나 홈 자동화 허브의 장치 검색과 같이 로컬 네트워크의 브로드캐스트 또는 멀티캐스트 트래픽을 확인해야 하는 프로세스는 브리지 뒤에서는 이를 볼 수 없습니다. 브리지가 해당 트래픽을 컨테이너로 전달하지 않기 때문입니다. 호스트의 인터페이스 카운터를 읽는 모니터링 에이전트는 호스트의 인터페이스가 필요합니다. 또한 주소 변환 단계를 건너뛰므로 높은 패킷 처리 속도가 중요할 때 유리합니다.

그에 따른 대가는 구체적입니다.

ports:가 작동하지 않습니다. Docker는 호스트 네트워크 모드를 사용할 때 게시된 포트가 무시된다고 경고하며, 컨테이너는 프로세스가 바인딩하는 모든 것을 바인딩합니다. 포트 8080을 사용하려는 두 개의 호스트 모드 컨테이너는 충돌하며, 두 번째 컨테이너는 bind: address already in use 오류와 함께 종료됩니다.

서비스 이름에 의한 이름 해석은 양방향 모두 불가능해집니다. 컨테이너가 프로젝트 네트워크에 있지 않으므로 db을 해석할 수 없으며, 다른 서비스들도 해당 컨테이너를 해석할 수 없습니다. 컨테이너는 호스트에 게시된 포트를 통해 보통 127.0.0.1에서 다른 서비스에 도달할 수 있습니다. Linux에서는 호스트가 브리지 네트워크의 컨테이너에 대해 열린 접근 권한을 가지고 있고 호스트 모드 컨테이너가 호스트의 네트워크 스택을 공유하므로, 브리지 컨테이너의 IP 주소와 리스닝 포트로 직접 접근할 수도 있습니다. 이러한 주소는 컨테이너가 재생성될 때마다 변경되므로, 루프백에 게시된 포트가 안정적인 경로입니다.

격리 기능이 사라집니다. 호스트 모드 컨테이너 내부에서 0.0.0.0을 바인딩하는 프로세스는 apt로 설치된 패키지와 똑같이 서버의 모든 인터페이스에서 리스닝 상태가 됩니다. 한 가지 장점은 이 트래픽이 일반적인 입력 경로를 따르므로 UFW 규칙이 적용된다는 점입니다. 이는 게시된 포트에는 적용되지 않는 특징입니다.

호스트 모드는 Linux Docker Engine 기능입니다. Docker Desktop은 버전 4.34부터 이 기능을 지원하며, 활성화한 후에만 사용할 수 있습니다. 또한 컨테이너가 호스트 IP 주소를 바인딩할 수 없으며 TCP와 UDP만 처리된다는 추가 제한이 있습니다. 팀의 절반은 Linux 서버를, 절반은 Docker Desktop을 사용한다면 동일한 파일이 다르게 동작할 수 있음을 예상해야 합니다.

호스트의 인터페이스가 필요할 때만 호스트 모드를 사용하십시오. 연결 문제를 해결하기 위해 호스트 모드를 사용하지 마십시오. 보통 하나의 문제를 더 어려운 문제로 대체할 뿐입니다.

외부 네트워크를 사용하여 두 개의 Compose 프로젝트 연결하기

한 프로젝트에서 생성한 네트워크는 다른 프로젝트에서 볼 수 없습니다. 따라서 같은 서버에 있더라도 proxy/compose.yaml의 리버스 프록시가 app/compose.yaml의 애플리케이션을 찾을 수 없습니다. 이를 해결하려면 어느 프로젝트에도 속하지 않는 별도의 네트워크가 필요합니다.

다음 명령어를 사용하여 수동으로 한 번 생성합니다.

docker network create edge

그다음 각 프로젝트에서 해당 네트워크를 external로 선언합니다. 프록시 측 설정은 다음과 같습니다.

services:
  proxy:
    image: traefik:v3.1
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - edge

networks:
  edge:
    name: edge
    external: true

애플리케이션 측 설정은 다음과 같습니다.

services:
  app:
    image: nginx:1.27
    networks:
      - edge
      - internal
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    networks:
      - internal

networks:
  edge:
    name: edge
    external: true
  internal:

external: true은 Compose에게 네트워크를 새로 생성하는 대신 기존 네트워크에 연결하고, docker compose down 시점에 해당 네트워크를 유지하도록 지시합니다. 별도의 name: 키는 보이는 것보다 중요합니다. 이 키가 없으면 Compose는 정확히 edge이라는 이름의 네트워크를 찾으려 하지만, 이 키를 사용하면 파일 내부의 이름과 호스트상의 이름을 다르게 지정할 수 있습니다.

네트워크가 존재하지 않으면 Compose는 시작을 거부하며, 네트워크가 external로 선언되었으나 찾을 수 없다는 오류를 보고합니다. 따라서 네트워크를 먼저 생성해야 합니다.

애플리케이션 파일에서 internal를 어떻게 사용하는지 확인하십시오. 데이터베이스는 해당 프로젝트 로컬 네트워크에만 위치하므로 프록시는 접근할 수 없으며 오직 app만 접근할 수 있습니다. 네트워크 아래에 internal: true를 추가하면 외부 세계와의 경로가 완전히 차단되어 보안이 강화됩니다. 이는 데이터베이스를 위한 좋은 기본 설정이지만, 설정 전에 알아두어야 할 비용이 있습니다. 내부 네트워크에 있는 컨테이너는 외부에서 아무것도 다운로드할 수 없으므로, 시작 시 apt-get update나 pip install을 실행하는 entrypoint는 응답이 없다가 타임아웃으로 실패하게 됩니다.

라우팅 규칙과 인증서를 포함한 전체 구성 예시는 하나의 Traefik 인스턴스 뒤에서 여러 앱 실행하기를 참조하십시오.

게시된 포트는 UFW를 우회합니다

Docker Compose 네트워킹에서 보안 사고로 이어지는 부분입니다. 포트를 게시하고 UFW가 활성화되어 SSH를 제외한 모든 연결을 차단하는지 확인해도, 서비스는 여전히 인터넷에서 접근 가능합니다.

sudo ufw status
curl http://203.0.113.10:8080

UFW는 해당 포트가 차단되었다고 표시합니다. 하지만 curl은 여전히 페이지를 반환합니다. 고장 난 것이 아닙니다. Docker는 자체 주소 변환 및 전달 규칙을 iptables에 직접 작성합니다. 게시된 컨테이너 포트로 향하는 트래픽은 호스트로 전달되는 대신 컨테이너로 즉시 전달되므로, 로컬 트래픽을 관리하는 UFW 체인을 거치지 않습니다. 또한 Docker의 규칙은 UFW의 규칙보다 먼저 적용됩니다.

간단한 해결책은 필요한 곳에만 포트를 게시하는 것입니다.

    ports:
      - "127.0.0.1:8080:80"

이렇게 하면 호스트 측이 루프백(loopback)에 바인딩됩니다. 따라서 해당 포트는 서버 자체와 SSH 터널을 통해서만 접근할 수 있으며, 외부에서는 접근할 수 없습니다. 공개 진입점은 80 및 443 포트를 의도적으로 게시하는 리버스 프록시 뒤에 배치하십시오. 게시된 포트를 필터링해야 하는 경우를 위한 DOCKER-USER 체인을 포함한 전체 설명은 Docker가 UFW를 우회하여 포트를 게시하는 이유와 해결 방법에서 확인할 수 있습니다.

네 가지 명령어로 디버깅하는 방법

먼저 각 컨테이너가 실제로 어떤 네트워크에 있는지 확인하십시오.

docker network inspect shop_default

Containers 블록은 연결된 모든 컨테이너와 그 주소를 나열합니다. 이 목록에 없는 서비스는 다른 네트워크에 있거나, 호스트 모드이거나, 실행 중이지 않은 것입니다.

자체 이미지 내부에 도구가 필요 없도록, 같은 네트워크에 연결된 일회용 컨테이너에서 이름 해석을 테스트하십시오.

docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432

nslookup가 실패하면 이름 해석이나 네트워크 소속을 확인해야 합니다. nslookup은 성공하는데 nc이 실패한다면 이름과 네트워크는 정상이며 연결 자체에 문제가 있는 것입니다. nc이 Connection refused를 보고한다면, 일반적으로 서비스가 해당 포트에서 리스닝하지 않거나 컨테이너 내부에서 0.0.0.0이 아닌 127.0.0.1에서 리스닝하고 있기 때문입니다. 후자는 개발 서버에서 흔히 발생하며, 해결책은 Docker가 아닌 애플리케이션의 바인딩 주소 설정에 있습니다. 대신 nc가 응답 없이 시간 초과된다면 방화벽 규칙이나 일시 중지된 컨테이너 등 트래픽을 차단하는 요소를 찾아야 합니다.

Docker 버그처럼 보이는 또 다른 오류가 있습니다. 컨테이너끼리는 통신이 가능하지만 사무실이나 VPN 네트워크의 장비에 도달할 수 없다면, Docker 서브넷이 해당 네트워크와 겹칠 가능성이 큽니다. Docker는 기본적으로 172.17.0.0/16부터 주소를 할당합니다. /etc/docker/daemon.json에서 풀을 이동하십시오.

{
  "default-address-pools": [
    { "base": "10.200.0.0/16", "size": 24 }
  ]
}

그런 다음 sudo systemctl restart docker를 실행하고 영향을 받는 네트워크를 재생성하십시오. 기존 네트워크는 생성될 때 할당된 서브넷을 유지하기 때문입니다.

FAQ

컨테이너끼리 서비스 이름으로 통신할 수 없는 이유는 무엇입니까?

컨테이너들이 같은 네트워크에 속해 있지 않기 때문입니다. Compose는 모든 서비스를 자동으로 <project>_default에 배치하지만, 서비스에 networks: 목록을 추가하는 순간 해당 목록이 그 서비스의 전체 네트워크 설정이 되며 기본 네트워크는 더 이상 적용되지 않습니다. docker network inspect <network> 명령을 실행하여 두 컨테이너가 모두 Containers 블록에 나타나는지 확인하십시오. 또한 어떤 서비스도 network_mode: host를 사용하지 않는지 확인하십시오. 호스트 모드 컨테이너는 Docker 네트워크에 속하지 않으므로 서비스 이름을 해석할 수 없습니다.

한 서비스가 다른 서비스에 접근하려면 포트를 공개해야 합니까?

아니요. Compose 네트워크상에서는 한 컨테이너의 모든 포트를 같은 네트워크 내의 다른 컨테이너들이 접근할 수 있습니다. ports:은 Docker 외부에서 들어오는 트래픽에 컨테이너를 노출할 때만 필요하며, expose:는 문서화 목적일 뿐입니다. 데이터베이스 포트를 공개하는 것은 서버의 공용 인터페이스에 데이터베이스를 노출하는 위험한 습관이므로 피해야 합니다.

브리지(bridge) 네트워크와 호스트(host) 네트워크의 차이점은 무엇입니까?

브리지 네트워크는 컨테이너에 가상 스위치상의 고유한 네트워크 네임스페이스와 주소를 부여하며, 컨테이너 간 자동 이름 해석과 아웃바운드 트래픽 변환을 지원합니다. 호스트 네트워크는 컨테이너가 호스트의 네트워크 스택을 직접 사용하게 합니다. 이 경우 별도의 주소나 서비스 이름 해석 기능이 없으며, 포트 공개도 필요 없고 호스트의 다른 리스너와 격리되지도 않습니다. 프로세스가 호스트의 인터페이스를 직접 사용해야 하는 경우가 아니라면, 기본값인 브리지 네트워크를 사용하는 것이 올바른 선택입니다.

서로 다른 두 Compose 파일의 컨테이너를 연결하려면 어떻게 해야 합니까?

docker network create edge 명령으로 공유 네트워크를 생성한 뒤, 두 파일 모두에서 external: true을 사용하여 해당 네트워크를 선언하고 통신이 필요한 서비스를 연결하십시오. Compose는 이 네트워크를 직접 생성하거나 삭제하지 않습니다. 생성 단계를 건너뛰면 Compose는 네트워크가 외부(external)로 선언되었으나 찾을 수 없다는 오류를 내며 시작을 거부합니다.

UFW가 포트를 차단했는데도 컨테이너가 인터넷에서 접근 가능한 이유는 무엇입니까?

공개된 포트는 Docker가 iptables에 추가하는 포워딩 규칙에 의해 처리되기 때문입니다. 이 규칙들은 UFW의 규칙보다 우선 적용되며, 포워딩된 트래픽은 애초에 UFW가 필터링하는 체인을 거치지 않습니다. 호스트 측 포트를 "127.0.0.1:8080:80"을 사용하여 루프백(loopback) 주소에 바인딩하고, 공개 서비스는 포트 80과 443을 사용하는 리버스 프록시 뒤에 배치하십시오.

이 글이 도움이 되었나요?