Docker bind mount 뒤 파일이 안 보이는 이유와 초기 파일 복사

호스트의 빈 폴더를 컨테이너에 연결했더니 이미지에 넣어 둔 설정 파일이 사라진 것처럼 보인다. 파일은 정말 없어졌을까, 호스트로 꺼낼 방법은 있을까?

Docker bind mount 뒤 파일이 안 보이는 이유와 초기 파일 복사
세 줄 요약
  • bind mount는 호스트 경로의 내용을 컨테이너 경로에 보여 주므로, 그 자리에 있던 이미지 파일은 가려진다.
  • 빈 volume에는 컨테이너의 기존 파일이 기본적으로 복사되지만, 이미 내용이 있는 volume은 그런 초기 복사를 하지 않는다.
  • 실제 Mounts와 Compose 경로를 읽은 뒤, 필요한 이미지 초기 파일은 별도 확인용 컨테이너에서 새 폴더로 복사할 수 있다.

빈 폴더를 연결하자 이미지 파일이 안 보이는 이유

Docker 이미지의 /app에 프로그램과 설정 파일이 있는데, 빈 호스트 폴더를 같은 위치에 연결하면 컨테이너 안에서도 빈 폴더처럼 보인다. bind mount는 두 폴더의 내용을 합치는 기능이 아니다. 연결한 호스트 폴더를 그 경로에서 보게 하므로 이미지에 있던 파일이 뒤로 가려진다. 마운트했다는 사실만으로 이미지의 원래 파일이 삭제되지는 않는다.

실제 Docker 포럼의 2024년 1월 질문도 같은 상황이었다. 사용자는 호스트의 빈 /tmp/myapp을 컨테이너의 /app에 연결했다. 기존 이미지 파일은 호스트에 나타나지 않았지만, 연결한 뒤 컨테이너 안에서 그 경로로 새로 옮겨 넣은 파일은 호스트에서도 보였다. 이후 옮겨 넣은 파일은 연결된 호스트 폴더에 기록됐고, 기존 이미지 파일은 자동 복사되지 않았기 때문이다.

Docker의 bind mount 문서는 기존 파일이 있는 위치에 마운트하면 그 파일이 가려진다고 설명한다. 예를 들어 호스트의 프로젝트 폴더를 /app 전체에 붙이면 코드뿐 아니라 이미지가 그 아래 설치해 둔 다른 파일도 보이지 않을 수 있다. 수정하려는 대상이 설정 파일 하나라면 폴더 전체보다 해당 파일만 연결하는 구성이 주변 파일을 그대로 쓰는 데 맞는다.

bind mount와 volume이 처음 보여 주는 내용

이미지의 초기 파일을 저장 공간에 자동으로 채우는 동작은 빈 Docker volume에서 제공된다. volume 문서에 따르면 비어 있는 볼륨을 기존 파일이 있는 컨테이너 경로에 연결하면 기본적으로 그 파일을 복사한다. volume-nocopy를 지정하면 이 복사를 생략한다. 호스트 폴더를 연결하는 bind mount에는 같은 기본 복사 동작이 없다.

연결 대상의 처음 상태컨테이너에서 보이는 내용이미지의 기존 파일 처리
빈 호스트 폴더를 bind mount한다.빈 호스트 폴더가 보인다.가려지며 호스트로 자동 복사되지 않는다.
파일이 있는 호스트 폴더를 bind mount한다.호스트 폴더의 파일이 보인다.기존 파일과 합쳐지지 않고 가려진다.
빈 volume을 연결한다.기본값에서는 대상 경로의 기존 파일로 채워진다.초기 파일이 복사되며 nocopy로 생략할 수 있다.
파일이 있는 volume을 연결한다.기존 volume의 파일이 보인다.이미지 쪽 내용은 가려진다.

이 조건은 이미지 업데이트 때도 영향을 준다. 이전 실행에서 설정 파일이 들어간 volume을 새 이미지에 다시 연결하면, 볼륨은 이미 비어 있지 않다. 따라서 새 이미지의 기본 설정을 자동으로 덮어써 주는 갱신 기능으로 기대할 수 없다. 초기 복사는 저장 공간이 비었을 때의 동작이므로, 새 버전의 설정 변경은 애플리케이션의 갱신 절차에 맞춰 반영해야 한다.

호스트 편집기로 소스나 설정을 직접 고칠 작업에는 bind mount가 편하다. 컨테이너가 생성하고 관리하는 데이터를 호스트의 특정 폴더 구조에 묶지 않고 보관하려면 Docker가 관리하는 volume을 사용할 수 있다.

실제로 연결된 경로 찾기

설정 파일에 적힌 의도보다 현재 컨테이너의 Mounts가 실제 연결 상태를 보여 준다. 다음 명령은 이미 있는 컨테이너를 조회하며, app은 실제 컨테이너 이름이나 ID로 바꾼다. 출력만 읽는 명령이므로 서비스를 다시 만들 필요는 없다.

docker inspect app --format '{{json .Mounts}}'

출력에서 Type이 bind라면 Source는 연결한 호스트 경로, Destination은 컨테이너 안에서 보이는 위치다. RW가 true면 해당 마운트가 쓰기 가능 모드라는 뜻이다. 파일시스템 권한까지 모두 허용됐다는 뜻은 아니지만, 예상한 읽기 전용 설정이 적용됐는지는 이 값으로 대조할 수 있다.

예를 들어 Source가 /srv/demo/config, Destination이 /app/config라면 컨테이너는 그 호스트 폴더를 설정 폴더로 본다. 호스트의 다른 위치인 /home/user/config를 편집해도 이 연결에는 반영되지 않는다. Type이 volume이면 Docker가 관리하는 볼륨을 연결한 것이며, Name 항목에서 그 이름을 찾을 수 있다. 작성한 bind mount 예제와 다른 설정으로 컨테이너가 만들어졌는지 살펴볼 수 있다.

여기서 호스트는 Docker 명령을 입력한 컴퓨터가 아니라 Docker 데몬이 실행되는 쪽이다. 원격 서버의 Docker를 사용한다면 Source도 그 서버의 경로다. Docker Desktop은 가상 머신과 로컬 파일을 연결하는 장치를 제공하지만, 원격 데몬이 내 노트북의 임의 경로를 그대로 읽는 것은 아니다. 같은 경로 문자열을 입력해도 어느 Docker 엔진에 연결했는지에 따라 실제 파일이 달라질 수 있다.

Compose에서 잘못된 빈 폴더가 생기는 경우

존재하지 않는 호스트 경로를 지정했을 때의 처리도 마운트 구문에 따라 다르다. Docker의 -v 방식은 없는 bind source를 디렉터리로 만든다. 그래서 설정 파일 이름을 잘못 적어도 그 이름의 폴더가 생길 수 있다. 반면 --mount type=bind는 기본적으로 source가 없으면 오류를 낸다. 별도의 생성 옵션을 켜지 않은 기본 동작의 차이다.

Compose의 volumes 짧은 구문도 호환성을 위해 없는 호스트 경로를 디렉터리로 만든다. 파일을 연결하려다 오타가 났다면, 새로 생긴 디렉터리 때문에 파일과 폴더 종류가 맞지 않는 오류까지 이어질 수 있다. 긴 구문에서는 bind.create_host_path: false로 이 자동 생성을 막고 잘못된 경로에서 작업이 멈추도록 할 수 있다.

다음은 이미 존재하는 설정 파일 하나를 읽기 전용으로 연결하는 예시다. my-app:1.0은 실제 사용할 이미지와 버전으로 바꾸고, target도 그 애플리케이션이 읽는 설정 파일 경로를 쓴다. 로컬 Docker 엔진과 단일 Compose 파일을 사용하는 이 예시에서 ./config/settings.yml은 Compose 파일이 있는 폴더를 기준으로 찾는다. 실행 전에 해당 위치에 올바른 파일이 있어야 한다. 원격 배포에 이 상대 경로 예제를 그대로 적용하지 않는다.

services:
  app:
    image: my-app:1.0
    volumes:
      - type: bind
        source: ./config/settings.yml
        target: /app/config/settings.yml
        read_only: true
        bind:
          create_host_path: false

이 구성은 /app/config 전체를 가리지 않고 지정한 파일만 바꿔 보이게 한다. read_only: true는 컨테이너가 이 마운트를 통해 파일을 수정하지 못하게 한다. 애플리케이션이 시작하면서 설정을 직접 저장하는 구조라면 이 설정과 맞지 않으므로, 읽기 전용 입력 파일과 쓰기 가능한 데이터 경로를 애플리케이션 방식에 맞춰 정해야 한다.

Compose가 해석한 최종 경로는 아래 명령으로 볼 수 있다. 서비스 실행 때 별도의 -f 파일을 사용했다면 같은 파일 지정을 붙여 조회한다. 여러 설정 파일이나 환경 변수가 끼어 있을 때는 원본 YAML 한 줄만 보는 것보다 실제 해석 결과를 읽는 편이 연결 대상을 찾기 쉽다.

docker compose config

config 명령은 변수 치환과 설정 병합 등을 거친 모델을 출력한다. 여기서 source와 target을 읽고 앞 절의 현재 컨테이너 Mounts와 비교한다. 설정을 수정했지만 컨테이너를 재생성하지 않았다면 두 결과가 다를 수 있다. 명령이 성공했다는 사실만으로 호스트의 파일 내용이나 실행 중인 컨테이너까지 바뀌지는 않는다. 출력에는 해석된 환경 변수 값도 들어갈 수 있으므로 외부 질문에 붙일 때는 필요한 마운트 부분만 사용한다.

이미지의 초기 설정을 새 폴더로 꺼내기

빈 호스트 폴더에 이미지의 기본 설정을 넣고 싶은 경우에는 먼저 이미지 쪽 파일을 복사하면 된다. 운영 컨테이너를 지워서 가림을 없애려 하지 않고, 같은 이미지로 확인용 컨테이너를 시작하지 않은 상태로 만든다. 이 컨테이너에는 문제가 된 bind mount를 연결하지 않으므로 이미지에 포함된 파일을 읽을 수 있다.

아래 예시는 로컬에 사용할 이미지가 있고 그 이미지에 /app/config가 포함된 경우다. 이미지 이름과 경로는 실제 값으로 바꾼다. image-files-check라는 컨테이너와 image-defaults라는 로컬 폴더가 이미 있다면 다른 새 이름을 쓴다. 각 명령이 성공한 것을 확인한 뒤 다음 명령을 실행한다.

docker create --name image-files-check my-app:1.0
mkdir image-defaults
docker cp image-files-check:/app/config/. ./image-defaults/

docker create는 컨테이너를 만들지만 프로세스를 시작하지 않는다. 따라서 시작 스크립트가 처음 실행될 때 만들어 내는 설정은 이 방법으로 얻지 못한다. 이미지에 미리 포함된 파일을 가져오는 절차이며, 애플리케이션이 실행 중에 만든 최신 설정이나 사용자의 운영 데이터를 복구하는 절차와는 범위가 다르다.

docker cp는 정지 상태의 컨테이너에도 사용할 수 있다. 예시의 원본 경로 끝에 붙은 /.은 config 폴더 자체를 한 단계 더 만드는 대신 그 안의 내용을 목적지에 복사한다. 새로 만든 목적지 폴더를 사용하면 이미 편집한 호스트 설정을 같은 이름으로 덮어쓰는 일을 피할 수 있다. 출력 오류가 없더라도 목적지에 기대한 파일이 생겼는지와 내용이 맞는지 읽는다.

가져온 파일을 그대로 운영 설정으로 바꾸기 전에 이미지 버전의 기본값과 기존 사용자 설정을 비교한다. 필요한 값을 편집한 파일을 앞 절의 host source 위치에 놓고, 그 경로로 컨테이너를 재생성하는 기존 서비스 절차를 사용한다. 재생성 전에 기존 컨테이너에만 남아 있는 필요한 변경을 보존해야 한다. 새 이미지에서 꺼낸 파일에는 그 변경이 들어 있지 않다.

복사 결과를 확보한 뒤에는 방금 만든 확인용 컨테이너만 다음 명령으로 제거할 수 있다. -v 옵션은 이 컨테이너에 연결된 익명 볼륨도 제거한다. 이 예시에서 새로 만든 확인용 이름에만 적용하며, 기존 운영 컨테이너 이름을 넣지 않는다. 이름 있는 볼륨은 이 옵션으로 제거되지 않는다.

docker rm -v image-files-check

초기 파일과 이후 저장된 데이터

마운트 뒤에 애플리케이션이 쓴 파일은 현재 연결된 저장 공간에 남는다. bind mount를 썼다면 지정한 호스트 경로를, volume을 썼다면 그 볼륨을 다음 컨테이너에도 연결해야 이어 사용할 수 있다. 새 빈 폴더나 다른 이름의 볼륨을 연결하면 애플리케이션은 다른 내용을 보게 된다. 기존 데이터가 남아 있는지 찾을 때도 먼저 전에 사용한 Source 또는 볼륨 이름부터 따라가면 된다.

volume의 내용은 개별 컨테이너의 생명주기 밖에 보관되지만, 컨테이너의 쓰기 레이어는 컨테이너가 제거되면 함께 사라진다. 설정을 바꾸며 컨테이너를 재생성하기 전에는 보존할 파일이 마운트 경로에 있는지 확인한다. 이미지에서 초기 파일을 다시 꺼낼 수 있어도 이후 작성한 문서나 설정 변경까지 되돌려 주지는 않으므로, 그 파일은 실제 저장 위치에서 별도로 보존해야 한다.