Skip to main content
Summary

컨테이너가 서로 만나야 애플리케이션이 된다

지금까지는 컨테이너 하나를 실행하고, 그 안의 데이터가 사라지지 않도록 볼륨을 연결하는 방법을 배웠다. 하지만 실제 애플리케이션은 보통 하나의 컨테이너로 끝나지 않는다. 웹 애플리케이션은 데이터베이스에 접속해야 하고, 백엔드 API는 Redis 같은 캐시 서버를 사용할 수 있다.

이때 필요한 것이 Docker 네트워크다. 네트워크를 알면 다음 질문에 답할 수 있다.

  • 브라우저에서 localhost:8080으로 접속하면 왜 Nginx 컨테이너가 열릴까?

  • 웹 컨테이너는 PostgreSQL 컨테이너를 어떤 주소로 찾아갈까?

  • 데이터베이스 포트 5432는 반드시 내 컴퓨터에 공개해야 할까?

  • 컨테이너 안에서 localhost를 쓰면 왜 기대한 서버에 연결되지 않을까?

핵심은 간단하다. 호스트에서 접근할 포트는 공개하고, 컨테이너끼리만 쓸 서비스는 같은 Docker 네트워크 안에서 이름으로 연결한다.

컨테이너 네트워크와 서비스 연결AI 생성 이미지

호스트 포트와 컨테이너 포트는 다르다

먼저 포트부터 정리하자. 컨테이너 안에서 실행되는 웹 서버가 80번 포트를 사용한다고 해서, 내 컴퓨터의 80번 포트가 자동으로 열리는 것은 아니다.

예를 들어 다음 명령을 보자.

docker run -d --name web -p 8080:80 nginx:1.27

여기서 -p 8080:80은 다음 뜻이다.

호스트의 8080번 포트 → 컨테이너의 80번 포트

왼쪽 8080은 호스트 포트다. 사용자가 브라우저에서 접속하는 내 컴퓨터의 포트다.

오른쪽 80은 컨테이너 포트다. Nginx가 컨테이너 내부에서 실제로 요청을 기다리는 포트다.

따라서 브라우저에서는 다음 주소로 접속한다.

http://localhost:8080

하지만 컨테이너 내부의 Nginx는 여전히 80번 포트에서 동작한다. Docker가 호스트의 8080번으로 들어온 요청을 컨테이너의 80번으로 전달해 주는 것이다. Docker의 포트 게시 형식은 기본적으로 host_ip:host_port:container_port 구조이며, 호스트 포트를 통해 컨테이너 포트를 외부에서 접근 가능하게 만든다.1

실행 중인 컨테이너를 확인해 보자.

docker ps

PORTS 항목에 대략 다음과 같은 내용이 보인다.

0.0.0.0:8080->80/tcp

이는 호스트의 모든 네트워크 주소에서 8080번 포트로 들어온 요청이 컨테이너의 80번 포트로 전달된다는 뜻이다.

포트 충돌은 호스트 포트에서 생긴다

같은 컨테이너 포트를 여러 번 사용하는 것은 문제가 아니다. Nginx 컨테이너가 여러 개 있어도 각 컨테이너 내부에서는 모두 80번 포트를 사용할 수 있다. 컨테이너마다 네트워크 공간이 분리되어 있기 때문이다.

문제는 호스트 포트다. 한 컴퓨터에서 같은 IP 주소의 같은 포트는 동시에 두 프로세스가 사용할 수 없다.

앞에서 web 컨테이너가 이미 호스트의 8080번 포트를 사용하고 있다면, 다음 명령은 실패한다.

docker run -d --name web2 -p 8080:80 nginx:1.27

오류 메시지는 환경마다 조금씩 다르지만, 핵심은 이미 8080번 포트가 사용 중이라는 뜻이다.

해결 방법은 세 가지다.

첫째, 기존 컨테이너를 중지하고 삭제한다.

docker rm -f web

둘째, 새 컨테이너의 호스트 포트를 다르게 지정한다.

docker run -d --name web2 -p 8081:80 nginx:1.27

이 경우 브라우저에서는 다음 주소로 접속한다.

http://localhost:8081

셋째, 외부 공개가 필요 없다면 아예 -p를 사용하지 않는다. 컨테이너끼리만 통신할 서비스라면 호스트 포트를 차지할 이유가 없다. 데이터베이스, 캐시, 메시지 큐는 보통 이 방식이 더 안전하다.

실습을 계속하기 위해 기존 컨테이너를 정리하자.

docker rm -f web web2

존재하지 않는 컨테이너라는 오류가 나와도 괜찮다. 이미 정리되어 있다는 뜻이다.

사용자 정의 bridge 네트워크 만들기

Docker에는 여러 네트워크 드라이버가 있지만, 로컬 개발에서 가장 자주 만나는 기본 형태는 bridge 네트워크다. bridge 네트워크는 같은 Docker 호스트 안의 컨테이너들이 서로 통신할 수 있게 해 주는 가상의 스위치처럼 이해하면 된다.

Docker에는 기본 bridge 네트워크도 있지만, 실무와 학습에서는 사용자 정의 bridge 네트워크를 만드는 편이 좋다. 사용자 정의 bridge 네트워크에서는 같은 네트워크에 연결된 컨테이너들이 컨테이너 이름으로 서로를 찾을 수 있다.2

네트워크를 하나 만들자.

docker network create app-net

생성된 네트워크를 확인한다.

docker network ls

자세한 정보가 필요하면 다음 명령을 사용한다.

docker network inspect app-net

이제 이 네트워크 안에 데이터베이스 컨테이너와 웹 서버 컨테이너를 실행해 보자.

docker run -d \
  --name db \
  --network app-net \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=appdb \
  postgres:16

이번에는 PostgreSQL의 5432번 포트를 호스트에 공개하지 않았다. -p 5432:5432가 없다는 점을 보자. 이 데이터베이스는 지금 내 브라우저나 외부 클라이언트가 직접 접근할 대상이 아니다. 같은 Docker 네트워크 안의 애플리케이션만 접근하면 충분하다.

웹 서버도 같은 네트워크에 실행한다.

docker run -d \
  --name web \
  --network app-net \
  nginx:1.27

이 web 컨테이너도 아직 호스트에 포트를 공개하지 않았다. 따라서 브라우저에서 http://localhost:80 또는 http://localhost:8080으로 접속해도 이 컨테이너에 연결되지 않는다.

하지만 같은 app-net 네트워크 안의 다른 컨테이너에서는 web이라는 이름으로 접근할 수 있다.

docker run --rm \
  --network app-net \
  busybox:1.36 \
  wget -qO- http://web

Nginx 기본 HTML이 출력되면 성공이다. 여기서 중요한 부분은 http://web이다. IP 주소를 몰라도 된다. Docker가 같은 사용자 정의 bridge 네트워크 안에서 컨테이너 이름을 주소처럼 사용할 수 있게 해 준다.

PostgreSQL도 이름으로 확인할 수 있다.

docker run --rm \
  --network app-net \
  postgres:16 \
  pg_isready -h db -p 5432 -U postgres

대략 다음과 비슷한 결과가 나오면 데이터베이스가 연결을 받을 준비가 된 것이다.

db:5432 - accepting connections

실제 웹 애플리케이션 컨테이너라면 데이터베이스 주소를 이렇게 설정한다.

DB_HOST=db
DB_PORT=5432
DB_NAME=appdb
DB_USER=postgres
DB_PASSWORD=secret

여기서 db는 컨테이너 이름이다. 같은 네트워크 안에서는 컨테이너 이름이 곧 서비스 주소가 된다.

localhost는 어디에서 실행하느냐에 따라 달라진다

초보자가 Docker 네트워크에서 가장 많이 헷갈리는 부분이 localhost다.

localhost는 항상 "내 자신"을 뜻한다. 문제는 "내 자신"이 누구냐는 것이다.

호스트에서 브라우저를 열고 다음 주소에 접속한다고 하자.

http://localhost:8080

이때 localhost는 내 컴퓨터, 즉 Docker를 실행하는 호스트를 뜻한다. 그래서 호스트의 8080번 포트가 컨테이너로 게시되어 있다면 접속할 수 있다.

반대로 컨테이너 안에서 다음 주소를 사용하면 의미가 달라진다.

http://localhost:8080

이때 localhost는 호스트가 아니라 그 컨테이너 자신이다. 즉, 컨테이너 안에서 실행 중인 자기 자신의 8080번 포트를 찾는다. 같은 네트워크에 있는 다른 컨테이너나 호스트의 포트를 찾는 것이 아니다.

직접 확인해 보자.

docker run --rm busybox:1.36 wget -qO- http://localhost:8080

대부분 실패한다. 이 busybox 컨테이너 안에는 8080번 포트에서 실행 중인 웹 서버가 없기 때문이다.

컨테이너에서 다른 컨테이너에 접근하려면 localhost가 아니라 컨테이너 이름을 사용한다.

docker run --rm \
  --network app-net \
  busybox:1.36 \
  wget -qO- http://web

웹 애플리케이션 컨테이너가 데이터베이스에 연결할 때도 마찬가지다.

잘못된 예는 다음과 같다.

DATABASE_URL=postgres://postgres:secret@localhost:5432/appdb

이 설정을 웹 컨테이너 안에서 사용하면, 웹 컨테이너는 자기 자신 안의 PostgreSQL을 찾으려고 한다. 하지만 PostgreSQL은 db 컨테이너에서 실행 중이다.

올바른 예는 다음과 같다.

DATABASE_URL=postgres://postgres:secret@db:5432/appdb

컨테이너 네트워크에서는 "내 컴퓨터에서 접속할 주소"와 "컨테이너끼리 접속할 주소"를 구분해야 한다.

외부에 공개할 포트와 내부에서만 쓸 포트

모든 포트를 호스트에 공개하는 것은 편해 보이지만 좋은 습관은 아니다. Docker에서 포트를 게시하면 기본적으로 호스트 바깥에서도 접근 가능한 형태가 될 수 있으므로, 필요한 포트만 공개해야 한다.1

구분 기준은 간단하다.

사용자나 외부 도구가 직접 접근해야 하는 서비스는 공개한다.

예를 들어 웹 애플리케이션은 브라우저에서 접속해야 하므로 호스트 포트를 공개한다.

docker rm -f web

docker run -d \
  --name web \
  --network app-net \
  -p 8080:80 \
  nginx:1.27

이제 호스트에서 접속할 수 있다.

http://localhost:8080

반대로 애플리케이션 내부에서만 사용하는 서비스는 공개하지 않는다.

PostgreSQL의 5432, Redis의 6379 같은 포트는 웹 애플리케이션 컨테이너만 접근하면 되는 경우가 많다. 이런 서비스는 같은 Docker 네트워크에만 연결하고 -p를 생략한다.

docker run -d \
  --name cache \
  --network app-net \
  redis:7

이 Redis는 호스트 포트를 차지하지 않는다. 하지만 같은 app-net 안의 컨테이너에서는 다음 주소로 접근할 수 있다.

cache:6379

개발 중 로컬 DB 관리 도구에서 PostgreSQL에 직접 접속해야 할 때도 있다. 그럴 때는 제한적으로 호스트 포트를 공개할 수 있다.

docker rm -f db

docker run -d \
  --name db \
  --network app-net \
  -p 127.0.0.1:5432:5432 \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=appdb \
  postgres:16

여기서는 127.0.0.1:5432:5432를 사용했다. 이는 호스트의 로컬 주소에서만 접근하도록 제한하는 형태다. 즉, 같은 컴퓨터의 DB 도구에서는 접속할 수 있지만, 외부 네트워크에 무심코 공개하는 위험을 줄인다.

정리하면 다음과 같다.

대상예포트 공개 여부
브라우저가 접속할 웹 서버Nginx, React 개발 서버, API 서버보통 공개
컨테이너끼리만 쓰는 데이터베이스PostgreSQL, MySQL보통 비공개
캐시 서버Redis, Memcached보통 비공개
로컬 개발 도구가 직접 접속할 DBPostgreSQL 관리 도구 연결필요할 때만 127.0.0.1로 제한 공개

정리 실습: 네트워크와 컨테이너 삭제

이번 장에서 만든 리소스를 정리하자.

docker rm -f web db cache

네트워크도 삭제한다.

docker network rm app-net

컨테이너가 아직 네트워크에 연결되어 있으면 네트워크 삭제가 실패할 수 있다. 그럴 때는 docker ps -a로 남아 있는 컨테이너를 확인하고 먼저 삭제하면 된다.

핵심 정리

컨테이너 네트워크를 이해할 때는 네 가지를 기억하면 된다.

첫째, -p 8080:80에서 왼쪽은 호스트 포트, 오른쪽은 컨테이너 포트다. 브라우저는 호스트 포트로 접속하고, Docker가 컨테이너 포트로 전달한다.

둘째, 포트 충돌은 주로 호스트 포트에서 생긴다. 컨테이너 내부 포트는 같아도 되지만, 같은 호스트 포트를 두 컨테이너가 동시에 사용할 수는 없다.

셋째, 사용자 정의 bridge 네트워크를 사용하면 컨테이너 이름으로 통신할 수 있다. 웹 애플리케이션은 데이터베이스를 localhost가 아니라 db 같은 이름으로 찾아가야 한다.

넷째, 외부에 공개할 포트와 내부에서만 사용할 포트를 구분해야 한다. 웹 서버처럼 사용자가 접근해야 하는 포트는 공개하고, 데이터베이스나 캐시는 같은 Docker 네트워크 안에서만 사용하도록 두는 것이 기본 원칙이다.

다음 장에서는 이 네트워크, 포트, 환경 변수 설정을 매번 긴 docker run 명령으로 입력하지 않고, compose.yaml 파일 하나로 여러 컨테이너를 함께 정의하고 실행하는 Docker Compose를 배운다.

참고

1Port publishing and mapping | Docker Docs

2Bridge network driver | Docker Docs

책 목록

Docker와 Docker Compose 입문: 컨테이너 개발 환경 구축부터 운영 준비까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기