도커 기초 강좌 #4 볼륨과 네트워크 — 데이터와 통신

9 분 소요

#3 까지 명령군을 잡았다면, 이번 글은 데이터 영속성컨테이너 간 통신 — 두 운영 주제로 들어갑니다.

도커 기초 강좌 시리즈에서 이번 글의 위치:

컨테이너의 파일시스템은 휘발성 #

먼저 한 가지 사실부터. 컨테이너 안에서 만든 파일은 컨테이너가 사라지면 같이 사라집니다.

확인이 어렵지 않습니다.

휘발성 시연
docker run --rm -it ubuntu:24.04 bash
root@xxx:/# echo "hello" > /tmp/note.txt
root@xxx:/# cat /tmp/note.txt
hello
root@xxx:/# exit

# 다시 들어가면
docker run --rm -it ubuntu:24.04 bash
root@yyy:/# cat /tmp/note.txt
cat: /tmp/note.txt: No such file or directory

새 컨테이너는 이미지에서 다시 만들어진 깨끗한 인스턴스라, 이전 컨테이너에서 만든 파일을 모릅니다. 이게 컨테이너의 핵심 특성이고 — 재현성의 원천입니다. 같은 이미지로 띄운 컨테이너는 어디서 띄워도 똑같다는 보장을 제공합니다.

문제는 DB 데이터, 업로드된 이미지, 로그처럼 살아남아야 하는 것들입니다. 이런 데이터를 컨테이너 바깥에 두는 방법이 볼륨(volume) 입니다.

두 가지 마운트 — bind mount vs named volume #

도커가 제공하는 두 가지 영속화 방식:

두 가지 마운트
┌─────────────────────────────────────────────────────────┐
│                      Host                               │
│  ┌──────────────────┐      ┌────────────────────────┐   │
│  │ /Users/me/data   │      │ Docker-managed area    │   │
│  │  (내가 관리)     │      │  (도커가 관리)         │   │
│  └────────┬─────────┘      └────────┬───────────────┘   │
│           │ bind mount              │ named volume      │
│           ▼                         ▼                   │
│   ┌─────────────────────────────────────────────────┐   │
│   │            Container — /app/data                │   │
│   └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

bind mount는 호스트의 특정 경로를 컨테이너 안에 그대로 꽂아 주는 방식입니다. 호스트와 컨테이너가 같은 디렉터리를 공유합니다.

named volume은 도커가 자체적으로 관리하는 영역에 데이터를 두고, 그 영역을 컨테이너에 꽂아 주는 방식입니다. 호스트의 특정 경로에 묶여 있지 않습니다.

bind mountnamed volume
호스트 경로내가 직접 지정도커 관리 영역 (/var/lib/docker/volumes/)
백업/이동호스트 디렉터리만 옮기면 됨docker volume 명령 또는 docker run으로
권한호스트 사용자 권한 그대로도커가 알아서 처리
주요 쓰임개발 — 코드 hot reload운영 — DB 데이터, 업로드
OS 이식성경로가 OS 마다 다름동일하게 동작

기준 한 줄: 개발에선 bind mount, 운영에선 named volume.

bind mount — 코드 hot reload #

#2 의 Flask 앱을 다시 가져옵니다. 코드를 한 글자 고칠 때마다 이미지를 다시 빌드하는 건 비효율적입니다. bind mount로 호스트의 코드 디렉터리를 컨테이너 안에 꽂으면, 호스트에서 저장만 해도 컨테이너가 바뀐 코드를 읽습니다.

bind mount
docker run --rm -it \
  -p 8000:8000 \
  -v $(pwd):/app \
  hello-docker

-v $(pwd):/app의 의미:

  • $(pwd) — 호스트의 현재 디렉터리
  • :로 구분
  • /app — 컨테이너 안의 마운트 지점 (WORKDIR과 같은 곳)

이러면 app.py를 호스트 에디터에서 고치고 저장하면 컨테이너 안의 /app/app.py도 즉시 바뀝니다. Flask의 dev 서버를 --reload로 띄우면 자동 재시작까지 됩니다.

--mount — 더 명시적인 형태 #

-v 대신 --mount를 쓰면 의미가 더 또렷해집니다.

--mount 형태
docker run --rm \
  -p 8000:8000 \
  --mount type=bind,source=$(pwd),target=/app \
  hello-docker

-v는 한 줄로 가볍고, --mount는 옵션을 키-값으로 풀어 적어 가독성이 좋습니다. 새로 짤 땐 --mount 권장, 다만 짧은 명령엔 -v가 편해서 둘 다 자주 보입니다.

읽기 전용 #

데이터를 보호하고 싶으면 :ro 또는 readonly를 붙입니다.

읽기 전용 bind mount
docker run --rm -v $(pwd)/config:/etc/myapp:ro myapp

설정 파일을 컨테이너에 주입할 때 자주 쓰는 패턴입니다.

named volume — 운영 데이터 #

DB 컨테이너처럼 데이터가 살아남아야 하는 경우엔 named volume 입니다.

named volume 만들기
docker volume create pgdata

이걸 PostgreSQL 컨테이너에 마운트:

postgres + named volume
docker run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

-v pgdata:/var/lib/postgresql/data에서 pgdata가 호스트 경로가 아니라 볼륨 이름이라는 점이 차이입니다. (도커는 : 앞이 절대 경로면 bind, 이름이면 named volume으로 해석합니다.)

이제 컨테이너를 지웠다가 다시 만들어도 데이터는 살아남습니다.

컨테이너 재생성, 데이터는 유지
docker rm -f pg
docker run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16
# 같은 데이터를 그대로 본다

볼륨 명령군 #

볼륨 다루기
docker volume ls               # 볼륨 목록
docker volume inspect pgdata   # 메타데이터, 호스트 경로 등
docker volume rm pgdata        # 삭제 (안 쓰일 때만)
docker volume prune            # 안 쓰이는 볼륨 일괄 정리

docker volume inspect로 보면 호스트의 어디에 데이터가 있는지 나옵니다. 보통 /var/lib/docker/volumes/pgdata/_data. macOS / Windows 에선 Docker Desktop의 VM 안이라 호스트에서 직접 접근하기 어렵지만, named volume은 그 차이를 신경 쓰지 않아도 동작이 일관적입니다.

익명 볼륨 #

-v /var/lib/postgresql/data처럼 이름 없이 마운트하면 도커가 임의 이름으로 만들어 줍니다. 이게 익명 볼륨입니다. 컨테이너를 지울 때 docker rm -v로 같이 지우지 않으면 계속 쌓입니다. 운영에선 항상 named로 쓰세요.

컨테이너 네트워크 개요 #

이제 통신. 도커 데몬은 기본으로 세 가지 네트워크를 만들어 둡니다.

기본 네트워크들
docker network ls
# NETWORK ID    NAME      DRIVER    SCOPE
# xxx           bridge    bridge    local
# yyy           host      host      local
# zzz           none      null      local
모드의미
bridge (기본)도커가 만든 가상 브리지에 컨테이너들이 붙음. 별도 IP를 받음
host컨테이너가 호스트 네트워크 스택을 그대로 씀. 격리가 줄어듦
none네트워크 없음. 외부와 어떤 통신도 안 함

대부분 bridge 위에서 일이 일어납니다. 다만 — 기본 bridge와 사용자 정의 bridge가 다르게 동작합니다.

기본 bridge의 함정 #

docker run만 하면 기본 bridge에 붙는데, 이 네트워크에서는 컨테이너끼리 이름으로 못 부릅니다. IP 로만 가능합니다. IP는 컨테이너가 다시 뜰 때마다 바뀌니 불편합니다.

안 되는 예 (기본 bridge)
docker run -d --name web nginx
docker run -d --name app myapp

docker exec app ping web
# ping: bad address 'web'

대신 사용자 정의 bridge 네트워크를 만들면 컨테이너 이름이 자동으로 DNS처럼 동작합니다.

사용자 정의 네트워크
docker network create mynet

docker run -d --name web --network mynet nginx
docker run -d --name app --network mynet myapp

docker exec app ping web
# 64 bytes from web (172.18.0.2) ...  ← 됩니다

web 이라는 이름이 DNS로 해석됩니다. 컨테이너의 IP가 바뀌어도, 이름은 안 바뀌니 안정적인 참조가 됩니다.

실무 규칙: 같이 통신해야 하는 컨테이너들은 항상 같은 사용자 정의 네트워크에 둔다. 도커 컴포즈는 이걸 자동으로 처리합니다 — 컴포즈 파일의 서비스들은 같은 네트워크에 들어가, 서로를 서비스 이름으로 부를 수 있습니다.

자주 쓰는 네트워크 명령 #

네트워크 명령군
docker network create mynet              # 만들기
docker network ls                        # 목록
docker network inspect mynet             # 어떤 컨테이너가 붙어 있나
docker network connect mynet myapp       # 떠 있는 컨테이너에 추가 연결
docker network disconnect mynet myapp    # 분리
docker network rm mynet                  # 삭제
docker network prune                     # 안 쓰는 네트워크 일괄 정리

포트 매핑 — -p 깊이 #

-p의 형태는 몇 가지가 있습니다.

-p 의 변형
-p 8000:8000              # 호스트 8000 → 컨테이너 8000
-p 80:8000                # 호스트 80 → 컨테이너 8000
-p 127.0.0.1:8000:8000    # 호스트의 127.0.0.1 에만 (외부 접근 차단)
-p 8000                   # 호스트 임의 포트 → 컨테이너 8000

운영에서 자주 쓰는 패턴은 127.0.0.1 바인딩 입니다.

로컬에서만 접근 가능
docker run -d -p 127.0.0.1:5432:5432 postgres:16

-p 5432:5432만 쓰면 모든 인터페이스에서 5432가 열려, 호스트가 인터넷에 노출돼 있다면 외부에서도 닿습니다(보통 방화벽이 막지만 안전장치는 한 겹 더). DB처럼 외부에 열 필요 없는 건 127.0.0.1 바인딩이 안전합니다.

macOS/Windows 사용자 주의: Docker Desktop 환경에선 -p가 호스트 OS의 포트로 직접 매핑됩니다. 그래서 호스트의 5432가 다른 PostgreSQL로 이미 쓰이고 있으면 충돌합니다. 이럴 땐 호스트 포트를 바꿉니다 (-p 5433:5432).

컨테이너끼리 통신은 “내부 포트” #

여기서 한 번 헷갈릴 수 있습니다.

셋업
docker network create mynet
docker run -d --name pg --network mynet \
  -e POSTGRES_PASSWORD=secret postgres:16     # -p 안 줌
docker run -d --name app --network mynet \
  -e DB_HOST=pg -e DB_PORT=5432 myapp

pg 컨테이너에 -p를 주지 않아도 app 에서는 pg:5432로 접근됩니다. 같은 네트워크 안에서는 컨테이너의 내부 포트가 그대로 노출됩니다. -p는 호스트 ↔ 컨테이너 매핑이지, 컨테이너 간 통신과는 별개입니다.

이게 중요한 이유: 운영에서 DB 컨테이너에는 -p를 안 주는 게 정석입니다. 외부 호스트 포트를 열지 말고, 같은 네트워크의 앱 컨테이너에서만 닿게 하는 식입니다.

호스트 네트워크와 none 모드 #

가끔 보는 변형입니다.

host 네트워크
docker run --rm --network host nginx

--network host는 컨테이너가 호스트의 네트워크 스택을 그대로 씁니다. -p 매핑이 필요 없습니다. 대신 격리가 줄어들고, macOS/Windows 에선 동작이 다릅니다 (Docker Desktop의 VM 경계 때문에). 리눅스 호스트의 특수한 운영 시나리오 외엔 잘 안 씁니다.

none — 네트워크 없음
docker run --rm --network none alpine sh

--network none은 외부와 통신하지 않는 격리된 컨테이너입니다. CTF, 보안 격리, 오프라인 처리 같은 상황에 가끔 등장합니다.

진단 — 통신이 안 될 때 #

도커 네트워크 트러블슈팅의 첫 걸음:

진단 순서
# 1. 같은 네트워크인지
docker inspect myapp --format '{{json .NetworkSettings.Networks}}'

# 2. 컨테이너 안에서 DNS 해석되는지
docker exec myapp getent hosts pg

# 3. 포트가 열려 있는지
docker exec myapp nc -zv pg 5432
# nc 가 없는 슬림 이미지가 많으니, busybox 한 번 띄워서 확인하기도 합니다
docker run --rm --network mynet busybox nc -zv pg 5432

# 4. 컨테이너 자체 포트 매핑 확인
docker port pg

대부분의 “닿지 않는다"는 네트워크 분리 / 컨테이너 이름 오타 / 컨테이너가 안 떠 있음 / 앱이 0.0.0.0이 아니라 127.0.0.1에 바인딩한 것 — 이 넷 중 하나입니다.

0.0.0.0 vs 127.0.0.1 한 단락 #

자주 만나는 함정입니다. 컨테이너 안의 앱이 127.0.0.1에 바인딩하면 컨테이너 안에서만 그 포트가 열립니다. 호스트나 다른 컨테이너에서 닿을 수 없습니다. 컨테이너 안의 서버는 항상 0.0.0.0에 바인딩 해야 외부에서 들어오는 연결을 받습니다. #2 의 Flask 예제에서 host="0.0.0.0"을 쓴 이유가 이거입니다.

정리 #

이번 글에서 잡은 그림:

  • 컨테이너 파일시스템은 휘발성. 살아남아야 하는 데이터는 볼륨으로 컨테이너 바깥에 둔다
  • bind mount는 호스트 경로를 그대로 꽂음 — 개발 hot reload에 적합
  • named volume은 도커 관리 영역 — DB 데이터 같은 운영 데이터에 적합
  • 사용자 정의 bridge 네트워크 에서는 컨테이너 이름이 DNS처럼 동작 — 항상 사용자 정의 네트워크를 쓴다
  • -p는 호스트 ↔ 컨테이너 매핑, 컨테이너 간 통신은 별개 (내부 포트 그대로)
  • DB 같은 컨테이너는 -p 없이 띄워 외부 노출을 막고, 같은 네트워크 앱에서만 접근하게 한다
  • 컨테이너 안의 서버는 0.0.0.0에 바인딩

다음 글(#5 레지스트리 — Docker Hub, GHCR, push/pull)에서는 만든 이미지를 다른 머신에서도 쓸 수 있게 — 레지스트리에 올리고 받는 흐름을 다룹니다. Docker Hub, GitHub Container Registry, 그리고 이미지 이름과 태그의 규칙까지 정리합니다.

X