Docker Compose 네트워킹 설정 및 서비스 통신 방법
Docker Compose의 기본 브리지 네트워크와 서비스 이름 기반 DNS 통신 원리를 설명합니다. 프로젝트 간 네트워크 공유 방법과 UFW 방화벽을 우회하는 포트 노출 문제 등 실무에서 반드시 알아야 할 네트워킹 핵심 가이드를 제공합니다.
애플리케이션 시작 전 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 네트워크는 이름 해석 기능을 전혀 제공하지 않습니다. 이것이 2016년의 컨테이너 링크 관련 조언이 현재의 환경과 맞지 않는 이유입니다.
두 서비스를 연결할 때 ports:은 필요하지 않습니다
ports:은 컨테이너 포트를 호스트에 게시합니다. 이는 Docker 외부에서 들어오는 트래픽을 위한 것입니다. 프로젝트 네트워크 전체에서 이미 모든 포트 범위로 통신이 가능한 서비스 간 트래픽과는 아무런 관련이 없습니다.
따라서 많은 사람이 데이터베이스 서비스에 추가하는 ports: - "5432:5432"은 아무런 도움이 되지 않으며 오히려 해롭습니다. 서버의 공용 인터페이스에 Postgres를 노출하기 때문입니다. 이를 삭제하십시오. 마이그레이션을 위해 노트북에서 접근해야 한다면 "127.0.0.1:5432:5432"를 사용하여 루프백에 바인딩하고 SSH 터널을 통해 접속하십시오. 리스닝 소켓, 게시된 포트, 방화벽 규칙의 차이점은 Linux에서 포트와 리스닝 서비스가 작동하는 방식에서 다룹니다.
expose:은 Compose에서 문서화 목적으로만 사용됩니다. 같은 네트워크에 있는 컨테이너 사이에는 닫힌 포트가 없으므로, 이 설정이 무언가를 열어주는 것은 아닙니다.
network_mode host가 적절한 경우와 그 대가
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를 통해 호스트에 게시된 포트로만 다른 서비스에 접근할 수 있습니다.
격리 기능도 사라집니다. 호스트 모드 컨테이너 내부에서 0.0.0.0를 바인딩하는 프로세스는 apt로 설치된 패키지와 동일하게 서버의 모든 인터페이스(공용 인터페이스 포함)에서 수신 대기 상태가 됩니다. 한 가지 장점은 이 트래픽이 일반적인 입력 경로를 따르므로 UFW 규칙이 적용된다는 점입니다. 이는 게시된 포트에는 적용되지 않는 특징입니다.
Host 모드는 Linux Docker Engine의 기능입니다. Docker Desktop은 버전 4.34부터 이 기능을 지원하며, 별도로 활성화해야 합니다. 또한 컨테이너가 호스트 IP 주소를 바인딩할 수 없고 TCP와 UDP만 처리된다는 추가적인 제한이 있습니다. 팀의 절반은 Linux 서버를 사용하고 절반은 Docker Desktop을 사용한다면, 동일한 파일이 환경에 따라 다르게 동작할 수 있음을 고려해야 합니다.
호스트의 인터페이스가 반드시 필요한 경우에만 Host 모드를 사용하십시오. 연결 문제를 해결하기 위한 수단으로 사용해서는 안 됩니다. 보통은 기존 문제를 더 해결하기 어려운 새로운 문제로 대체하기 때문입니다.
두 개의 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을 실행하는 엔트리포인트는 응답 대기 상태가 되다가 타임아웃으로 실패하게 됩니다.
라우팅 규칙과 인증서를 포함한 전체 구성 예시는 하나의 Traefik 인스턴스 뒤에서 여러 앱 실행하기를 참조하십시오.
Published ports bypass UFW
This is the part of Compose networking that turns into a security incident. You publish a port, you check that UFW is active and denies everything except SSH, and the service is still reachable from the internet.
sudo ufw status
curl http://203.0.113.10:8080UFW says the port is blocked. The curl returns the page anyway. Nothing is broken. Docker writes its own address translation and forwarding rules straight into iptables, and traffic to a published container port is forwarded to the container rather than delivered to the host, so it never passes the chain UFW manages for locally destined traffic. Docker's rules are also matched before UFW's.
The short fix is to publish only where you need it:
ports:
- "127.0.0.1:8080:80"That binds the host side to loopback, so the port is reachable from the server itself and over an SSH tunnel, and from nowhere else. Put the public entry point behind a reverse proxy that publishes 80 and 443 on purpose. The full explanation, including the DOCKER-USER chain for the cases where you must filter a published port, is in why Docker publishes straight past UFW and how to fix it.
네 가지 명령어로 디버깅하는 방법
먼저 각 컨테이너가 실제로 어떤 네트워크에 속해 있는지 확인합니다.
docker network inspect shop_defaultContainers 블록에는 연결된 모든 컨테이너와 해당 주소가 나열됩니다. 이 목록에 서비스가 없다면 다른 네트워크에 있거나, host 모드이거나, 실행 중이지 않은 상태입니다.
자체 이미지 내부에 별도의 도구를 설치할 필요가 없도록, 동일한 네트워크에 연결된 일회용 컨테이너에서 이름 해석을 테스트합니다.
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432nslookup 명령이 실패한다면 이름 해석이나 네트워크 소속 문제일 가능성이 큽니다. nslookup은 성공하는데 nc가 실패한다면, 서비스는 실행 중이지만 해당 포트에서 수신 대기하지 않거나, 컨테이너 내부에서 127.0.0.1이 아닌 0.0.0.0에서 수신 대기 중인 경우입니다. 마지막 사례는 개발 서버에서 흔히 발생하며, 해결책은 Docker가 아닌 애플리케이션의 바인딩 주소 설정에 있습니다.
Docker 버그처럼 보이는 또 다른 실패 사례가 있습니다. 컨테이너끼리는 통신이 가능하지만 사무실이나 VPN 네트워크의 장비에 도달할 수 없다면, Docker 서브넷이 해당 네트워크와 중복될 가능성이 높습니다. Docker는 기본적으로 172.17.0.0/16부터 주소를 할당합니다. /etc/docker/daemon.json에서 풀(pool)을 이동하십시오.
{
"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를 사용하지 않는지 확인하십시오. host 모드 컨테이너는 Docker 네트워크에 속하지 않으므로 서비스 이름을 해석할 수 없습니다.
한 서비스가 다른 서비스에 접근하려면 포트를 게시(publish)해야 합니까?
아닙니다. Compose 네트워크상에서는 한 컨테이너의 모든 포트에 같은 네트워크 내의 다른 컨테이너가 접근할 수 있습니다. ports:은 오직 Docker 외부의 트래픽을 컨테이너로 전달하기 위해 존재하며, expose:는 문서화 목적일 뿐입니다. 데이터베이스 포트를 게시하는 것은 흔하지만 위험한 습관입니다. 데이터베이스를 서버의 공용 인터페이스에 노출하기 때문입니다.
bridge 네트워크와 host 네트워크의 차이점은 무엇입니까?
bridge는 컨테이너에 가상 스위치상의 고유한 네트워크 네임스페이스와 주소를 부여하며, 컨테이너 간 자동 이름 해석과 아웃바운드 트래픽 변환을 제공합니다. host는 컨테이너에 호스트의 네트워크 스택을 직접 제공합니다. 이 경우 별도의 주소나 서비스 이름 해석, 포트 게시 기능이 없으며 호스트의 다른 리스너와 격리되지 않습니다. 프로세스가 호스트의 인터페이스를 직접 사용해야 하는 경우가 아니라면, 기본값인 bridge를 사용하는 것이 올바른 선택입니다.
서로 다른 두 Compose 파일의 컨테이너를 연결하려면 어떻게 해야 합니까?
docker network create edge를 사용하여 공유 네트워크를 생성한 뒤, 두 파일 모두에서 external: true을 사용하여 해당 네트워크를 선언하고 통신이 필요한 서비스를 연결하십시오. Compose는 이 네트워크를 생성하거나 삭제하지 않습니다. 생성 단계를 건너뛰면 Compose는 시작을 거부하며 네트워크가 external로 선언되었으나 찾을 수 없다는 오류를 보고합니다.
UFW가 포트를 차단하고 있는데도 컨테이너가 인터넷에서 접근 가능한 이유는 무엇입니까?
게시된 포트는 Docker가 iptables에 추가하는 포워딩 규칙에 의해 처리되기 때문입니다. 이 규칙들은 UFW의 규칙보다 먼저 적용되며, 포워딩된 트래픽은 애초에 UFW가 필터링하는 체인을 거치지 않습니다. 호스트 측 바인딩을 "127.0.0.1:8080:80"을 사용하여 루프백으로 설정하고, 공개 서비스는 포트 80과 443을 사용하는 리버스 프록시 뒤에 배치하십시오.