infra
Platform

모듈 맵

[Docker] 컨테이너가 삭제되어도 안전하게 데이터를 보관하는 Volume & Bind Mount

0 / 27 완료

펼치기
0 / 27 완료0%

Docker · 09 / 27

[Docker] 컨테이너가 삭제되어도 안전하게 데이터를 보관하는 Volume & Bind Mount

컨테이너의 휘발성 원리를 이해하고 Named Volume과 Bind Mount로 데이터를 영구 보존합니다

🚨INCIDENT ALERT
HIGH

개발 DB 컨테이너를 지웠다가 다시 띄웠더니 어제 넣어둔 테스트 데이터가 전부 사라졌습니다. 컨테이너는 살아있는 동안만 쓰기 레이어를 유지하고, docker rm으로 삭제하면 그 레이어도 함께 사라집니다.

데이터베이스, 업로드 파일, 로그처럼 사라지면 안 되는 데이터는 컨테이너 바깥에 둬야 합니다. 이 모듈에서는 Named Volume과 Bind Mount를 실제로 써보며 어떤 데이터는 어디에 저장해야 하는지 판단합니다.

영구적인 데이터 보존 (Volume & Bind Mount)

컨테이너의 가장 중요한 특성 중 하나는 **휘발성(Ephemeral)**입니다. 컨테이너를 삭제하면 그 안의 모든 데이터는 사라집니다. 데이터베이스, 업로드 파일, 로그 등 영구적으로 보존해야 하는 데이터를 어떻게 관리할까요? 이 챕터에서는 Docker의 두 가지 데이터 영속화 메커니즘인 Named Volume과 Bind Mount를 상세히 학습합니다.


이번 챕터에서 배울 것

컨테이너가 삭제되어도 데이터를 유지하는 방법을 세 가지 관점에서 비교하고, 각 상황에 맞는 방식을 선택하는 기준을 잡습니다.

  • 1컨테이너 레이어(writable layer)의 휘발성 — 컨테이너 삭제 시 데이터가 사라지는 이유
  • 2Named Volume vs Bind Mount vs tmpfs 세 가지 방식의 차이와 선택 기준
  • 3docker volume create / inspect / ls / rm 명령어로 볼륨 생명주기 관리
  • 4Bind Mount로 개발 중 소스 코드 실시간 반영 (hot-reload 환경)
  • 5볼륨 데이터 백업(tar)과 다른 환경으로 마이그레이션
실습 환경 준비

볼륨 실습에서 PostgreSQL 컨테이너를 사용합니다. 이미지를 미리 pull해 두면 실습 중 대기 시간을 줄일 수 있습니다.

Docker 실행 중인지 확인
docker ps
실습용 디렉토리 생성
mkdir -p ~/vol-lab/data && echo 'hello' > ~/vol-lab/data/test.txt
PostgreSQL 이미지 미리 pull (볼륨 실습에서 사용)
docker pull postgres:16-alpine
💡개념

컨테이너의 휘발성 원리와 데이터 저장 옵션

Docker를 처음 쓰는 개발자가 가장 먼저 겪는 당혹감이 있습니다. PostgreSQL 컨테이너를 실행하고 데이터를 넣었는데, 다음 날 docker rm하고 다시 실행하면 테이블이 통째로 사라집니다. 컨테이너 파일시스템은 이미지 레이어(읽기 전용)와 컨테이너 레이어(쓰기 가능)로 구성되는데, docker rm은 그 쓰기 레이어까지 함께 삭제합니다. 이것은 버그가 아닌 설계 원칙입니다. 컨테이너를 교체 가능한 단위로 만들기 위해 의도적으로 휘발성을 부여한 것입니다. 데이터를 유지하려면 컨테이너 레이어 밖에 저장해야 하고, Docker는 그 방법으로 Named Volume, Bind Mount, tmpfs를 제공합니다. 이 ConceptBlock에서는 휘발성의 원리와 세 가지 저장 옵션의 선택 기준을 다룹니다.

컨테이너 휘발성과 저장 옵션 — Named Volume, Bind Mount, tmpfs

왜 컨테이너는 휘발적인가?

컨테이너의 파일시스템은 이미지 레이어(읽기 전용)와 컨테이너 레이어(쓰기 가능)로 구성됩니다.

┌────────────────────────────────────────────────────────────┐
│  컨테이너 레이어 (읽기/쓰기)                                  │
│  — 컨테이너 실행 중 생성/변경된 파일 저장                      │
│  — 컨테이너 삭제(docker rm) 시 이 레이어도 삭제됨 ← 휘발성!   │
├────────────────────────────────────────────────────────────┤
│  이미지 레이어 4 (읽기 전용)                                  │
├────────────────────────────────────────────────────────────┤
│  이미지 레이어 3 (읽기 전용)                                  │
├────────────────────────────────────────────────────────────┤
│  이미지 레이어 2 (읽기 전용)                                  │
├────────────────────────────────────────────────────────────┤
│  이미지 레이어 1 (읽기 전용)                                  │
└────────────────────────────────────────────────────────────┘

컨테이너 레이어는 컨테이너의 수명과 함께합니다. docker stop으로 중지한 컨테이너는 레이어가 유지되어 docker start로 재시작할 수 있습니다. 하지만 docker rm으로 컨테이너를 삭제하면 쓰기 레이어도 함께 사라집니다.

데이터 영속화 옵션 세 가지

Docker는 컨테이너 외부에 데이터를 저장하는 세 가지 방법을 제공합니다:

┌──────────────────────────────────────────────────────────┐
│                    컨테이너                               │
│                                                          │
│  /app/data ──────────────────┐                          │
│  /var/lib/mysql ─────────────┤                          │
│  /workspace ─────────────────┘                          │
└──────────────────────┬───────────────────────────────────┘
                       │
         ┌─────────────┼─────────────┐
         ▼             ▼             ▼
    Named Volume   Bind Mount    tmpfs Mount
    (Docker 관리)  (호스트 경로)  (메모리, 임시)
    영속적 저장     개발 환경       민감 정보

Named Volume (권장: 프로덕션 데이터)

  • Docker가 /var/lib/docker/volumes/볼륨명/_data/에 데이터를 저장
  • 호스트 경로 지식 불필요 → 이식성 높음
  • docker volume 명령어로 독립 관리
  • 여러 컨테이너에서 공유 가능

Bind Mount (권장: 개발 환경)

  • 호스트의 특정 디렉토리를 컨테이너에 직접 연결
  • 소스코드 변경이 즉시 컨테이너에 반영 (hot reload)
  • 호스트 경로에 의존적 → 이식성 낮음
  • 민감한 설정 파일 주입 시 유용

tmpfs Mount (메모리 기반 임시 저장)

  • 호스트 메모리에만 저장, 디스크 I/O 없음
  • 컨테이너 중지 시 데이터 소멸
  • 비밀 정보, 임시 캐시 등에 적합

Named Volume vs Bind Mount 비교

실전 볼륨 마운트의 물리적 규칙과 문법

Docker에서 볼륨을 바인딩할 때 사용하는 -v 옵션은 콜론(:) 좌우측을 기준으로 호스트의 영역과 컨테이너의 영역을 매핑하는 엄격한 구조적 약속을 가집니다.

  • -v [호스트경로_또는_볼륨이름]:[컨테이너경로][:옵션] 문법:
    • 콜론(:) 좌측: 호스트의 절대경로나 생성해 둔 Named Volume의 이름이 위치합니다.
    • 콜론(:) 우측: 컨테이너 내부에서 마운트되어 노출될 경로가 위치합니다.
    • 옵션(선택): ro(Read-only) 등을 선언하여 쓰기 권한을 통제할 수 있습니다.
  • 실전 예시 (호스트경로(또는볼륨이름):컨테이너경로[:옵션] 구조):
    로컬 터미널
    # 호스트의 절대경로인 /home/user/data를 컨테이너 내부의 /app/data로 바인드 마운트
    $ docker run -d -v /home/user/data:/app/data nginx
    
    이러면 컨테이너가 삭제되어도 /home/user/data 내에 보존된 소스 및 데이터는 영구히 유지됩니다. 만약 좌측에 /로 시작하는 절대경로 대신 my-vol 같은 단순 명칭을 적으면 Docker는 이를 Named Volume으로 인식하여 Docker 전용 격리 공간인 /var/lib/docker/volumes/ 하위에 자동 할당합니다.

Named Volume의 물리적 호스트 저장소 실체

Named Volume은 마술이 아닙니다. Linux 환경에서 Docker는 생성된 볼륨 데이터를 호스트 OS의 /var/lib/docker/volumes/[볼륨명]/_data 디렉토리 하위에 물리적으로 온전히 격리 보관합니다. SRE는 이 경로의 소유권이 엄격히 root 권한으로 통제된다는 사실을 인지해야 합니다. 따라서 주니어 개발자가 호스트 단에서 이 격리 공간 내부의 파일을 무단 수정하거나 권한을 훼손할 경우 컨테이너 실행이 크래시될 수 있어 각별한 주의가 필요합니다.

실전 볼륨 마운트의 물리적 규칙과 문법

Docker에서 볼륨을 바인딩할 때 사용하는 -v 옵션은 콜론(:) 좌우측을 기준으로 호스트의 영역과 컨테이너의 영역을 매핑하는 엄격한 구조적 약속을 가집니다.

  • -v [호스트경로_또는_볼륨이름]:[컨테이너경로][:옵션] 문법:
    • 콜론(:) 좌측: 호스트의 절대경로나 생성해 둔 Named Volume의 이름이 위치합니다.
    • 콜론(:) 우측: 컨테이너 내부에서 마운트되어 노출될 경로가 위치합니다.
    • 옵션(선택): ro(Read-only) 등을 선언하여 쓰기 권한을 통제할 수 있습니다.
  • 실전 예시 (호스트경로(또는볼륨이름):컨테이너경로[:옵션] 구조):
    로컬 터미널

실습 디렉토리 준비

mkdir -p /tmp/docker/part3/exam_9 && cd /tmp/docker/part3/exam_9

호스트의 절대경로인 /home/user/data를 컨테이너 내부의 /app/data로 바인드 마운트

$ docker run -d -v /home/user/data:/app/data nginx

  이러면 컨테이너가 삭제되어도 `/home/user/data` 내에 보존된 소스 및 데이터는 영구히 유지됩니다. 만약 좌측에 `/`로 시작하는 절대경로 대신 `my-vol` 같은 단순 명칭을 적으면 Docker는 이를 **Named Volume**으로 인식하여 Docker 전용 격리 공간(`/var/lib/docker/volumes/`)에 할당합니다.


| 항목 | Named Volume | Bind Mount |
|------|-------------|-----------|
| 데이터 위치 | Docker가 관리 | 호스트 임의 경로 |
| 이식성 | 높음 | 낮음 (호스트 의존) |
| 초기 데이터 | 이미지 기본값 채움 | 호스트 파일 그대로 |
| 백업 | docker volume + tar | 일반 파일 백업 |
| 주용도 | DB, 영구 데이터 | 개발 소스코드 |
| 성능 | 좋음 | 좋음 (Linux) / 느림 (macOS/Windows) |

</ConceptBlock>

---

<ConceptBlock title="Named Volume 상세 — 생성, 마운트, 관리">

`docker-compose down`을 실행했더니 MySQL 데이터가 모두 사라졌습니다. compose 파일에서 볼륨을 Named Volume이 아닌 익명 볼륨으로 설정했기 때문입니다. Named Volume은 이름으로 관리되기 때문에 컨테이너가 삭제되어도 데이터가 남고, 동일한 이름으로 새 컨테이너를 연결하면 그대로 이어집니다. 프로덕션에서 DB 컨테이너를 업그레이드하거나 재시작할 때 데이터를 유지하는 표준 방법입니다. 또한 Named Volume은 Docker가 내부에서 권한과 경로를 관리하기 때문에 Bind Mount처럼 UID/GID 문제가 자주 발생하지 않습니다. 이 ConceptBlock에서는 Named Volume 생성부터 마운트, 백업, 삭제까지 전체 생명주기를 다룹니다.

![Named Volume 상세 — 생성, 마운트, 컨테이너 교체 시 데이터 유지](/images/docker/volumes/named-volume-detail.png)

## Named Volume 전체 워크플로우

### 볼륨 생성

```bash
# 명시적 볼륨 생성
docker volume create mysql-data
docker volume create --driver local --name app-uploads

# 볼륨 목록 확인
docker volume ls
# DRIVER    VOLUME NAME
# local     mysql-data
# local     app-uploads

# 볼륨 상세 정보
docker volume inspect mysql-data

docker volume inspect 출력:

JSON
[
  {
    "CreatedAt": "2024-01-15T10:30:00Z",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/mysql-data/_data",
    "Name": "mysql-data",
    "Options": {},
    "Scope": "local"
  }
]

Mountpoint가 실제 데이터가 저장되는 호스트 경로입니다.

볼륨 마운트 옵션

Docker
# 형식 1: -v 플래그
docker run -d \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  --name mysql-server \
  mysql:8.0

# 형식 2: --mount 플래그 (더 명시적, 권장)
docker run -d \
  --mount type=volume,source=mysql-data,target=/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  --name mysql-server \
  mysql:8.0

# 읽기 전용 마운트
docker run -d \
  --mount type=volume,source=config-data,target=/app/config,readonly \
  myapp

# 볼륨 마운트 확인
docker inspect mysql-server --format '{{json .Mounts}}'

볼륨의 초기 데이터 채움

Named Volume을 처음 마운트할 때, 해당 컨테이너 경로에 이미지에 기존 파일이 있으면 Docker가 자동으로 볼륨을 채워줍니다. 이는 Bind Mount와의 중요한 차이점입니다.

Dockerfile
# MySQL 이미지의 경우
# /var/lib/mysql 디렉토리에 초기 MySQL 파일들이 있음
# Named Volume 첫 마운트 시 이 파일들이 볼륨에 복사됨

# Bind Mount라면 빈 호스트 디렉토리가 그대로 마운트되어
# MySQL 초기화 파일들이 없어 문제 발생

볼륨 관리 명령어

로컬 터미널
# 사용하지 않는 볼륨 삭제 (컨테이너와 연결 안 된 것)

# 특정 볼륨 삭제
docker volume rm mysql-data

# 여러 볼륨 삭제
docker volume rm vol1 vol2 vol3

# 컨테이너 삭제 시 연결된 볼륨도 함께 삭제
docker rm -v mysql-server

# 볼륨 내용 확인 (임시 컨테이너 활용)
docker run --rm \
  -v mysql-data:/data \
  alpine:3.18 \
  ls -la /data/
위험 명령어컨테이너에 연결되지 않은 볼륨 데이터가 영구 삭제됩니다.

미사용 Docker 볼륨 삭제

안전한 실행 조건: 삭제 대상 볼륨이 실습용이며 데이터베이스·업로드·운영 데이터가 아님을 확인한 경우에만 실행합니다.

실행 전 반드시 확인

  • docker volume ls -f dangling=true로 삭제 후보를 확인했다
  • 볼륨 이름과 용도를 확인했다
  • 필요한 데이터 백업이 끝났다
docker volume prune

위 항목을 모두 확인한 후 복사할 수 있습니다

볼륨 백업 및 복원

Docker
# 볼륨 백업 (tar 아카이브)
docker run --rm \
  -v mysql-data:/source \
  -v $(pwd):/backup \
  alpine:3.18 \
  tar czf /backup/mysql-data-backup.tar.gz -C /source .

# 볼륨 복원
docker volume create mysql-data-restored
docker run --rm \
  -v mysql-data-restored:/target \
  -v $(pwd):/backup \
  alpine:3.18 \
  tar xzf /backup/mysql-data-backup.tar.gz -C /target

# 다른 서버로 볼륨 이전
scp mysql-data-backup.tar.gz user@newserver:/tmp/
# 새 서버에서
docker volume create mysql-data
docker run --rm \
  -v mysql-data:/target \
  -v /tmp:/backup \
  alpine:3.18 \
  tar xzf /backup/mysql-data-backup.tar.gz -C /target

MySQL 컨테이너 데이터 영속성 실습 — 삭제 후 복원

목표

Named Volume을 사용하여 MySQL 컨테이너의 데이터가 컨테이너 삭제 후에도 유지되는 것을 직접 확인합니다.

실습 전 디렉토리와 예제 파일을 먼저 준비합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/docker/part2/volumes-demo/{data,config,logs}

# 예제 설정 파일 생성
cat > /tmp/docker/part2/volumes-demo/config/app.conf << 'EOF'
server_name=myapp
log_level=info
max_connections=100
data_dir=/data
EOF

echo "Initial data: $(date)" > /tmp/docker/part2/volumes-demo/data/seed.txt

실습

1단계: Named Volume과 함께 MySQL 컨테이너 실행

로컬 터미널
# Named Volume 명시적 생성 (선택사항 — docker run 시 자동 생성됨)
docker volume create mysql-persistent

# MySQL 컨테이너 실행
docker run -d \
  --name mysql-test \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=rootpassword \
  -e MYSQL_DATABASE=testdb \
  -e MYSQL_USER=testuser \
  -e MYSQL_PASSWORD=testpassword \
  -v mysql-persistent:/var/lib/mysql \
  mysql:8.0

# 컨테이너가 완전히 시작될 때까지 대기 (약 30초)
docker logs -f mysql-test  # 'ready for connections' 메시지 대기
# Ctrl+C로 로그 팔로우 종료

2단계: 데이터베이스에 데이터 삽입

Docker
# MySQL 컨테이너 내부에서 쿼리 실행
docker exec -it mysql-test mysql -u testuser -ptestpassword testdb

# MySQL 프롬프트에서:
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100),
  email VARCHAR(100),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name, email) VALUES
  ('김철수', 'chulsoo@example.com'),
  ('이영희', 'younghee@example.com'),
  ('박민준', 'minjoon@example.com');

SELECT * FROM users;
-- 3개 행 확인

EXIT;

3단계: 컨테이너 삭제

로컬 터미널
# 컨테이너 강제 삭제 (데이터가 사라질 것 같지만...)

# 컨테이너가 없어졌는지 확인
docker ps -a | grep mysql-test
# (출력 없음 — 완전히 삭제됨)

# 그런데 볼륨은 여전히 존재!
docker volume ls | grep mysql-persistent
# local     mysql-persistent
위험 명령어컨테이너 쓰기 레이어는 삭제되지만 Named Volume 데이터는 남는지 확인하는 실습입니다.

MySQL 컨테이너 강제 삭제

안전한 실행 조건: mysql-test가 실습용 컨테이너이고 데이터가 mysql-persistent 볼륨에 저장된 경우에만 실행합니다.

실행 전 반드시 확인

  • docker ps -a에서 mysql-test 이름을 확인했다
  • docker volume ls에서 mysql-persistent 볼륨을 확인했다
  • 컨테이너 내부가 아닌 볼륨에 데이터를 저장했음을 확인했다
docker rm -f mysql-test

위 항목을 모두 확인한 후 복사할 수 있습니다

4단계: 동일한 볼륨으로 새 컨테이너 생성

Docker
# 완전히 새로운 컨테이너를 동일 볼륨으로 실행
docker run -d \
  --name mysql-restored \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=rootpassword \
  -e MYSQL_DATABASE=testdb \
  -e MYSQL_USER=testuser \
  -e MYSQL_PASSWORD=testpassword \
  -v mysql-persistent:/var/lib/mysql \
  mysql:8.0

# 완전히 시작될 때까지 대기
sleep 30

# 데이터 확인
docker exec -it mysql-restored mysql -u testuser -ptestpassword testdb -e "SELECT * FROM users;"
# +----+-----------+------------------------+---------------------+
# | id | name      | email                  | created_at          |
# +----+-----------+------------------------+---------------------+
# |  1 | 김철수    | chulsoo@example.com    | 2024-01-15 ...      |
# |  2 | 이영희    | younghee@example.com   | 2024-01-15 ...      |
# |  3 | 박민준    | minjoon@example.com    | 2024-01-15 ...      |
# +----+-----------+------------------------+---------------------+
# 데이터가 완벽하게 복원됨!

5단계: 볼륨 정보 확인

로컬 터미널
# 볼륨 상세 정보
docker volume inspect mysql-persistent

# 실제 데이터 저장 위치 확인 (root 권한 필요)
sudo ls -la /var/lib/docker/volumes/mysql-persistent/_data/

# 컨테이너의 마운트 정보 확인
docker inspect mysql-restored --format '{{json .Mounts}}' | python3 -m json.tool

6단계: 정리

로컬 터미널
# 컨테이너 삭제 (볼륨은 남김)

# 볼륨도 삭제하려면
docker volume rm mysql-persistent

# 모든 미사용 볼륨 정리
위험 명령어연결되지 않은 볼륨의 데이터가 영구 삭제됩니다.

실습 후 미사용 볼륨 일괄 정리

안전한 실행 조건: 실습에서 만든 볼륨만 남아있고 보존할 데이터가 없는 경우에만 실행합니다.

실행 전 반드시 확인

  • docker volume ls -f dangling=true로 삭제 대상을 확인했다
  • mysql-persistent 같은 보존 대상 볼륨을 별도로 처리했다
  • 데이터 복구가 필요 없는 실습 환경임을 확인했다
docker volume prune

위 항목을 모두 확인한 후 복사할 수 있습니다

🔍실행 후 확인할 것
  • mysql-test 컨테이너 삭제 후 docker ps -a에서 해당 컨테이너가 사라졌는가?
  • docker volume ls 출력에서 mysql-persistent 볼륨은 계속 남아있는가?
  • mysql-restored 컨테이너에서 SELECT * FROM users 결과가 그대로 출력되는가?
  • docker volume inspect mysql-persistent로 실제 Mountpoint를 확인했는가?

Bind Mount로 개발 환경 구성 — 소스코드 실시간 반영

목표

Bind Mount를 사용하여 로컬 소스코드 변경이 컨테이너에 즉시 반영되는 개발 환경을 구성합니다.

실습 — Python Flask 개발 환경

프로젝트 구조 생성

로컬 터미널
mkdir -p /tmp/docker/part2/volumes-demo/flask-dev && cd /tmp/docker/part2/volumes-demo/flask-dev

# app.py 생성
cat > app.py << 'EOF'
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({
        "message": "Hello from Flask!",
        "version": "1.0"
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
EOF

# requirements.txt 생성
cat > requirements.txt << 'EOF'
flask==3.0.0
EOF

Bind Mount로 컨테이너 실행

Docker
# 현재 디렉토리를 컨테이너의 /app에 마운트
docker run -d \
  --name flask-dev \
  -p 5000:5000 \
  -v /tmp/docker/part2/volumes-demo/flask-dev:/app \
  -w /app \
  python:3.11-slim \
  sh -c "pip install -r requirements.txt -q && python app.py"

# 앱 실행 확인
curl http://localhost:5000/
# {"message": "Hello from Flask!", "version": "1.0"}

소스코드 변경 즉시 반영 확인

로컬 터미널
# app.py 수정 (컨테이너 재빌드 없이)
cat > /tmp/docker/part2/volumes-demo/flask-dev/app.py << 'EOF'
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({
        "message": "Hello from Flask — 수정됨!",
        "version": "2.0"
    })

@app.route('/status')
def status():
    return jsonify({"status": "running", "env": os.getenv("FLASK_ENV", "development")})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)
EOF

# Flask debug 모드이면 자동 리로드됨
sleep 2
curl http://localhost:5000/
# {"message": "Hello from Flask — 수정됨!", "version": "2.0"}

curl http://localhost:5000/status
# {"status": "running", "env": "development"}

--mount 문법으로 Bind Mount

Docker
# 더 명시적인 --mount 문법
docker run -d \
  --name flask-dev-v2 \
  -p 5001:5000 \
  --mount type=bind,source=/tmp/docker/part2/volumes-demo/flask-dev,target=/app \
  -w /app \
  python:3.11-slim \
  sh -c "pip install -r requirements.txt -q && python app.py"

# 읽기 전용 Bind Mount (설정 파일 주입 시 유용)
docker run -d \
  --mount type=bind,source=/etc/myapp/config.json,target=/app/config.json,readonly \
  myapp

Bind Mount 사용 시 주의사항

Docker
# 주의: Bind Mount는 컨테이너 경로의 기존 파일을 숨김
# 예: node_modules가 컨테이너에 있어도 호스트의 빈 디렉토리가 마운트되면 안 보임

# 해결: 소스코드 마운트 + node_modules는 Named Volume으로
docker run -d \
  -v /tmp/myapp:/app \                # Bind Mount (소스코드)
  -v node_modules:/app/node_modules \ # Named Volume (의존성)
  -p 3000:3000 \
  node:20-alpine \
  npm start

docker volume 명령어 완전 정복

목표

docker volume 서브커맨드를 사용하여 볼륨을 생성, 조회, 검사, 정리하는 방법을 익힙니다.

docker volume 서브커맨드

로컬 터미널
# 사용 가능한 서브커맨드 확인
docker volume --help
# Commands:
#   create      볼륨 생성
#   inspect     볼륨 상세 정보
#   ls          볼륨 목록
#   prune       사용하지 않는 볼륨 삭제
#   rm          특정 볼륨 삭제

docker volume create

로컬 터미널
# 기본 로컬 볼륨 생성
docker volume create my-data

# 레이블 추가 (메타데이터 관리)
docker volume create \
  --label project=myapp \
  --label environment=production \
  my-data-prod

# NFS 드라이버 사용 (원격 스토리지)
docker volume create \
  --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw \
  --opt device=:/nfs/shared \
  nfs-data

docker volume ls

로컬 터미널
# 전체 볼륨 목록
docker volume ls
# DRIVER    VOLUME NAME
# local     mysql-data
# local     redis-data
# local     5f3a2b1c...  ← 이름 없는 볼륨 (Dangling)

# 특정 드라이버만 필터링
docker volume ls --filter driver=local

# 레이블로 필터링
docker volume ls --filter label=environment=production

# Dangling 볼륨 (어떤 컨테이너에서도 미사용)
docker volume ls --filter dangling=true

# 볼륨 이름만 출력
docker volume ls -q

docker volume inspect

로컬 터미널
# 단일 볼륨 상세 정보
docker volume inspect my-data

# 여러 볼륨 동시 검사
docker volume inspect my-data redis-data

# 특정 필드만 추출
docker volume inspect my-data --format '{{.Mountpoint}}'
# /var/lib/docker/volumes/my-data/_data

docker volume inspect my-data --format '{{.CreatedAt}}'

docker volume rm과 prune

로컬 터미널
# 특정 볼륨 삭제 (사용 중인 볼륨은 에러)
docker volume rm my-data

# 사용 중인 볼륨 강제 삭제 (위험!)
# docker volume rm -f my-data  ← 이 옵션은 실제로 없음
# 대신: 연결된 컨테이너 먼저 삭제
docker volume rm my-data

# 사용하지 않는 볼륨 모두 삭제

# 확인 없이 즉시 실행

# 레이블 기반 삭제 (특정 프로젝트 볼륨만)

볼륨 내용 확인 및 조작

Docker
# 볼륨 내용 나열
docker run --rm -v my-data:/data alpine ls -la /data

# 볼륨에 파일 추가
docker run --rm -v my-data:/data alpine sh -c "echo 'test' > /data/test.txt"

# 볼륨에서 파일 복사
docker run --rm \
  -v my-data:/source \
  -v $(pwd):/dest \
  alpine cp /source/important.db /dest/

# 볼륨 크기 확인
docker run --rm -v my-data:/data alpine du -sh /data

문제 상황

로컬 터미널
$ docker run -d \
  -v /home/ubuntu/uploads:/app/uploads \
  --name myapp \
  myapp:1.0

$ docker exec myapp touch /app/uploads/test.txt
touch: cannot touch '/app/uploads/test.txt': Permission denied

또는 컨테이너 로그에서:

Error: EACCES: permission denied, open '/app/uploads/data.json'
PermissionError: [Errno 13] Permission denied: '/app/data'

원인 심층 분석

로컬 터미널
# 호스트 디렉토리 소유자 확인
ls -la /home/ubuntu/
# drwxr-xr-x 2 ubuntu ubuntu 4096 ... uploads/

# 컨테이너 내 프로세스 실행 UID 확인
docker exec myapp id
# uid=1001(appuser) gid=1001(appuser) groups=1001(appuser)

# 문제: 호스트 uploads 소유자 UID=1000(ubuntu), 컨테이너 앱 UID=1001(appuser)
# UID 1001은 UID 1000 소유 디렉토리에 쓰기 권한 없음

Linux는 파일 권한을 이름이 아닌 UID로 확인합니다. 호스트의 ubuntu(UID 1000)와 컨테이너의 appuser(UID 1001)는 이름이 달라도 UID가 같으면 같은 사용자로 인식합니다.

해결 방법

방법 1: 호스트 디렉토리 권한 조정

로컬 터미널
# 컨테이너 앱이 쓸 수 있도록 권한 부여

# 또는 다른 사람도 쓸 수 있게 group 권한 추가
chmod g+w /home/ubuntu/uploads

# 또는 컨테이너 UID를 소유자로 변경
sudo chown 1001:1001 /home/ubuntu/uploads
위험 명령어모든 사용자에게 쓰기 권한을 열어 보안 사고와 파일 변조 위험이 생깁니다.

디렉토리 전체 쓰기 권한 부여

안전한 실행 조건: 권한 문제를 재현하는 임시 실습 디렉토리에서만 실행합니다.

실행 전 반드시 확인

  • 대상이 운영 데이터 경로가 아님을 확인했다
  • chown 또는 그룹 쓰기 권한 같은 더 안전한 대안을 검토했다
  • 실습 후 권한을 되돌릴 수 있다
chmod 777 /home/ubuntu/uploads

위 항목을 모두 확인한 후 복사할 수 있습니다

방법 2: Dockerfile에서 UID 맞추기

Dockerfile
# 호스트의 uploads 디렉토리 UID(예: 1000)와 동일한 UID로 사용자 생성
FROM python:3.11-slim

# 호스트 ubuntu 사용자와 동일한 UID:GID 사용
RUN groupadd -g 1000 appgroup && \
    useradd -u 1000 -g appgroup appuser

RUN mkdir -p /app/uploads && chown appuser:appgroup /app/uploads
USER appuser

방법 3: --user 플래그로 UID 지정

Docker
# 컨테이너를 호스트 디렉토리 소유자 UID로 실행
docker run -d \
  --user 1000:1000 \
  -v /home/ubuntu/uploads:/app/uploads \
  myapp:1.0

# 현재 사용자 UID/GID로 자동 설정
docker run -d \
  --user $(id -u):$(id -g) \
  -v $(pwd)/uploads:/app/uploads \
  myapp:1.0

방법 4: Named Volume 사용으로 문제 회피

Docker
# Named Volume은 Docker가 권한을 자동 관리하므로 UID 문제가 적음
docker run -d \
  -v app-uploads:/app/uploads \
  myapp:1.0

방법 5: tmpfs로 임시 해결 (테스트용)

Docker
# 임시 메모리 마운트 (UID 문제 없음)
docker run -d \
  --tmpfs /app/uploads:rw,size=100m \
  myapp:1.0

진단 스크립트

로컬 터미널
#!/bin/bash
# Volume Permission 진단 스크립트
CONTAINER=$1
MOUNT_PATH=$2

echo "=== 컨테이너 내 실행 사용자 ==="
docker exec $CONTAINER id

echo "=== 마운트 경로 권한 ==="
docker exec $CONTAINER ls -la $(dirname $MOUNT_PATH)

echo "=== 쓰기 테스트 ==="
docker exec $CONTAINER touch $MOUNT_PATH/permission_test.tmp && \
  echo "쓰기 가능" || echo "Permission Denied!"

docker exec $CONTAINER rm -f $MOUNT_PATH/permission_test.tmp 2>/dev/null

문제 상황

MySQL 컨테이너 실행 시 데이터가 없다는 에러:

Docker
docker run -d \
  -v /home/user/mysql-data:/var/lib/mysql \  # Bind Mount!
  -e MYSQL_ROOT_PASSWORD=secret \
  mysql:8.0

docker logs mysql-server
# [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist

또는 Node.js 앱에서:

Docker
docker run -d \
  -v $(pwd):/app \       # 소스코드 Bind Mount
  node:20-alpine \
  npm start

docker logs node-app
# Error: Cannot find module 'express'  ← node_modules가 없음!

원인 분석

Bind Mount는 호스트 경로의 내용으로 컨테이너 경로를 덮어씁니다.

MySQL 케이스:
  이미지에는 /var/lib/mysql에 초기 MySQL 파일들이 있음
  -v /home/user/mysql-data:/var/lib/mysql 실행 시
  → 비어있는 /home/user/mysql-data가 /var/lib/mysql을 덮어씀
  → 초기 MySQL 파일들이 안 보임 → 에러

Node.js 케이스:
  이미지 빌드 시 npm install로 /app/node_modules가 생성됨
  -v $(pwd):/app 실행 시
  → 로컬 소스코드(node_modules 없음)가 /app 전체를 덮어씀
  → node_modules가 안 보임 → 에러

해결 방법

MySQL: Bind Mount 대신 Named Volume 사용

Docker
# Named Volume은 이미지의 초기 파일을 자동으로 채워줌
docker run -d \
  -v mysql-data:/var/lib/mysql \  # Named Volume!
  -e MYSQL_ROOT_PASSWORD=secret \
  --name mysql-server \
  mysql:8.0

Node.js: node_modules는 Named Volume으로 분리

Docker
# Bind Mount + node_modules는 Named Volume
docker run -d \
  -v $(pwd):/app \                    # 소스코드 (Bind Mount)
  -v app_node_modules:/app/node_modules \  # 의존성 (Named Volume)
  -p 3000:3000 \
  --name node-dev \
  node:20-alpine \
  npm start

또는 개발 전용 Dockerfile 작성

Dockerfile
# Dockerfile.dev
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
# 소스코드는 Bind Mount로 주입
EXPOSE 3000
CMD ["npm", "run", "dev"]
Docker
docker build -f Dockerfile.dev -t myapp:dev .
docker run -d \
  -v $(pwd)/src:/app/src \  # src 디렉토리만 Bind Mount
  -p 3000:3000 \
  myapp:dev

예방 원칙

상황권장 방법
DB 데이터 (MySQL, PostgreSQL, Redis)Named Volume
개발 소스코드Bind Mount
컨테이너 초기 파일 덮어쓰기 필요 없는 경우Named Volume
호스트 파일을 컨테이너에 주입 (설정, 인증서)Bind Mount

💼
실무 맥락
현업 패턴

실제 서비스에서의 볼륨 설계

스테이트풀(Stateful) vs 스테이트리스(Stateless) 서비스

현대 컨테이너 기반 아키텍처에서는 가능한 한 서비스를 스테이트리스로 설계합니다:

스테이트리스 서비스 (컨테이너 적합):
  API 서버, 웹 서버, 비즈니스 로직
  → 언제든 종료/재시작/스케일 아웃 가능

스테이트풀 서비스 (볼륨 관리 필요):
  데이터베이스 (PostgreSQL, MySQL, MongoDB)
  캐시 (Redis, Memcached with persistence)
  메시지 큐 (Kafka, RabbitMQ)
  파일 스토리지

클라우드 환경에서의 볼륨 전략

Docker
# AWS ECS + EBS Volume
docker run -d \
  --mount type=volume,source=my-ecs-volume,target=/data \
  --volume-driver rexray/ebs \
  mydb

# Kubernetes에서는 PersistentVolumeClaim(PVC) 사용
# (Docker Volume의 Kubernetes 버전)

docker-compose.yml에서의 볼륨 관리

실제 서비스에서는 docker-compose를 통해 여러 서비스의 볼륨을 통합 관리합니다:

YAML
# docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    restart: unless-stopped

  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - app-uploads:/app/uploads
      - ./config:/app/config:ro  # 설정 파일 읽기 전용
    depends_on:
      - db
      - redis

volumes:
  postgres-data:
    driver: local
    labels:
      - "com.example.backup=daily"
  redis-data:
  app-uploads:

정기 백업 자동화

로컬 터미널
# 백업 스크립트 예시 (/usr/local/bin/docker-backup.sh)
#!/bin/bash
BACKUP_DIR="/backup/docker-volumes"
DATE=$(date +%Y%m%d_%H%M%S)

mkdir -p "$BACKUP_DIR"

for VOLUME in postgres-data redis-data app-uploads; do
  echo "볼륨 백업 중: $VOLUME"
  docker run --rm \
    -v ${VOLUME}:/source:ro \
    -v ${BACKUP_DIR}:/backup \
    alpine:3.18 \
    tar czf /backup/${VOLUME}_${DATE}.tar.gz -C /source .

  echo "완료: ${BACKUP_DIR}/${VOLUME}_${DATE}.tar.gz"
done

# 30일 이상 오래된 백업 삭제
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +30 -delete

# cron에 추가: 매일 새벽 2시 백업
# 0 2 * * * /usr/local/bin/docker-backup.sh >> /var/log/docker-backup.log 2>&1

볼륨 관리는 단순해 보이지만 데이터 손실은 서비스에 치명적입니다. 처음부터 올바른 볼륨 전략을 설계하고, 백업과 복원을 정기적으로 테스트하는 것이 프로덕션 운영의 핵심입니다.


💡개념

컨테이너 간 볼륨 공유와 초기화 패턴

API 서버가 /app/uploads에 이미지를 저장하면, nginx는 그 파일을 정적으로 서빙해야 합니다. 두 컨테이너가 같은 파일에 접근해야 하는데, 각자 독립된 파일시스템을 갖습니다. 이 때 두 컨테이너가 동일한 Named Volume을 마운트하면 공유 파일시스템처럼 동작합니다. 또 다른 패턴은 앱 컨테이너가 시작하기 전에 초기화 컨테이너가 볼륨에 설정 파일이나 시드 데이터를 넣어두는 것입니다. 이 방식은 Kubernetes의 init container 패턴의 Docker 버전이기도 합니다. 이 ConceptBlock에서는 여러 컨테이너의 볼륨 공유 방법과 볼륨 초기화 컨테이너 패턴을 다룹니다.

볼륨 공유 패턴 — 여러 컨테이너가 동일 Named Volume 마운트

여러 컨테이너가 하나의 볼륨을 공유하기

Named Volume은 여러 컨테이너가 동시에 마운트할 수 있습니다. 파일 업로드 처리, 공유 설정, 정적 파일 서빙 등에 활용됩니다.

Docker
# uploads 볼륨을 앱 서버와 nginx가 공유
docker run -d --name app \
  -v uploads:/app/uploads \
  myapp:latest

docker run -d --name nginx \
  -v uploads:/usr/share/nginx/html/uploads:ro \  # ro = 읽기 전용
  -p 80:80 \
  nginx:alpine

이제 app 컨테이너가 /app/uploads에 파일을 저장하면, nginx/usr/share/nginx/html/uploads에서 바로 서빙합니다.

볼륨 초기화 컨테이너 패턴

컨테이너 첫 실행 전에 볼륨에 초기 데이터를 넣어야 할 때 사용합니다.

Docker
# 1. 초기 데이터 볼륨에 주입 (busybox 임시 컨테이너 사용)
docker run --rm \
  -v myapp-config:/config \
  busybox sh -c "
    echo 'LOG_LEVEL=info' > /config/app.conf
    echo 'MAX_CONN=100'  >> /config/app.conf
    echo 'TIMEOUT=30'    >> /config/app.conf
  "

# 2. 앱 컨테이너가 해당 볼륨 사용
docker run -d \
  --name myapp \
  -v myapp-config:/etc/myapp:ro \
  myapp:latest

Named Volume의 이미지 기본값 채우기 동작

빈 볼륨을 마운트하면 이미지의 해당 경로 파일이 자동으로 복사됩니다. 이미 데이터가 있는 볼륨은 이미지 기본값으로 덮어쓰지 않습니다.

Docker
# postgres:16 예시
# /var/lib/postgresql/data 에 빈 Named Volume 마운트 시
# → Docker가 자동으로 PostgreSQL 초기화 파일을 볼륨으로 복사

docker run -d \
  --name postgres \
  -v postgres-data:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16-alpine

# 로그에서 초기화 확인
docker logs postgres
# PostgreSQL init process complete; ready for start up.
로컬 터미널
# 이미 데이터가 있는 볼륨은 재사용 (초기화 안 함)
docker stop postgres && docker rm postgres
docker run -d \
  --name postgres-new \
  -v postgres-data:/var/lib/postgresql/data \   # 기존 볼륨 재사용
  -e POSTGRES_PASSWORD=newsecret \
  postgres:16-alpine
# 기존 데이터 그대로 유지됨 (패스워드 변경도 안 됨)

💡개념

볼륨 백업과 복구 — 실전 패턴

운영 서버에서 docker system prune --volumes를 실수로 실행하거나, 디스크가 갑자기 꽉 차서 컨테이너를 강제로 정리했을 때 DB 데이터가 복구 불가능하게 사라지는 사고가 발생합니다. Named Volume에 데이터를 저장한다고 해서 자동으로 백업되는 것은 아닙니다. 별도로 백업 절차를 만들어야 합니다. Docker에서 볼륨 백업은 가벼운 임시 컨테이너를 활용해서 볼륨을 tar로 압축하는 방식을 씁니다. 이 방법은 원본 컨테이너가 실행 중이어도 동작하고, 결과물이 일반 파일이라 scp나 S3로 이전이 쉽습니다. 이 ConceptBlock에서는 볼륨 백업, 복구, 다른 서버 이전 패턴을 다룹니다.

볼륨 백업과 복구 — tar 기반 임시 컨테이너 활용 패턴

tar로 볼륨 백업하기

Docker 볼륨을 백업할 때는 busybox 같은 가벼운 임시 컨테이너를 활용합니다. 볼륨을 마운트하고 tar로 압축하는 방식입니다.

Docker
# 볼륨 백업: 볼륨 + 현재 디렉토리 동시에 마운트
docker run --rm \
  -v mysql-data:/data \
  -v $(pwd):/backup \
  busybox \
  tar czf /backup/mysql-data-$(date +%Y%m%d-%H%M%S).tar.gz -C /data .

# 결과: 현재 디렉토리에 mysql-data-20260405-103045.tar.gz 생성
ls -lh mysql-data-*.tar.gz
# -rw-r--r-- 1 root root 45M Apr 5 10:30 mysql-data-20260405-103045.tar.gz

볼륨 복구하기

로컬 터미널
# 새 볼륨 생성 후 복구
docker volume create mysql-data-restored

docker run --rm \
  -v mysql-data-restored:/data \
  -v $(pwd):/backup \
  busybox \
  tar xzf /backup/mysql-data-20260405-103045.tar.gz -C /data

# 복구된 볼륨으로 컨테이너 실행
docker run -d \
  --name mysql-restored \
  -v mysql-data-restored:/var/lib/mysql \
  mysql:8

볼륨을 다른 서버로 이전하기

Docker
# 1. 원본 서버에서 백업
docker run --rm \
  -v app-data:/data \
  -v $(pwd):/backup \
  busybox tar czf /backup/app-data.tar.gz -C /data .

# 2. 백업 파일을 새 서버로 전송
scp app-data.tar.gz user@new-server:/tmp/

# 3. 새 서버에서 볼륨 생성 + 복구
ssh user@new-server "
  docker volume create app-data
  docker run --rm \
    -v app-data:/data \
    -v /tmp:/backup \
    busybox tar xzf /backup/app-data.tar.gz -C /data
"

실행 중인 컨테이너 백업 시 주의사항

Docker
# ❌ 주의: 데이터베이스 컨테이너 실행 중 파일 직접 백업은 일관성 보장 안 됨
# 쓰기 중인 파일을 tar로 복사하면 손상된 백업이 될 수 있음

# ✅ 올바른 방법: DB 덤프 명령어 사용
# PostgreSQL
docker exec postgres pg_dumpall -U postgres > backup.sql

# MySQL
docker exec mysql mysqldump -uroot -psecret --all-databases > backup.sql

# 덤프 파일을 볼륨에 저장하거나 외부로 전송

정리

핵심 개념 요약

  1. 컨테이너 휘발성: docker rm 시 컨테이너 레이어 데이터 소멸
  2. Named Volume: Docker 관리, 이식성 높음, DB 데이터 보존에 적합
  3. Bind Mount: 호스트 경로 직접 연결, 개발 환경 소스코드에 적합
  4. UID/GID 불일치: 퍼미션 에러의 주요 원인, UID 맞추기로 해결

핵심 명령어 요약

로컬 터미널
# Named Volume 생성 및 마운트
docker volume create my-data
docker run -v my-data:/app/data myapp

# Bind Mount
docker run -v $(pwd)/src:/app/src myapp
docker run --mount type=bind,source=$(pwd),target=/app myapp

# 볼륨 관리
docker volume ls                    # 목록
docker volume inspect my-data       # 상세 정보
docker volume rm my-data            # 삭제

# tmpfs (메모리 기반 임시)
docker run --tmpfs /app/tmp myapp

# 볼륨 백업
docker run --rm -v my-data:/src -v $(pwd):/backup \
  alpine tar czf /backup/data.tar.gz -C /src .

학습 경로 안내

이 챕터까지 완료하셨다면 Docker 핵심 기능의 기초를 모두 학습했습니다:

  1. 컨테이너 패러다임: VM vs 컨테이너, Docker 엔진 구조
  2. 컨테이너 생명주기: 상태 전환, 포트 바인딩
  3. 이미지와 레지스트리: 레이어 아키텍처, Docker Hub
  4. Dockerfile: 이미지 빌드, 멀티스테이지, 캐시 최적화
  5. 볼륨: 데이터 영속성, Named Volume, Bind Mount

다음 모듈에서는 컨테이너 간 통신과 Docker 네트워크를 다룹니다. 사용자 정의 네트워크를 만들어 컨테이너가 이름으로 서로를 찾는 흐름을 직접 확인합니다.

지식 확인

퀴즈 — 5문제

Q1

Docker 컨테이너를 `docker rm`으로 삭제하면 컨테이너 내부의 데이터는 어떻게 되나요?

Q2

Named Volume과 Bind Mount의 주요 차이점은 무엇인가요?

Q3

`-v /host/path:/container/path` 구문에서 콜론(:) 좌우의 의미는?

Q4

컨테이너 내부에서 마운트된 볼륨 경로에 쓰기가 안 되고 'Permission denied' 에러가 발생할 때 가장 흔한 원인은?

Q5

MySQL 컨테이너를 Named Volume과 함께 실행한 후, 컨테이너를 삭제하고 동일한 볼륨을 마운트하여 새 MySQL 컨테이너를 실행하면 어떻게 되나요?

0 / 5 답변

🧪 실습으로 확인하기

Docker Compose 멀티 서비스 구성

초급

docker-compose.yml로 nginx + 앱 컨테이너를 함께 정의하고, 서비스 간 통신과 볼륨 마운트를 구성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

docker중급 · 55
[Docker] 복잡한 멀티 서비스 환경의 유기적 연동과 배포
Docker 트랙 계속
networking입문 · 45
[Network] OSI 7계층과 TCP/IP 4계층 모델 실무적 관점 분석
Networking 트랙 시작점