Docker Compose 볼륨: Bind mount vs Named volume
Docker Compose에서 bind mount와 named volume을 선택하는 기준을 정리합니다. 설정 파일과 데이터 저장 시 발생하는 권한 문제, 볼륨 검사 및 마이그레이션 방법을 포함하여 운영 환경에서 데이터 손실을 방지하는 실무 가이드를 제공합니다.
Bind mount와 named volume: 간단한 답변
Docker Compose 볼륨에는 두 가지 종류가 있으며, 선택의 기준은 파일의 소유권이 누구에게 있는지에 달려 있습니다. 설정 파일, 템플릿, 정적 사이트와 같이 사용자가 직접 읽고 쓰는 파일에는 bind mount를 사용하십시오. 데이터베이스 파일, 검색 인덱스, 업로드된 미디어와 같이 애플리케이션이 소유하는 데이터에는 named volume을 사용하십시오. bind mount는 호스트의 특정 경로를 가리키므로 편집기로 직접 열 수 있습니다. named volume은 Docker가 생성하고 관리하는 저장소이며, Docker를 통해서만 접근할 수 있습니다.
두 방식 모두 서비스 내부의 동일한 volumes: 키 아래에 나타나기 때문에 혼동하기 쉽습니다. 차이점은 콜론의 왼쪽 부분에 있습니다. 왼쪽이 . 또는 /로 시작하면 호스트 경로를 의미하므로 bind mount입니다. 그 외의 것은 이름으로 간주되어 named volume이 되며, 이 이름은 최상위 volumes: 블록에도 반드시 선언되어야 합니다.
Compose 파일의 두 가지 문법
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data는 명명된 볼륨(named volume)입니다. ./nginx.conf:/etc/nginx/nginx.conf는 바인드 마운트(bind mount)이며, :ro은 이를 읽기 전용으로 마운트합니다. 컨테이너가 수정해서는 안 되는 설정 파일에는 이 방식이 올바른 기본값입니다. 최상위 수준의 volumes: 항목을 생략하면 Compose는 service "db" refers to undefined volume pgdata 오류와 함께 중단됩니다.
서비스를 실행하고 Docker가 생성한 항목을 확인합니다.
docker compose up -d
docker volume ls볼륨의 이름은 pgdata가 아닙니다. <project>_pgdata으로 생성되는데, 여기서 프로젝트 이름은 기본적으로 compose 파일이 포함된 디렉터리 이름을 따릅니다. 디렉터리 이름이 myapp이면 myapp_pgdata가 됩니다. 디렉터리 이름을 바꾸면 새로운 빈 볼륨이 생성되므로 애플리케이션의 데이터가 사라진 것처럼 보일 수 있어 주의해야 합니다. 실제로는 데이터가 사라진 것이 아니며, 이전 볼륨은 docker volume ls 명령으로 여전히 확인할 수 있습니다. 디렉터리 위치가 바뀔 가능성이 있다면 compose 파일 내에서 name:를 사용하여 이름을 고정하거나 COMPOSE_PROJECT_NAME를 설정하십시오. 이러한 설정은 다른 Compose 환경 파일 및 비밀값과 함께 관리하는 것이 좋습니다.
바인드 마운트에서만 권한 오류가 발생하는 이유
이는 가장 큰 실무적 차이점이며, 단 하나의 규칙에서 비롯됩니다. 처음 사용할 때 비어 있는 네임드 볼륨은 이미지로부터 데이터를 복제(seed)받지만, 바인드 마운트는 절대 그렇지 않습니다.
Docker가 이미지 내에 이미 콘텐츠가 있는 디렉터리 위에 빈 네임드 볼륨을 마운트하면, 이미지가 설정한 소유권과 모드를 유지한 채 해당 콘텐츠를 볼륨으로 복사합니다. 공식 Postgres 이미지는 /var/lib/postgresql/data를 자체 postgres 사용자의 소유로 배포하므로, 볼륨도 동일한 숫자 ID의 소유가 되어 데이터베이스가 정상적으로 시작됩니다.
바인드 마운트는 정반대로 동작합니다. 호스트에 있는 것은 무엇이든 컨테이너가 그대로 보게 되며, 소유권도 포함됩니다. 이 경로에 있던 이미지의 기존 콘텐츠는 가려집니다. 호스트 디렉터리가 존재하지 않으면 Docker 데몬이 이를 생성하는데, 데몬은 root 권한으로 실행되므로 결과적으로 root:root 소유의 디렉터리가 생성됩니다. 따라서 root가 아닌 사용자로 실행되는 컨테이너 프로세스는 해당 디렉터리에 쓰기 작업을 할 수 없습니다.
PermissionError: [Errno 13] Permission denied: '/data/app.db'해결책은 양쪽의 숫자 ID를 일치시키는 것입니다. 바인드 마운트에서의 소유권은 이름이 아닌 숫자 형태의 사용자 ID로 비교됩니다. 컨테이너는 고유한 /etc/passwd를 가지고 있기 때문입니다. 컨테이너 내부의 app이라는 사용자는 호스트에서 아무런 의미가 없습니다. 양쪽 모두에서 uid 1000은 동일한 uid 1000을 의미합니다.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./datadocker compose exec web id은 컨테이너 프로세스가 실제로 실행되는 uid를 출력합니다. 호스트 디렉터리의 소유자를 해당 번호로 맞추거나, 서비스 설정에서 user: "1000:1000"를 사용하여 컨테이너를 특정 번호로 고정하십시오. 직접 작성한 애플리케이션이라면 user:을 고정하는 방식이 더 깔끔합니다. 직접 작성하지 않은 이미지의 경우에는 호스트 디렉터리의 소유권을 변경(chown)하는 것이 더 안전합니다. 일부 이미지는 entrypoint를 root로 시작한 뒤 권한을 낮추며, 특정 소유권을 전제로 동작하기 때문입니다.
알아두어야 할 두 가지 함정이 더 있습니다. Fedora, RHEL 및 SELinux(security-enhanced Linux)가 적용된 시스템에서는 레이블을 재설정하기 전까지 바인드 마운트가 거부됩니다. 따라서 컨테이너 간 공유 경로라면 :z를, 단일 컨테이너만 사용하는 경로라면 :Z를 추가하십시오. 이는 - ./data:/data:Z과 같이 작성합니다. 또한 디렉터리가 아닌 단일 파일을 바인드 마운트할 경우, 편집기가 파일을 제자리에서 수정하지 않고 교체(replace)해버리면 마운트가 끊어집니다. 마운트는 원본 inode를 따라가기 때문입니다. 컨테이너는 재시작하기 전까지 계속 이전 콘텐츠를 보게 됩니다. 파일이 자주 수정된다면 부모 디렉터리를 마운트하십시오.
성능: 실제 차이가 발생하는 지점
Linux 서버에서는 두 방식 모두 동일한 커널 경로를 거치므로 처리량 차이는 무시할 수 있는 수준입니다. 따라서 이 기준을 선택의 근거로 삼아서는 안 됩니다. 기본 local 드라이버를 사용하는 Named volume은 Docker의 나머지 데이터와 함께 /var/lib/docker/volumes/ 경로 아래의 동일한 파일 시스템에 위치하며, Bind mount는 사용자가 지정한 위치에 생성됩니다.
성능 격차는 컨테이너가 가상 머신 내부에서 실행되는 macOS 및 Windows용 Docker Desktop에서 나타납니다. Bind mount는 호스트 파일 시스템에서 파일 공유 계층을 거쳐 가상 머신 내부로 연결되는데, Node.js 의존성 트리나 PHP 프레임워크 캐시처럼 작은 파일 작업이 많은 워크로드는 눈에 띄게 느려집니다. 반면 Named volume은 가상 머신 내부에 머무르므로 이러한 비용이 발생하지 않습니다. 이것이 많은 개발용 compose 파일이 소스 디렉터리는 Bind mount로 연결하면서도 node_modules 경로는 Named volume으로 선언하는 이유입니다.
또 다른 실질적인 차이는 데이터가 저장되는 위치입니다. /mnt/backup으로 설정된 Bind mount는 해당 디스크에 데이터를 기록합니다. 반면 Named volume은 /var/lib/docker이 위치한 파일 시스템을 사용하며, VPS 환경에서는 보통 루트 디스크가 됩니다. Named volume 내부에서 데이터베이스 용량이 커지면 시스템 로그가 저장된 디스크까지 가득 찰 수 있습니다. 장애로 이어지기 전에 미리 확인하십시오.
docker system df -v
df -h /var/lib/dockerdocker system df -v 명령은 모든 볼륨의 크기를 나열하며, 더 이상 컨테이너가 참조하지 않는 볼륨을 표시합니다.
Named volume 검사하기
Named volume은 블랙박스가 아닙니다. Docker를 통해 해당 볼륨의 위치를 확인하십시오.
docker volume inspect myapp_pgdataMountpoint 필드는 실제 호스트 경로를 제공하며, 일반적으로 /var/lib/docker/volumes/myapp_pgdata/_data입니다. sudo ls를 사용하여 내용을 읽을 수 있으며, 이는 간단한 확인 작업에 유용합니다. 해당 경로를 직접 수정하는 용도로 사용하지 마십시오. root 권한으로 파일을 작성하면 앞서 설명한 소유권 문제가 다시 발생하며, 해당 경로는 다른 볼륨 드라이버에서는 공유되지 않는 local 드라이버의 세부 구현 사항일 뿐입니다.
볼륨 내부를 안전하게 확인하는 방법은 해당 볼륨을 마운트하는 일회용 컨테이너를 사용하는 것입니다.
docker run --rm -v myapp_pgdata:/vol alpine ls -la /vol이 방법은 모든 드라이버에서 작동하며, 실제 컨테이너가 보는 것과 동일한 권한으로 내용을 확인할 수 있습니다. 또한 --rm 옵션 덕분에 작업 후 아무런 잔여물도 남지 않습니다.
각 유형별 백업
바인드 마운트는 일반적인 디렉터리이므로 파일 수준의 백업 도구로 이미 처리할 수 있습니다. 호스트 경로를 백업 대상으로 지정하면 완료됩니다. 네임드 볼륨은 도구가 내부로 접근해야 하므로 한 단계가 더 필요합니다. 볼륨과 호스트 디렉터리를 일시적인 컨테이너에 함께 마운트한 뒤 아카이브를 생성하십시오.
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .새 볼륨에 역순으로 복원하십시오.
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /datatar은 컨테이너 내부에서 root 권한으로 실행될 때 숫자 형태의 소유권을 유지하며, 이를 통해 복원된 볼륨을 애플리케이션에서 그대로 사용할 수 있습니다.
두 유형 모두 주의할 점이 있습니다. 데이터베이스가 실행 중일 때 파일을 복사하면 변경 중인 데이터의 아카이브가 생성되어, 복원 시 데이터가 손상될 수 있습니다. docker compose exec -T db pg_dump -U postgres appdb > appdb.sql와 같이 데이터베이스 자체 도구를 사용하여 덤프를 생성하거나, 먼저 서비스를 중지하십시오. 이렇게 생성된 일반 파일은 compose 파일과 함께 암호화된 restic 백업 루틴에 포함할 수 있습니다.
바인드 마운트를 네임드 볼륨으로 마이그레이션하기
이 작업은 이름 변경이 아닌 복사 과정이며, 약 1분 정도 소요됩니다.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'cp -a은 소유권, 모드, 타임스탬프를 유지하므로 기존 디렉터리를 읽을 수 있던 컨테이너 사용자가 새로운 볼륨도 그대로 읽을 수 있습니다. 이후 서비스를 수정하여 pgdata:/var/lib/postgresql/data을 사용하도록 변경하고, 최상위 volumes: 블록에 pgdata를 추가한 뒤, docker compose up -d를 실행하십시오. 기존 디렉터리를 삭제하기 전에 애플리케이션 로그를 확인해야 합니다. 반대 방향으로 작업할 때는 /from와 /to의 위치를 바꾸어 동일한 명령어를 사용합니다.
테스트 시 한 가지 주의할 점이 있습니다. docker compose down은 네임드 볼륨을 그대로 유지하지만, docker compose down -v은 프로젝트에 선언된 모든 네임드 볼륨을 삭제하며 되돌릴 수 없습니다. 바인드 마운트는 Docker가 해당 디렉터리의 소유권을 가지지 않으므로 두 명령어 모두에서 안전합니다. 라이프사이클 명령어가 아직 익숙하지 않다면 VPS를 위한 Docker Compose 기초 가이드에서 관련 내용을 확인하십시오.
서비스별 선택 기준
파일을 누가 작성하는지 확인하십시오. 텍스트 편집기로 수정하고 git에 커밋하는 설정 파일은 :ro에 마운트된 바인드 마운트(bind mount)에 위치해야 합니다. 이는 해당 파일을 눈으로 확인하고 버전 관리를 해야 하기 때문입니다. 사용자가 직접 열어볼 일이 없는 애플리케이션 상태 데이터는 네임드 볼륨(named volume)에 저장하십시오. Docker가 권한을 올바르게 설정하며, 데이터가 호스트 경로에 의존하지 않기 때문입니다.
미디어 파일은 혼합된 사례입니다. 사진 라이브러리는 애플리케이션이 작성하지만 사용자도 관리하며, 특정 디스크를 할당해야 할 만큼 용량이 큰 경우가 많습니다. 해당 디스크의 경로에 바인드 마운트하고, 소유권을 한 번 명확하게 설정하십시오. 이것이 대부분의 셀프 호스팅 스택이 채택하는 패턴입니다. 데이터베이스와 캐시에는 네임드 볼륨을 사용하고, 설정 파일과 관리 대상인 대용량 디렉터리에는 바인드 마운트를 사용합니다. VPS에서 실행되는 Chatwoot과 같은 지원 데스크 서비스가 정확히 이 방식을 따릅니다. Postgres는 네임드 볼륨에 저장하고, 업로드된 첨부 파일은 백업을 수행할 수 있는 경로에 배치합니다.
FAQ
bind mount와 named volume의 차이점은 무엇입니까?
bind mount는 호스트의 경로를 컨테이너 내부로 매핑하므로 양쪽에서 동일한 디렉터리를 볼 수 있으며 일반적인 도구로 편집할 수 있습니다. named volume은 Docker가 생성하고 관리하는 저장소로, 이름으로 참조하며 최상위 volumes: 블록에 선언합니다. 실무적인 구분 기준은 소유권입니다. 사용자가 관리하는 설정 파일에는 bind mount를, 애플리케이션이 관리하는 데이터에는 named volume을 사용합니다.
bind mount를 사용하면 왜 "permission denied" 오류가 발생하고 named volume은 그렇지 않습니까?
비어 있는 named volume은 이미지로부터 초기화되므로 이미지에 설정된 소유권을 상속받아 컨테이너 사용자가 쓰기 작업을 할 수 있습니다. bind mount는 호스트 디렉터리를 있는 그대로 보여주는데, Docker가 해당 디렉터리를 생성해야 했다면 소유권은 root가 됩니다. docker compose exec <service> id을 실행하여 컨테이너가 사용하는 숫자 ID를 확인한 다음, 호스트 디렉터리에 sudo chown -R <uid>:<gid>를 수행하거나 서비스에 user: "1000:1000"을 설정하십시오.
Docker는 named volume을 디스크 어디에 저장합니까?
기본 local 드라이버를 사용하는 경우 /var/lib/docker/volumes/<volume>/_data 아래에 저장되며, docker volume inspect <volume>을 실행하면 정확한 Mountpoint을 확인할 수 있습니다. 확인이 필요하면 읽을 수는 있지만, 쓰기 작업은 반드시 컨테이너를 통해서만 수행하십시오. 호스트에서 root 권한으로 직접 편집하면 컨테이너가 예상하지 못한 방식으로 소유권이 변경될 수 있기 때문입니다.
named volume은 어떻게 백업합니까?
볼륨과 호스트 디렉터리를 모두 마운트한 단기 컨테이너를 실행한 뒤, docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .을 사용하여 한쪽에서 다른 쪽으로 아카이브를 생성하십시오. 데이터베이스의 경우, 라이브 파일을 복사하면 쓰기 작업 중에 데이터가 손상된 상태로 복원될 수 있으므로 파일 복사 대신 데이터베이스 자체 도구를 사용하여 덤프를 생성하십시오.
docker compose down을 실행하면 볼륨이 삭제됩니까?
docker compose down는 컨테이너와 네트워크만 제거하며 named volume은 그대로 유지합니다. docker compose down -v은 프로젝트에 선언된 모든 named volume을 영구적으로 삭제합니다. bind mount는 호스트의 소유이며 Docker의 관리 대상이 아니므로 어떤 명령으로도 삭제되지 않습니다.