SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-10-28

Docker Compose 볼륨: Bind mount vs Named volume 차이

Docker Compose에서 Bind mount와 Named volume을 구분하는 기준을 정리합니다. 설정 파일과 데이터 저장소에 적합한 방식을 선택하고, service refers to undefined 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 환경 파일 및 비밀값과 함께 관리하는 것이 좋습니다.

bind mount에서 권한 오류가 주로 발생하는 이유

가장 큰 실무상 차이는 다음 규칙에서 비롯된다. 처음 사용할 때 비어 있는 named volume은 image에서 초기 데이터가 복사되지만, bind mount에는 그런 과정이 없다.

Docker가 image 안에 이미 내용이 있는 디렉터리에 비어 있는 named volume을 mount하면, image가 설정한 소유권과 mode를 유지한 채 해당 내용을 volume에 복사한다. 공식 Postgres image는 자체 /var/lib/postgresql/data를 자체 postgres 사용자가 소유하도록 제공하므로, volume도 동일한 numeric id가 소유하게 되고 database가 시작된다.

이 초기 데이터 복사는 named volume이 얻는 유일한 출발점이다. Docker는 volume의 저장 위치를 관리하지만, container는 일반 filesystem 작업으로 volume을 읽고 쓰므로 bind mount와 동일하게 소유권과 mode가 적용된다. image에 없는 경로에 named volume을 mount하거나, 다른 image 또는 사용자가 처음 채운 volume에는 image의 소유권이 적용되지 않는다. 따라서 non-root 프로세스도 해당 경로에서 쓰기가 거부될 수 있다. 아래의 id 확인은 두 방식 모두에 적용된다.

bind mount는 반대로 동작한다. host에 있는 내용이 소유권을 포함해 그대로 container에 표시되고, 해당 경로의 image 내용은 가려진다. host 디렉터리가 없으면 Docker daemon이 이를 생성한다. daemon은 root로 실행되므로 root:root가 소유한 디렉터리가 생성된다. 그러면 non-root 사용자로 실행되는 container 프로세스는 해당 디렉터리에 쓸 수 없다.

PermissionError: [Errno 13] Permission denied: '/data/app.db'

해결 방법은 숫자를 일치시키는 것이다. bind mount의 소유권은 이름이 아니라 numeric user id로 비교된다. container에는 자체 /etc/passwd가 있기 때문이다. container 내부의 app라는 사용자는 host에서 아무 의미가 없다. Uid 1000은 양쪽 모두에서 uid 1000을 의미한다. 단, daemon이 user-namespace remapping을 사용하는 경우는 예외다(userns-remap, /etc/docker/daemon.json 또는 dockerd command line에서 설정). Remapping은 모든 container id를 host의 subordinate range로 이동한다. Docker 예제에서 231072로 시작하는 range를 사용하면 container uid 0은 host uid 231072가 된다. 따라서 container uid 1000은 host uid 232072가 된다.

id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./data

docker compose exec web id은 container 프로세스가 실제로 어떤 uid로 실행되는지 출력한다. Remapping을 사용하지 않는다면 host 디렉터리를 해당 숫자에 맞추거나, service에서 user: "1000:1000"로 container를 자신의 숫자에 고정한다. Remapping을 사용하면 user:으로 container를 host의 사용자 번호에 고정할 수 없다. 설정한 id도 함께 이동하기 때문이다. 위의 예제 range에서는 user: "1000:1000"도 host uid 232072가 된다. 대신 host 디렉터리의 소유자를 매핑된 host id로 변경한다. 자신의 range는 /etc/subuid와 /etc/subgid에 있다. Docker 문서에서는 단일 container에서 remapping을 해제하는 방법으로 --userns=host을 docker container create, run 또는 exec에서만 지정한다. 직접 작성한 application에는 user:으로 고정하는 편이 더 명확하다. 직접 작성하지 않은 image에는 host 디렉터리의 소유자를 변경하는 편이 더 안전하다. 일부 image는 root로 entrypoint를 시작한 다음 권한을 낮추고, 하위 경로에 특정 소유권이 설정되어 있기를 기대하기 때문이다.

추가로 주의할 문제가 2가지 있다. Fedora, RHEL 및 SELinux(security-enhanced Linux)를 enforcing 상태로 사용하는 다른 시스템에서는 relabeling하기 전까지 bind mount가 거부된다. 따라서 여러 container가 공유하는 경로에는 :z를, 하나의 container만 사용해야 하는 경로에는 :Z를 - ./data:/data:Z 형식으로 추가한다. 또한 디렉터리가 아닌 단일 파일을 bind mount하면, editor가 파일에 직접 쓰지 않고 파일을 교체할 때 문제가 발생한다. mount는 원래 inode를 계속 참조하기 때문이다. 이 경우 container에는 이전 내용이 계속 표시되며, container를 재시작해야 새 내용이 표시된다. 파일을 자주 편집한다면 상위 디렉터리를 mount한다.

성능: 실제 차이가 발생하는 지점

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/docker

docker system df -v 명령은 모든 볼륨의 크기를 나열하며, 더 이상 컨테이너가 참조하지 않는 볼륨을 표시합니다.

Named volume 검사하기

Named volume은 블랙박스가 아닙니다. Docker를 통해 해당 볼륨의 위치를 확인하십시오.

docker volume inspect myapp_pgdata

Mountpoint 필드는 실제 호스트 경로를 제공하며, 일반적으로 /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 /data

tar은 컨테이너 내부에서 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에 커밋하는 설정 파일은 bind mount를 사용하여 :ro에 마운트해야 합니다. 이는 해당 파일을 눈으로 확인하고 버전 관리하기 위함입니다. 직접 열어볼 일이 없는 애플리케이션 상태 데이터는 named volume에 저장하십시오. Docker가 이미지로부터 권한을 자동으로 설정하며, 데이터가 호스트 경로에 의존하지 않기 때문입니다.

미디어 파일은 두 방식을 혼용합니다. 사진 라이브러리는 애플리케이션이 기록하지만 사용자가 직접 관리하기도 하며, 특정 디스크를 할당해야 할 만큼 용량이 큰 경우가 많습니다. 해당 디스크의 경로에 bind mount를 설정하고, 소유권을 한 번 명확하게 지정하십시오. 이것이 대부분의 셀프 호스팅 스택에서 정착된 패턴입니다. 데이터베이스와 캐시에는 named volume을, 설정 파일과 사용자가 관리하는 대용량 디렉터리에는 bind mount를 사용합니다. VPS에서 실행 중인 Chatwoot과 같은 지원 데스크 서비스가 정확히 이 방식을 따릅니다. Postgres는 named volume에 저장하고, 업로드된 첨부 파일은 백업 경로를 지정할 수 있는 위치에 둡니다.

FAQ

bind mount와 named volume의 차이는 무엇인가?

bind mount는 host의 경로를 container에 연결하므로 양쪽에서 같은 디렉터리가 보이며, 일반 도구로 편집할 수 있다. named volume은 Docker가 생성하고 관리하는 storage이며, 이름으로 참조하고 최상위 volumes: block에 선언한다. 실무상 구분 기준은 소유권이다. 직접 관리하는 config에는 bind mount를 사용하고, application이 관리하는 data에는 named volume을 사용한다.

bind mount에서는 "permission denied"가 발생하지만 named volume에서는 발생하지 않는 이유는 무엇인가?

비어 있는 named volume은 image에서 초기 데이터가 복사되므로 일반적으로 image가 설정한 소유권을 물려받고, container 사용자가 해당 volume에 쓸 수 있다. 그래도 권한은 적용되므로 image에 해당 경로가 없으면 같은 방식으로 실패할 수 있다. bind mount는 host 디렉터리를 현재 상태 그대로 표시한다. Docker가 해당 디렉터리를 생성해야 했다면 root가 소유하도록 생성한다. docker compose exec <service> id을 실행해 container가 사용하는 numeric id를 확인한 다음 host 디렉터리에 sudo chown -R <uid>:<gid>을 실행한다. 또는 daemon이 user namespace를 remap하지 않는 경우 service에 user: "1000:1000"을 설정한다. daemon이 user-namespace remapping을 사용한다면 매핑된 host id로 chown한다. container의 id가 host에서 이동되며 user:가 이를 변경하지 않기 때문이다.

Docker는 named volume을 디스크의 어디에 저장하는가?

기본 local driver를 사용하면 /var/lib/docker/volumes/<volume>/_data 아래에 저장되며, docker volume inspect <volume>는 정확한 Mountpoint을 출력한다. 확인이 필요할 때는 읽어도 되지만, host에서 직접 수정하지 말고 container를 통해서만 수정한다. host에서 root로 편집하면 소유권이 변경되어 container가 예상하지 못하는 상태가 될 수 있다.

named volume을 어떻게 백업하는가?

volume과 host 디렉터리를 모두 mount한 일회성 container를 실행한 다음, docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .으로 한쪽에서 다른 쪽으로 archive를 생성한다. database는 실행 중인 파일을 복사하지 말고 database 자체 도구로 dump한다. 쓰기가 진행 중일 때 파일을 복사하면 corrupt 상태로 복원될 수 있기 때문이다.

docker compose down은 volume을 삭제하는가?

docker compose down는 container와 network를 제거하지만 named volume은 그대로 둔다. docker compose down -v은 project가 선언한 모든 named volume도 삭제하며, 삭제된 volume은 영구적으로 사라진다. bind mount는 두 command 모두 제거하지 않는다. 해당 디렉터리는 Docker가 아니라 host에 속하기 때문이다.

이 글이 도움이 되었나요?