docker composeにおけるvolumeの挙動

対象は以下の4パターン

  1. bind mountでホスト側が空の場合
  2. bind mountでホスト側が空ではない場合
  3. local bindなnamed volumeのvolume mountでホスト側が空の場合
  4. local bindなnamed volumeのvolume mountでホスト側が空ではない場合

Dockerfileはこちら

FROM alpine:3.20

RUN mkdir -p /app/bind-1 \
    && mkdir -p /app/bind-2\
    && mkdir -p /app/named-bind-1 \
    && mkdir -p /app/named-bind-2

RUN echo "file in bind1" > /app/bind-1/file.txt \
    && echo "file in bind2" > /app/bind-2/file.txt \
    && echo "file in named-bind-1" > /app/named-bind-1/file.txt \
    && echo "file in named-bind-2" > /app/named-bind-2/file.txt

CMD ["sh", "-c", "sleep infinity"]

これをmountなしで起動する。

docker build -t volume-test-app .
docker run --rm -it volume-test-app sh

当然こうなる。

/ # tree app
app
├── bind-1
│   └── file.txt
├── bind-2
│   └── file.txt
├── named-bind-1
│   └── file.txt
└── named-bind-2
    └── file.txt

次に、docker composeで、先ほどあげたパターンでmountする。

services:
  app:
    build: .
    volumes:
      - ./host-bind-empty:/app/bind-1
      - ./host-bind-non-empty:/app/bind-2
      - named_bind_volume:/app/named-bind-1
      - named_bind_volume2:/app/named-bind-2

volumes:
  named_bind_volume:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ./host-named-bind-empty

  named_bind_volume2:
    driver: local
    driver_opts:
      type: none
      o: bind
      device: ./host-named-bind-non-empty

host側はこんな感じ。

.
├── Dockerfile
├── docker-compose.yml
├── host-bind-empty #1
├── host-bind-non-empty #2
│   └── host-file.txt
├── host-named-bind-empty #3
└── host-named-bind-non-empty #4
    └── host-named-bind-file.txt

実行。

docker compose build app
docker compose run --rm --no-deps app sh

コンテナ内を見るとこうなっている。

/ # tree app
app
├── bind-1 # イメージ内に存在したファイルが消えている。
├── bind-2 # ホスト側のファイルのみが存在する。
│   └── host-file.txt
├── named-bind-1 # イメージ内に存在したファイルがそのまま存在する。
│   └── file.txt
└── named-bind-2 # ホスト側のファイルのみが存在する。
    └── host-named-bind-file.txt

ホスト側はこう。

 tree
.
├── Dockerfile
├── docker-compose.yml
├── host-bind-empty # 空のまま。
├── host-bind-non-empty # 元のファイルがそのまま存在する。
│   └── host-file.txt
├── host-named-bind-empty # イメージ側のファイルが存在する。
│   └── file.txt
└── host-named-bind-non-empty # ホスト側のファイルがそのまま存在する。
    └── host-named-bind-file.txt

bind mountの場合、コンテナ内にもともと存在するファイルやディレクトリはホスト側によって上書きされる。
https://docs.docker.com/engine/storage/bind-mounts/?utm_source=chatgpt.com#bind-mounting-over-existing-data volume mountの場合、non-emptyなvolumeでマウントする場合は、bind mountと同様にホスト側によって上書きされる。しかし、emptyなvolumeでマウントする場合、コンテナ内に存在するファイルやディレクトリがコピーされる。
https://docs.docker.com/engine/storage/volumes/?utm_source=chatgpt.com#mounting-a-volume-over-existing-data

この挙動を利用すると、local bindなnamed volumeを定義して、そのvolumeでvolume mountをすることで、イメージ内にもともと存在するファイルをホスト側にコピーさせることが可能となる。
たとえばイメージビルド時にnpm installした内容をnode_modulesディレクトリを通じてホストと共有することできる。
その状態でコンテナ内で新たにnpm installすれば、ホスト側にもそれが共有される。