사용자 정의 Docker 네트워크 및 컨테이너 통신 실전 가이드

이 가이드는 사용자 정의 Docker 브리지 네트워크와 컨테이너 통신에서의 역할을 실용적으로 탐구합니다. Docker CLI와 Docker Compose를 사용하여 네트워크를 생성, 관리하고 컨테이너를 연결하는 방법을 배웁니다. 사용자 정의 네트워크가 자동 DNS 확인을 활성화하고, 격리를 개선하며, 서비스 간 통신을 단순화하여 더 강력하고 확장 가능한 컨테이너화된 애플리케이션을 구축하는 방법을 알아보세요.

사용자 정의 Docker 네트워크 및 컨테이너 통신 실전 가이드

사용자 정의 Docker 네트워크는 컨테이너를 두 개 이상 실행하기 전까지는 선택 사항처럼 느껴지는 기능 중 하나입니다. 기본 브리지는 빠른 테스트에 사용할 수 있지만, 사용자 정의 브리지는 예측 가능한 서비스 이름, 더 깔끔한 격리, 그리고 더 쉬운 디버깅을 제공합니다. 웹 컨테이너, API 컨테이너, 데이터베이스가 있는 소규모 앱의 경우 그 차이는 즉각적입니다. API는 오늘 Docker가 할당한 IP를 추적하는 대신 db:5432에 연결할 수 있습니다.

이 가이드는 단일 Docker 호스트에서 사용자 정의 브리지 네트워크에 초점을 맞춥니다. 오버레이 네트워크, Kubernetes 네트워킹, Swarm 서비스 디스커버리는 다중 호스트 설정에서 관련 문제를 해결하지만, 브리지 네트워크는 여전히 로컬 개발, 소규모 배포, Docker Compose 프로젝트에서 일상적으로 사용되는 도구입니다.

기본 브리지가 불편해지는 이유

Docker는 자동으로 bridge라는 네트워크를 생성합니다. 네트워크를 지정하지 않고 컨테이너를 실행하면 일반적으로 여기에 배치됩니다. 간단한 경우에는 작동하지만, 다중 컨테이너 애플리케이션에는 적합하지 않습니다.

사용자 정의 브리지 네트워크에서는 Docker가 컨테이너 이름과 Compose 서비스 이름에 대한 내장 DNS를 제공합니다. 기본 브리지에서는 이름 기반 디스커버리가 제한적이며, 레거시 링킹은 사용하지 않는 것이 좋습니다. 실제 결과는 사용자 정의 네트워크를 사용하면 안정적인 호스트 이름으로 애플리케이션을 구성할 수 있다는 것입니다:

DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000

이는 읽기 쉽고, 머신 간 이동이 용이하며, 컨테이너 IP 주소보다 덜 취약합니다.

사용자 정의 네트워크는 또한 더 명확한 경계를 만듭니다. 동일한 네트워크에 연결된 컨테이너는 서로 통신할 수 있습니다. 다른 네트워크의 컨테이너는 통신할 수 없습니다. 단, 하나의 컨테이너를 두 네트워크에 연결하거나 호스트를 통해 포트를 게시하는 경우는 예외입니다. 이것은 완전한 보안 모델은 아니지만, 유용한 분리 계층입니다.

Docker CLI로 네트워크 생성

사용자 정의 브리지 생성:

docker network create app-net

해당 네트워크에서 두 개의 컨테이너 시작:

docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer

adminer 컨테이너에서 데이터베이스 호스트 이름은 db입니다. IP를 알 필요가 없습니다.

네트워크 검사:

docker network inspect app-net

드라이버, 서브넷, 게이트웨이, 연결된 컨테이너를 확인할 수 있습니다. 디버깅 시 이 명령은 기본적인 질문에 답합니다: 두 컨테이너가 실제로 동일한 네트워크에 있습니까?

기존 컨테이너를 연결할 수 있습니다:

docker network connect app-net some-container

분리:

docker network disconnect app-net some-container

Docker는 컨테이너가 여전히 연결되어 있는 동안 네트워크를 제거하지 않습니다. 먼저 컨테이너를 분리하거나 제거하세요:

docker network rm app-net

게시된 포트와 컨테이너 간 포트는 다릅니다

흔한 혼동: 동일한 Docker 네트워크에 있는 컨테이너는 서로 통신하기 위해 게시된 호스트 포트가 필요하지 않습니다. 게시된 포트는 호스트 또는 호스트 외부에서 들어오는 트래픽을 위한 것입니다.

API 컨테이너가 포트 3000에서 수신하고 웹 컨테이너가 동일한 네트워크에 있는 경우, 웹 컨테이너는 다음을 호출할 수 있습니다:

http://api:3000

랩톱 브라우저나 다른 호스트에서 Docker 호스트를 통해 API에 접근하려는 경우에만 -p 3000:3000이 필요합니다.

이는 프로덕션 환경에서 데이터베이스가 Docker 외부에서 직접 접근해야 하는 특별한 이유가 없는 한 호스트 포트를 게시하지 않아야 함을 의미합니다. 대신 API가 개인 Docker 네트워크를 통해 db:5432에 접근하도록 하세요.

일반적인 다중 서비스 앱에는 Compose 사용

Docker Compose는 네트워크를 정의하지 않아도 프로젝트에 기본 네트워크를 생성합니다. 서비스는 서비스 이름으로 서로 접근할 수 있습니다:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - api

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass

해당 파일에서 api는 호스트 이름 db를 사용하여 db에 접근할 수 있습니다. web은 애플리케이션 수준 구성이 해당 위치를 가리키는 경우 호스트 이름 api를 사용하여 api에 접근할 수 있습니다.

명확한 의도나 분리를 원할 때 명명된 네트워크를 정의할 수도 있습니다:

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    networks:
      - frontend

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

여기서 webdb와 네트워크를 공유하지 않으므로 직접 통신할 수 없습니다. api는 두 애플리케이션 계층 간의 브리지 역할을 합니다. 이는 실제 서비스에 유용한 형태입니다: 에지 서비스만 호스트에 노출하고, 데이터베이스를 비공개로 유지하며, 각 서비스를 통신이 필요한 곳에만 연결합니다.

depends_on은 준비 상태를 의미하지 않습니다

Compose depends_on은 일반적인 Compose 사용에서 시작 순서를 제어하지만, 데이터베이스가 연결을 수락할 준비가 되었음을 보장하지는 않습니다. API는 db 컨테이너 프로세스가 시작된 후에 시작될 수 있지만 PostgreSQL이 초기화 중이기 때문에 여전히 실패할 수 있습니다.

애플리케이션에서 재시도 로직을 사용하거나, Compose 버전과 워크플로에 따라 서비스 상태를 존중하는 Compose 구성을 사용하여 준비 상태를 처리하세요. 그렇더라도 데이터베이스는 초기 시작 후 다시 시작될 수 있으므로 애플리케이션 수준의 재시도 로직이 가장 신뢰할 수 있는 습관입니다.

실용적인 API 구성은 DATABASE_HOST=db를 사용하고 명확한 오류와 함께 종료되기 전에 짧은 기간 동안 연결을 재시도합니다.

사용자 정의 서브넷은 유용하지만 과도하게 사용하지 마세요

서브넷을 선택할 수 있습니다:

docker network create --subnet 172.28.0.0/16 app-net

Compose에서:

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

이는 Docker의 자동 서브넷이 VPN, 사무실 네트워크 또는 호스트의 다른 경로와 충돌할 때 도움이 됩니다. 대부분의 프로젝트에서는 필요하지 않습니다. 컨테이너 IP를 하드코딩하는 것은 드물어야 합니다. 서비스 이름이 일반적으로 더 나은 계약입니다.

네트워크 통신 문제 해결

한 컨테이너가 다른 컨테이너에 도달할 수 없는 경우 다음 순서로 확인하세요:

  1. 두 컨테이너가 모두 실행 중인가요?
  2. 동일한 네트워크에 연결되어 있나요?
  3. 클라이언트가 localhost가 아닌 서비스/컨테이너 이름을 사용하고 있나요?
  4. 서버가 예상 포트와 인터페이스에서 수신하고 있나요?
  5. 포트는 호스트 접근이 필요할 때만 게시되었나요?

localhost 실수는 특히 흔합니다. 컨테이너 내부에서 localhost는 동일한 컨테이너를 의미하며, Docker 호스트나 다른 서비스를 의미하지 않습니다. API가 localhost:5432에 연결하려고 하면 API 컨테이너 내부에서 PostgreSQL을 찾는 것입니다. 데이터베이스 서비스 이름이 db인 경우 db:5432를 사용하세요.

네트워크 검사:

docker network inspect app-net

동일한 네트워크에서 임시 진단 컨테이너 실행:

docker run --rm -it --network app-net alpine sh

내부에서 필요에 따라 도구를 설치하거나 사용하세요:

getent hosts db
nc -vz db 5432

최소 이미지에는 nc, curl 또는 DNS 도구가 설치되지 않을 수 있습니다. 단기 디버그 컨테이너는 애플리케이션 이미지에 문제 해결 패키지를 추가하는 것보다 더 깔끔합니다.

합리적인 기본 패턴

대부분의 단일 호스트 앱의 경우 Compose를 사용하고 프로젝트 네트워크를 생성하도록 하세요. frontendbackend와 같은 분리가 필요할 때 명시적 네트워크를 추가하세요. 내부 트래픽에는 서비스 이름을 사용하세요. 인간, 리버스 프록시 또는 외부 시스템이 도달해야 하는 포트만 게시하세요.

이렇게 하면 설명하기 쉬운 설정이 됩니다:

  • 브라우저는 web이 포트를 게시하므로 localhost:8080에 도달합니다.
  • web은 Docker 네트워크를 통해 api에 도달합니다.
  • api는 백엔드 네트워크를 통해 db에 도달합니다.
  • db는 실제 운영상의 이유가 없는 한 호스트 포트가 없습니다.

사용자 정의 Docker 네트워크는 단지 멋진 기능이 아닙니다. 이는 같은 머신에서 실행되는 컨테이너와 명확한 통신 모델을 가진 서비스의 차이입니다.

네트워크 별칭으로 마이그레이션을 더 쉽게 만들기

때로는 애플리케이션이 Compose 서비스 이름으로 사용하고 싶지 않은 호스트 이름을 기대할 수 있습니다. 네트워크에 별칭을 추가할 수 있습니다:

services:
  postgres:
    image: postgres:16
    networks:
      backend:
        aliases:
          - database

networks:
  backend:
    driver: bridge

backend의 컨테이너는 이제 postgres 또는 database로 서비스에 접근할 수 있습니다. 이는 이미 DATABASE_HOST=database를 사용하는 오래된 앱을 마이그레이션할 때 유용하지만, 모든 곳에서 별칭을 사용하지는 않습니다. 애플리케이션 구성을 제어할 때는 서비스 이름이 더 간단합니다.

호스트 접근은 별도의 문제입니다

컨테이너가 다른 컨테이너와 통신하는 것과 컨테이너가 Docker 호스트와 통신하는 것은 다릅니다. Docker Desktop에서는 host.docker.internal이 일반적으로 사용 가능합니다. Linux에서는 Docker 버전과 구성에 따라 지원이 달라집니다. 많은 팀이 필요할 때 명시적으로 추가합니다:

docker run --add-host=host.docker.internal:host-gateway ...

이것은 드물게 사용하세요. 컨테이너가 호스트에서 직접 실행되는 서비스에 크게 의존하는 경우, CI나 다른 개발자의 머신에서 설정을 재현하기 어려워질 수 있습니다. 데이터베이스와 캐시의 경우 동일한 Docker 네트워크에서 다른 서비스로 종속성을 실행하는 것이 일반적으로 더 깔끔합니다.

내부 포트는 Dockerfile 주석이 아닌 프로세스와 일치해야 합니다

Docker 네트워킹은 Dockerfile의 EXPOSE 라인이 도구에서 메타데이터로 사용하지 않는 한 신경 쓰지 않습니다. 애플리케이션은 실제로 호출하는 포트에서 수신해야 합니다. Node 앱이 3000에서 수신하는 경우, 누군가 실수로 EXPOSE 8080을 작성했더라도 다른 컨테이너는 api:3000을 사용해야 합니다.

또한 바인드 주소를 확인하세요. 컨테이너 내부에서 127.0.0.1에서 수신하는 서비스는 다른 컨테이너에서 접근할 수 없을 수 있습니다. 컨테이너 간 트래픽의 경우 프로세스는 일반적으로 0.0.0.0 또는 컨테이너의 네트워크 인터페이스에서 수신해야 합니다.

네트워크 디자인을 지루하게 유지하세요

기능이 존재하기 때문에 많은 네트워크를 만드는 것은 유혹적입니다. 실제로 필요한 통신 경로부터 시작하세요. 소규모 앱은 기본 Compose 네트워크만 필요할 수 있습니다. 더 현실적인 웹 앱은 frontendbackend가 필요할 수 있습니다. 그 이상으로 각 새 네트워크는 인시던트 중에 누군가 설명할 수 있는 이유가 있어야 합니다.

좋은 네트워크 디자인은 문제 해결을 더 쉽게 만듭니다. webdb에 도달할 수 없고 의도적으로 네트워크를 공유하지 않는다는 것을 알 때, 답은 아키텍처적이며 신비롭지 않습니다. 모든 서비스가 모든 네트워크에 연결되어 있으면 네트워크는 더 이상 아무것도 문서화하지 않습니다.

배포 전 실제 리뷰 패스

스크립트나 컨테이너 설정을 완료했다고 부르기 전에, 오전 2시에 디버깅해야 할 다음 사람인 것처럼 한 번 읽어보세요. 그러면 눈에 띄는 것이 달라집니다. 스크립트를 작성할 때 이해가 되었던 프롬프트가 CI 로그에 나타날 때 모호할 수 있습니다. 명백해 보였던 Docker 서비스 이름이 애플리케이션의 변수 이름과 일치하지 않을 수 있습니다. Bash 기본값은 개발에는 안전하고 프로덕션에는 위험할 수 있습니다.

의도적으로 어색한 값으로 짧은 드라이 런을 하는 것을 좋아합니다. 공백이 있는 경로를 사용하세요. 빈 선택적 값을 사용하세요. 대시로 시작하는 파일 이름을 시도하세요. 다른 작업 디렉토리에서 스크립트를 실행하세요. 예상 환경 변수 없이 컨테이너를 시작하세요. 이러한 테스트는 화려하지 않지만, 일반적으로 먼저 깨지는 가정을 잡아냅니다.

또한 실패 메시지를 확인하세요. 출력이 failed뿐이라면, 기사의 조언이 구현에 반영되지 않은 것입니다. 유용한 실패는 어떤 값이 사용되었는지, 어떤 검사가 실패했는지, 운영자가 무엇을 변경할 수 있는지 알려줍니다. 이는 모든 환경 변수를 덤프하거나 비밀을 출력하는 것을 의미하지 않습니다. 이는 특정성이 도움이 되는 곳에서 구체적이어야 함을 의미합니다: 구성 경로, 누락된 명령 이름, 네트워크 이름, 서비스 호스트 이름, 또는 프로세스가 바인딩하려고 시도한 포트.

마지막 습관은 예제를 시스템이 실제로 실행되는 방식에 가깝게 유지하는 것입니다. 프로덕션에서 Compose를 사용한다면 Compose로 테스트하세요. 스크립트가 systemd에 의해 실행된다면 systemd 또는 유사한 최소 환경으로 테스트하세요. 명령이 복사하여 붙여넣기에 안전해야 한다면 인용, -- 구분자, 유효성 검사를 예제 자체에 포함하세요. 독자는 경고보다 작동하는 패턴을 더 자주 복사합니다.

그 리뷰 패스는 관료주의가 아닙니다. 작은 자동화가 지루하게 유지되는 방법입니다. 지루함은 셸 프롬프트, 구성 로더, 변수 확장, 컨테이너 진단, Docker 네트워킹에서 원하는 것입니다. 동작이 덜 놀라울수록 다음 운영자가 신뢰하기 쉽습니다.

Docker 네트워킹의 경우 Compose 파일 옆이나 서비스 README에 의도된 트래픽 경로를 문서화하세요. web -> api:3000 -> db:5432와 같은 짧은 메모는 많은 혼란을 방지합니다. 또한 리뷰를 더 쉽게 만듭니다: 누군가 데이터베이스 포트를 게시하거나 web을 백엔드 네트워크에 연결하는 경우, 변경 사항은 의도된 경로에 대해 스스로 정당화되어야 합니다.

앱이 성장하면 네트워크 맵을 다시 방문하세요. 오래된 별칭, 사용되지 않는 게시된 포트, 더 이상 필요하지 않은 네트워크에 연결된 서비스는 조용한 운영 위험의 원천입니다.