Docker 기초 — Image, Container, Volume, Network

Docker의 핵심 개념인 Image와 Container의 차이, Volume으로 데이터를 영속하는 방법, 컨테이너 간 통신을 가능하게 하는 Network를 설명한다.

Seobway · · 12분

왜 Docker인가

"내 컴퓨터에서는 됐는데요"

협업할 때 가장 많이 듣는 말이다.
Python 버전, 라이브러리 버전, OS 차이... 원인은 늘 환경 불일치다.

Docker는 애플리케이션과 그 실행 환경을 함께 패키징한다.
이미지를 받으면 어느 환경에서나 동일하게 실행된다.[1]

%% desc: Docker가 해결하는 문제 — 환경 불일치에서 환경 일관성으로
flowchart LR
  subgraph BEFORE["Docker 이전"]
    D1["개발자 A\nPython 3.10\nmysqlclient 2.1"]
    D2["개발자 B\nPython 3.12\nmysqlclient 2.2"]
    D3["서버\nPython 3.9\nmysqlclient 1.4"]
    ERR["❌ 버전 충돌\n환경 불일치"]
    D1 & D2 & D3 --> ERR
  end

  subgraph AFTER["Docker 이후"]
    IMG["Docker Image\n(환경 포함)"]
    R1["개발자 A\n✅ 동일 환경"]
    R2["개발자 B\n✅ 동일 환경"]
    R3["서버\n✅ 동일 환경"]
    IMG --> R1 & R2 & R3
  end

Image vs Container

가장 먼저 잡아야 할 개념이다.

Image Container
정의 실행 환경의 스냅샷 (read-only) Image를 실행한 인스턴스
상태 불변 (Immutable) 가변 (실행 중 상태 변화)
비유 클래스 (Class) 인스턴스 (Instance)
저장 Docker Hub / 레지스트리 로컬 도커 데몬
%% desc: Image를 여러 Container로 실행하는 관계 — 하나의 Image에서 N개의 Container
flowchart TD
  IMG["🖼️ Django Image\n(python:3.12 + requirements.txt)"]

  C1["Container 1\n(web 서버, port 8000)"]
  C2["Container 2\n(celery worker)"]
  C3["Container 3\n(celery beat)"]

  IMG --> C1
  IMG --> C2
  IMG --> C3

하나의 Image에서 여러 Container를 띄울 수 있다.
Container가 삭제돼도 Image는 남아있다.

기본 명령어

# 이미지 빌드 (Dockerfile 기반)
docker build -t myapp:latest .

# 이미지 목록
docker images

# 컨테이너 실행
docker run -d --name myapp -p 8000:8000 myapp:latest

# 실행 중인 컨테이너 목록
docker ps

# 모든 컨테이너 (중지 포함)
docker ps -a

# 컨테이너 로그
docker logs myapp
docker logs -f myapp          # follow (실시간)

# 컨테이너 내부 접속
docker exec -it myapp bash

# 컨테이너 중지/삭제
docker stop myapp
docker rm myapp

Volume — 데이터 영속성

컨테이너는 삭제되면 내부 데이터도 함께 사라진다.
DB 데이터, 업로드 파일처럼 영속해야 하는 데이터는 Volume에 저장해야 한다.[2]

Volume의 종류:

타입 형식 특징
Named Volume mysql_data:/var/lib/mysql Docker가 관리. 이름으로 참조. 권장
Bind Mount ./src:/app/src 호스트 경로 직접 마운트. 개발 시 코드 동기화
tmpfs type=tmpfs 메모리에만 저장. 컨테이너 종료 시 삭제
%% desc: Named Volume vs Bind Mount의 데이터 흐름 차이
flowchart LR
  subgraph HOST["호스트"]
    HF["./src 디렉토리"]
    DV["Docker Named Volume\nmysql_data"]
  end

  subgraph CONTAINERS["컨테이너"]
    WEB["/app/src\n(소스코드 동기화)"]
    DB["/var/lib/mysql\n(DB 데이터 영속)"]
  end

  HF -- "Bind Mount\n코드 변경 즉시 반영" --> WEB
  DV -- "Named Volume\n컨테이너 삭제 후에도 보존" --> DB
# Named Volume 생성
docker volume create mysql_data

# Volume 목록
docker volume ls

# Volume 상세 정보
docker volume inspect mysql_data

# Volume 삭제 (데이터 삭제 주의!)
docker volume rm mysql_data

# 컨테이너와 Volume 함께 삭제
docker compose down -v

docker compose down -v는 Named Volume까지 삭제한다.
DB를 완전 초기화할 때 유용하지만, 운영 환경에서 절대 사용 금지.

Network — 컨테이너 간 통신

컨테이너는 기본적으로 격리된 환경이다.
서로 통신하려면 같은 Network에 속해야 한다.[3]

Docker Compose는 자동으로 프로젝트 단위 네트워크를 생성한다.
같은 Compose 파일의 서비스끼리는 서비스 이름으로 접근할 수 있다.

%% desc: Docker 내부 네트워크에서 서비스 이름으로 통신하는 구조
flowchart LR
  subgraph NETWORK["Docker Bridge Network (pms_v3_default)"]
    WEB["web\n(Django)\n172.18.0.2"]
    MYSQL["pms_v3-mysql\n172.18.0.3"]
    REDIS["pms_v3-redis\n172.18.0.4"]
    WORKER["worker\n(Celery)\n172.18.0.5"]
  end

  WEB -- "mysql://pms_v3-mysql:3306" --> MYSQL
  WEB -- "redis://pms_v3-redis:6379" --> REDIS
  WORKER -- "redis://pms_v3-redis:6379" --> REDIS

  HOST["호스트"] -- "localhost:8000" --> WEB

컨테이너 내부에서 DB에 접속할 때 localhost가 아니라 서비스 이름을 써야 한다.

# 잘못된 예: 컨테이너 내부에서 localhost는 자기 자신을 가리킴
DATABASE_URL = "mysql://user:pass@localhost:3306/mydb"  # ❌

# 올바른 예: 서비스 이름으로 접근
DATABASE_URL = "mysql://user:pass@pms_v3-mysql:3306/mydb"  # ✅

포트 매핑

컨테이너의 포트를 호스트에 노출할 때 ports를 사용한다.

services:
  web:
    ports:
      - "8000:8000"    # 호스트:컨테이너

내부 통신에는 포트 매핑이 필요 없다. pms_v3-mysql:3306처럼 컨테이너 내부 포트로 바로 접근한다.

Dockerfile 기초

Image를 빌드하는 설계도다.

# Dockerfile
FROM python:3.12-slim          # 베이스 이미지

WORKDIR /app                   # 작업 디렉토리

# 의존성 먼저 복사 (레이어 캐시 활용)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 소스 복사
COPY . .

# 포트 노출 (문서 목적, 실제 바인딩은 ports:로)
EXPOSE 8000

# 기본 실행 명령
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
%% desc: Dockerfile 레이어 구조 — 각 명령이 레이어를 쌓고, 변경 시 해당 레이어부터 재빌드
flowchart TD
  L1["FROM python:3.12-slim\n(Base Layer)"]
  L2["WORKDIR /app"]
  L3["COPY requirements.txt ."]
  L4["RUN pip install ...\n(의존성 레이어 — 자주 캐시됨)"]
  L5["COPY . .\n(소스 레이어 — 코드 변경 시 여기부터 재빌드)"]
  L6["CMD gunicorn ..."]

  L1 --> L2 --> L3 --> L4 --> L5 --> L6

참고

  1. Docker Inc., Docker Overview
  2. Docker Inc., Volumes — Docker Docs
  3. Docker Inc., Networking overview — Docker Docs

관련 글