Docker Compose 네트워킹과 서비스 이름 DNS 설정
기본 프로젝트 bridge 네트워크, 서비스 이름 DNS, host 모드 사용 시점, 여러 프로젝트의 네트워크 공유와 UFW를 우회하는 published port의 동작을 설명합니다.
애플리케이션이 시작되기 전에 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이며, 이는 호스트 내부의 가상 스위치입니다. 각 컨테이너는 전용 서브넷의 주소를 할당받고, 외부로 나가는 트래픽은 이동 과정에서 호스트의 주소로 변환됩니다.
docker compose down는 해당 네트워크를 다시 삭제합니다. 따라서 이전 프로젝트의 오래된 컨테이너가 네트워크를 계속 사용 중이면 네트워크가 삭제되지 않습니다. Docker는 error while removing network: network shop_default has active endpoints와 함께 작업을 거부합니다. 이 경우 해당 네트워크에 계속 연결된 컨테이너를 중지하거나 제거해야 합니다.
Compose를 처음 사용하는 경우 Compose 파일 구조 및 수명 주기 명령을 먼저 읽는 것이 좋습니다. 다음 내용은 프로젝트를 시작하고 중지할 수 있다고 가정합니다.
서비스 이름을 통한 DNS는 초보자가 놓치는 부분입니다
사용자가 정의한 네트워크에서는 Docker가 각 컨테이너에 127.0.0.11에서 제공되는 내장 DNS 서버를 실행합니다. 이 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이어야 합니다. 호스트 부분에는 서비스 이름을 지정합니다.
나중에 시간을 절약할 수 있는 세부 사항이 2가지 있습니다. 이름은 현재 실행 중인 대상에 연결되므로 docker compose up -d --scale web=3는 하나의 이름에 3개의 주소를 반환할 수 있습니다. DNS 결과를 영구적으로 캐시하는 클라이언트는 종료된 컨테이너에 계속 연결될 수 있습니다. 또한 --network 없이 일반 docker run가 사용하는 레거시 bridge 네트워크에서는 이름 확인이 전혀 작동하지 않습니다. 따라서 2016년에 작성된 컨테이너 링크 관련 안내가 현재 확인되는 동작과 일치하지 않습니다.
두 서비스를 연결하는 데 ports:가 필요하지 않습니다
ports:는 컨테이너 포트를 호스트에 게시합니다. 이는 Docker 외부에서 들어오는 네트워크 트래픽을 위한 기능입니다. 서비스 간 트래픽과는 관련이 없습니다. 서비스 간 트래픽은 프로젝트 네트워크의 전체 포트 범위에서 이미 작동합니다.
따라서 많은 사람이 데이터베이스 서비스에 추가하는 ports: - "5432:5432"는 아무런 도움이 되지 않으며 실제로는 문제를 일으킵니다. 서버의 공용 인터페이스에 Postgres를 노출하기 때문입니다. 이를 삭제합니다. 마이그레이션을 위해 노트북에서 접근해야 한다면 "127.0.0.1:5432:5432"를 사용하여 loopback에 바인딩하고 SSH 터널을 통해 접근합니다. listening socket, 게시된 포트 및 방화벽 규칙의 차이는 Linux에서 포트와 listening service가 작동하는 방식에서 설명합니다.
Compose에서 expose:는 문서화 용도로만 사용됩니다. 같은 네트워크에 있는 컨테이너 간에는 차단된 항목이 없으므로 아무것도 열지 않습니다.
network_mode host가 적합한 경우와 그 대가
Host 모드는 컨테이너 자체의 네트워크 네임스페이스를 제거하고 프로세스가 호스트의 인터페이스를 직접 사용하도록 합니다.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinity이를 사용해야 하는 실제 이유가 있습니다. 로컬 네트워크의 브로드캐스트 또는 멀티캐스트 트래픽을 확인해야 하는 프로세스는 브리지 뒤에서는 해당 트래픽을 확인할 수 없습니다. 브리지가 그 트래픽을 컨테이너로 전달하지 않기 때문입니다. 예를 들어 미디어 서버의 장치 검색이나 홈 자동화 허브가 이에 해당합니다. 호스트 인터페이스의 카운터를 읽는 모니터링 에이전트에도 호스트 인터페이스가 필요합니다. 또한 주소 변환 단계를 건너뛸 수 있어 패킷 속도가 높을 때 유용합니다.
대가는 명확합니다.
ports:은 작동하지 않습니다. Docker는 host 네트워크 모드에서 게시된 포트가 무시된다고 경고합니다. 컨테이너는 해당 프로세스가 바인딩하는 포트에 그대로 바인딩됩니다. host 모드의 컨테이너 2개가 포트 8080을 사용하려 하면 충돌이 발생하고 두 번째 컨테이너는 bind: address already in use와 함께 종료됩니다.
서비스 이름을 사용한 이름 확인은 양방향으로 사라집니다. 컨테이너가 프로젝트 네트워크에 연결되어 있지 않으므로 db을 확인할 수 없습니다. 다른 서비스도 해당 컨테이너의 이름을 확인할 수 없습니다. 컨테이너는 호스트에 게시된 포트를 통해서만 다른 서비스에 연결할 수 있으며, 일반적으로 127.0.0.1에서 연결합니다.
격리가 사라집니다. host 모드 컨테이너 내부의 프로세스가 0.0.0.0에 바인딩하면 서버의 모든 인터페이스에서 수신 대기합니다. 공용 인터페이스도 포함됩니다. 이는 apt로 설치한 패키지와 동일합니다. 이 방식에는 한 가지 장점이 있습니다. 이 트래픽은 일반 입력 경로를 따르므로 UFW 규칙이 적용됩니다. 게시된 포트에는 이 동작이 적용되지 않습니다.
Host 모드는 Linux Docker Engine 기능입니다. Docker Desktop은 version 4.34부터만 이를 지원하며, 먼저 기능을 활성화해야 합니다. 또한 컨테이너는 호스트 IP 주소에 바인딩할 수 없고 TCP 및 UDP만 처리할 수 있습니다. 팀의 일부가 Linux 서버를 사용하고 나머지가 Docker Desktop을 사용한다면 동일한 파일의 동작이 달라질 수 있습니다.
호스트의 인터페이스가 필요할 때 host 모드를 사용합니다. 연결 문제를 해결하기 위해 사용하지는 마십시오. 일반적으로 한 문제를 더 복잡한 문제로 바꾸기 때문입니다.
외부 네트워크로 2개의 Compose 프로젝트 연결
한 프로젝트에서 생성한 네트워크는 다른 프로젝트에서 보이지 않습니다. 따라서 같은 서버에 있어도 proxy/compose.yaml의 reverse proxy가 app/compose.yaml의 앱을 확인할 수 없습니다. 해결 방법은 어느 프로젝트도 소유하지 않는 네트워크를 사용하는 것입니다.
한 번 수동으로 생성합니다.
docker network create edge그런 다음 각 프로젝트에서 외부 네트워크로 선언합니다. proxy 측 설정은 다음과 같습니다.
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를 어떻게 처리하는지 확인합니다. 데이터베이스는 해당 프로젝트의 로컬 네트워크에만 연결되므로 proxy는 데이터베이스에 접근할 수 없고 app만 접근할 수 있습니다. 네트워크 아래에 internal: true를 추가하면 외부 네트워크로 나가는 경로도 완전히 제거됩니다. 데이터베이스에는 적절한 기본값입니다. 다만 설정하기 전에 알아야 할 비용이 있습니다. internal 네트워크의 컨테이너는 아무것도 다운로드할 수 없습니다. 따라서 시작 시 apt-get update 또는 pip install를 실행하는 entrypoint는 대기한 후 timeout으로 실패합니다.
라우팅 규칙과 인증서를 포함한 전체 설정 예제는 하나의 Traefik 인스턴스 뒤에서 여러 앱 실행을 참조합니다.
공개된 포트는 UFW를 우회합니다
이 부분은 Compose 네트워킹이 보안 사고로 이어지는 지점입니다. 포트를 공개하고, UFW가 활성 상태이며 SSH를 제외한 모든 연결을 거부하는지 확인했는데도 서비스에 인터넷에서 계속 접근할 수 있습니다.
sudo ufw status
curl http://203.0.113.10:8080UFW는 포트가 차단되었다고 표시합니다. 그러나 curl는 여전히 페이지를 반환합니다. 아무것도 고장 난 것이 아닙니다. Docker는 자체 주소 변환 및 전달 규칙을 iptables에 직접 기록합니다. 공개된 컨테이너 포트로 향하는 트래픽은 호스트로 전달되지 않고 컨테이너로 전달됩니다. 따라서 로컬 대상 트래픽에 대해 UFW가 관리하는 체인을 통과하지 않습니다. 또한 Docker 규칙은 UFW 규칙보다 먼저 일치합니다.
간단한 해결 방법은 필요한 위치에서만 포트를 공개하는 것입니다.
ports:
- "127.0.0.1:8080:80"이렇게 하면 호스트 측이 loopback에 바인딩됩니다. 따라서 포트는 서버 자체와 SSH 터널을 통해서만 접근할 수 있고, 그 외의 위치에서는 접근할 수 없습니다. 80 및 443을 의도적으로 공개하는 reverse proxy 뒤에 공개 진입점을 배치합니다. 공개된 포트를 필터링해야 하는 경우의 DOCKER-USER 체인을 포함한 전체 설명은 Docker가 UFW를 곧바로 우회하여 공개하는 이유와 해결 방법에 있습니다.
4개의 명령으로 디버깅하는 방법
먼저 각 컨테이너가 실제로 어느 네트워크에 연결되어 있는지 확인합니다.
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가 아니라 애플리케이션의 bind address를 수정해야 합니다.
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를 사용하지 않는지 확인합니다. host 모드 컨테이너는 Docker 네트워크에 연결되지 않으므로 서비스 이름을 확인할 수 없습니다.
한 서비스가 다른 서비스에 연결하려면 포트를 게시해야 합니까?
아니요. 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"을 사용하여 호스트 측을 loopback에 바인딩하고, 외부에 공개할 서비스는 80 및 443 포트의 reverse proxy 뒤에 배치합니다.