Docker を使う時のデータ永続化の方法としていくつか方法があるらしいのでそれぞれ試してみる.

Docker でのデータ永続化

公式ドキュメントを確認してみると、Docker ではデータ永続化の手段として以下の 2 つのファイルシステムのマウント方法を提供しているらしい. (https://docs.docker.com/storage/)

  • ボリューム (volumes)

  • バインド・マウント (bind mounts)

(加えて、永続化しない in-memory ストレージとして tmpfs mount というマウントの設定も存在する.)

この中のどのマウント方式を選んだ場合であっても、コンテナの内部からはどれも全く同様のファイルとして見えるようだ.

types of mounts

各マウント方式の特徴は以下の通り.

  • Volumes

    • Docker 中においてデータを永続化するための最善の方法として推奨されている.

    • ホストのファイルシステムの中で、Docker によって管理される一部分 (Linux では /var/lib/docker/volumes/) にデータが保存される.

    • Docker 以外のプロセスはファイル・システム中のこの部分を書き換えることはできない.

  • Bind mounts

    • ホストシステム中のあらゆる場所にデータを保存できる.

    • 重要なシステムファイル・ディレクトリも指定できるのでセキュリティ上の問題が生じる可能性もある.

    • Docker のホスト上で動いている Docker 以外のプロセスや Docker コンテナはいつでもこれらのファイルを書き換えることができる.

  • tmpfs mounts

    • ホストのメモリ上にのみデータを保存できる. ホストのファイルシステムには書き込まれない.

docker-compose でのマウント設定の書き方

docker-compose.yml を使う場合、これらのマウント設定は各サービスの中の volumes に記述する. volumes の書き方は一行で書く書式と複数行で書く書式があり、特に一行で書く場合は Volumes を使うのか Bind mounts を使うのかの区別がつきにくい. (https://amateur-engineer-blog.com/docer-compose-volumes/)

まず、 Volumes を使う場合の一行での書き方は以下の通り.

services:
  service_name:
    volumes:
      # パス指定だけを行う場合. (無名ボリューム) Engine に自動的にボリュームを作成させる.
      - /var/lib/mysql
      # 名前付きボリューム. yaml ファイルの一番外側の volumes の中で名前を定義しておく必要がある.
      - mydata_volume:/var/lib/mysql

volumes:
  mydata_volume

次に Bind mounts を使う場合の一行での書き方は以下の通り.

services:
  service_name:
    volumes:
      # 絶対パス指定.
      - /opt/data:/var/lib/mysql
      # 相対パス指定. docker-compose.yml ファイルからの相対パスとなる.
      - ./cache:/tmp/cache
      # ユーザーディレクトリからの相対パス. アクセスモードを読み込み専用に設定.
      - ~/configs:/etc/configs/:ro

複数行で記述する場合には、以下のような項目を個別に記述することとなる.

  • type : マウントタイプ (volume / bind / tmpfs / npipe) を設定する.

  • source : マウント元.

  • target : マウントされるコンテナのパス.

  • read_only : 読み込み専用に設定する.

  • bind : バインドオプション.

    • propagation : バインドの伝播モード.

  • volume : ボリュームオプション.

    • nocopy : ボリューム生成時、コンテナからのデータのコピーを無効化する.

  • tmpfs : tmpfs オプション.

    • size : tmpfs マウントのサイズを指定する.

services:
  service_name:
    volumes:
      # Volumes の設定例
      - type: volume
        source: mydata_volume
        target: /data
        volume:
          nocopy: true

      # Bind mounts の設定例
      - type: bind
        source: ./static
        target: /opt/app/static

volumes:
  mydata_volume

各マウント方式を試してみる

以下、それぞれのマウント方式を試してみることにする. まずは Bind mounts のためにホスト環境にフォルダを作っておく.

$ mkdir docker-mount-test
$ cd docker-mount-test
$ mkdir mydata_bind

ここでは BusyBox イメージを使って最小限のコンテナ環境で作業してみることにする.


BusyBox は様々な標準 UNIX コマンドを単一バイナリにまとめた Swiss army knife のようなツール. 組み込み Linux 向けであり、root 権限でサーバー操作をミスしてしまって標準 UNIX コマンドへのパスが全く通らなくなってしまった場合などで頼ることもあるらしい. (https://qiita.com/S_Katz/items/a82554447491fb8079f0)

単一の BusyBox のバイナリ中におよそ 400 個もの標準コマンドが入っているらしい. (https://hetarena.com/archives/2864)

多くのコードを複数のコマンドで共有しているためファイルサイズも極めて小さい. 全部入りでも 1 MB 程度. (https://busybox.net/downloads/binaries/)

使用する場合には busybox psbusybox wget のように busybox コマンドのサブコマンドとして実行したいツールの名前を指定すればよい. Docker コンテナ中に入れると様々なツールがまとめて手に入って便利かも. (https://kazuhira-r.hatenablog.com/entry/2019/04/28/022717)


今回は以下のような docker-compose.yml を作成し、1 つの BusyBox コンテナに対して 3 つのマウント方式をそれぞれ試してみる.

docker-compose.yml
version: "3.8"

services:
  my-busybox:
    image: busybox:stable
    volumes:
      # Volumes
      - type: volume
        source: mydata_volume
        target: /my_volume
        volume:
          nocopy: true

      # Bind mounts
      - type: bind
        source: ./mydata_bind
        target: /my_bind

      # tmpfs mounts
      - type: tmpfs
        target: /my_tmpfs

    # バックグラウンドで busybox コンテナを動かすため、top を実行する.
    command: busybox top

volumes:
  mydata_volume:
    name: mydata_storage

コンテナを起動してみる.

$ docker compose up -d

起動したコンテナに対して inspect を実行してみると、マウント情報がきちんと記載されていることがわかる.

$ docker inspect docker-mount-test-my-busybox-1
[
    {
        "Id": "565212ba495d1d4e3cf5aed53c277267dd5f5564ffed7d8b941b1d382cb9c0b4",
        "Created": "2022-11-02T05:20:55.120213709Z",
        "Path": "busybox",
        "Args": [
            "top"
        ],
        ...
        "HostConfig": {
            ...
            "Mounts": [
                {
                    "Type": "volume",
                    "Source": "mydata_storage",
                    "Target": "/my_volume",
                    "VolumeOptions": {
                        "NoCopy": true
                    }
                },
                {
                    "Type": "bind",
                    "Source": "/host_mnt/Users/annpin/src/bitbucket.org/AnnPin/docker-mount-test/mydata
_bind",
                    "Target": "/my_bind"
                },
                {
                    "Type": "tmpfs",
                    "Target": "/my_tmpfs"
                }
            ],
            ...
        },
        ...
        "Mounts": [
            {
                "Type": "volume",
                "Name": "mydata_storage",
                "Source": "/var/lib/docker/volumes/mydata_storage/_data",
                "Destination": "/my_volume",
                "Driver": "local",
                "Mode": "z",
                "RW": true,
                "Propagation": ""
            },
            {
                "Type": "bind",
                "Source": "/host_mnt/Users/annpin/src/bitbucket.org/AnnPin/docker-mount-test/mydata_bin
d",
                "Destination": "/my_bind",
                "Mode": "",
                "RW": true,
                "Propagation": "rprivate"
            },
            {
                "Type": "tmpfs",
                "Source": "",
                "Destination": "/my_tmpfs",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
        "Config": {
            ...
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "busybox",
                "top"
            ],
            "Image": "busybox:stable",
            "Volumes": {
                "/my_bind": {},
                "/my_tmpfs": {},
                "/my_volume": {}
            },
            ...
        },
        ...
    }
]

次に、 busybox/bin/sh を実行してコンテナ内に入ってみる.

$ docker compose exec my-busybox busybox /bin/sh

ルートディレクトリ / の中身を確認してみると、 docker-compose.yml で指定したように /my_volume/my_bind/my_tmpfs というディレクトリがマウントされているようだ.

/ # ls
bin        etc        my_bind    my_volume  root       tmp        var
dev        home       my_tmpfs   proc       sys        usr

この中にそれぞれ echo でファイルを作成した上でコンテナの外に出てみよう.

/ # echo "Hello, volumes!" > /my_volume/volume.txt

/ # echo "Hello, bind mounts!" > /my_bind/bind.txt

/ # echo "Hello, tmpfs mounts!" > /my_tmpfs/tmpfs.txt

/ # cat /my_volume/volume.txt
Hello, volumes!

/ # cat /my_bind/bind.txt
Hello, bind mounts!

/ # cat /my_tmpfs/tmpfs.txt
Hello, tmpfs mounts!

/ # exit

コンテナを終了・削除する.

$ docker compose down

これにより、データ永続化が失敗していた場合には volume.txtbind.txttmpfs.txt という 3 つのファイルは完全に失われてしまうことになる.

データ永続化が成功したかを確認してみよう. まず、確認が簡単な Bind mounts から見てみる.

Bind mounts を使ってマウントしていた /my_bind ディレクトリの中身はホスト上の mydata_bind 中にすべて保存されているのが確認できる. これにより、 Bind mounts を使うことでデータ永続化が実現できるのがわかる.

$ ls ./mydata_bind
bind.txt

$ cat ./mydata_bind/bind.txt
Hello, bind mounts!

次に Volumes を使ってマウントしていた /my_volume を確認してみよう. Volumes の場合、ホスト OS 上の通常のファイルシステム上にはファイルは保存されずに Docker の volume 中に保存される.

まず、 docker volume ls を使って volume の一覧を確認してみよう.

$ docker volume ls
DRIVER    VOLUME NAME
...
local     ed57a4ac2599f3e340b2c42b21ab17f6986c2ee5685cc062e78bcb29b2173ee4
local     ee651ef72a7010385f51691d3f3a39e74aafc4ec7ea608c7d5bff489b0daaf15
local     mydata_storage

すると、 docker-compose.yml 中の volumes に指定した mydata_storage という名前の volume が作成されていることが確認できる.

ちなみに、 docker volume ls はフォーマット文字列を指定することで出力を変更することができる.

$ docker volume ls --format "{{.Mountpoint}}"
/var/lib/docker/volumes/0a0f2cd48b8a2b2eb92bdd3633f25f2eca51bcb44ed0fd3f25ae42061fb57d08/_data
/var/lib/docker/volumes/1d73ad43cede9d360a737c01aabea887c6d91149ade4d948f7e613f3ea54e676/_data
/var/lib/docker/volumes/3c4ce8682950529f75d33ecd2de5ef5b375ce782447755eb912dafe685351429/_data
/var/lib/docker/volumes/49b8963ab85ca21b398ed8d390db59d4ece1f9c956a85f7240e69c76f2b62de8/_data
/var/lib/docker/volumes/63f525cc59e323c291ab9f18192e74b360a92a319ad2f7e14aeca0fd831777e7/_data
/var/lib/docker/volumes/4320a5bcef9e03c60ea65e6f329a476ee98c459d356eb8c335209e2e3d67ecd4/_data
/var/lib/docker/volumes/6106d066d6fef6313d217fe1e0f36dfaf12f54074cc9915d9844a202a574bea7/_data
/var/lib/docker/volumes/375869738a11d81204598accd9691a590a26476fc63f32902ce72e0d972c0565/_data
/var/lib/docker/volumes/bbd2d25166a696fea0e5567f886f12dcdd97ad7f1fc0a24406433750bb001a62/_data
/var/lib/docker/volumes/ed57a4ac2599f3e340b2c42b21ab17f6986c2ee5685cc062e78bcb29b2173ee4/_data
/var/lib/docker/volumes/ee651ef72a7010385f51691d3f3a39e74aafc4ec7ea608c7d5bff489b0daaf15/_data
/var/lib/docker/volumes/mydata_storage/_data

この mydata_storage という volume の中身の確認はホスト OS 上では行えず、Docker コンテナを立ち上げることで確認する必要がある. 元のコンテナでなくとも、 --privileged を付与したり pid をホストとコンテナ内で揃えてやることによって権限問題をクリアすることで別のコンテナ内でこの volume をマウントして確認することもできる.

今回は再び同じコンテナを立ち上げ直すことで確認してみよう.

なお、 tmpfs mount したデータについてはデータ永続化が行われずにコンテナの削除と同時に消失しているはずである.

再びコンテナを生成し、コンテナ内に入って確認してみよう.

$ docker compose up -d

$ docker compose exec my-busybox busybox /bin/sh
/ # ls
bin        etc        my_bind    my_volume  root       tmp        var
dev        home       my_tmpfs   proc       sys        usr

/ # ls /my_volume
volume.txt

/ # cat /my_volume/volume.txt
Hello, volumes!

/ # ls /my_bind
bind.txt

/ # ls /my_bind/bind.txt
/my_bind/bind.txt

/ # ls /my_tmpfs

/ # exit

これにより、 Volumes や Bind mounts ではきちんとデータが永続化できており、tmpfs mounts ではデータの永続化がなされないということが確認できた.

最後に、 mydata_storage volume を削除してからコンテナを起動することで、 /my_volume の中身が空になることを確認しよう.

# コンテナを停止・削除.
$ docker compose down

# mydata_storage volume を削除.
$ docker volume rm mydata_storage
mydata_storage

# 削除されたか確認.
$ docker volume ls
DRIVER    VOLUME NAME
...
local     ed57a4ac2599f3e340b2c42b21ab17f6986c2ee5685cc062e78bcb29b2173ee4
local     ee651ef72a7010385f51691d3f3a39e74aafc4ec7ea608c7d5bff489b0daaf15

# 再度コンテナを起動.
$ docker compose up -d

# コンテナ内に入る.
$ docker compose exec my-busybox busybox /bin/sh

確認してみると、確かに /my_volume の中が空になった.

/ # ls
bin        etc        my_bind    my_volume  root       tmp        var
dev        home       my_tmpfs   proc       sys        usr

/ # ls /my_volume

/ # ls /my_bind/
bind.txt

/ # cat /my_bind/bind.txt
Hello, bind mounts!

/ # exit

ちなみに、BusyBox コンテナ自体はすごーく軽い. 動いているプロセスを busybox top で調べてみるとこんな感じ. (busybox top が 2 つ出ているのは docker-compose.ymlcommandbusybox top と指定したため.)

Mem: 1337872K used, 6803340K free, 342916K shrd, 36940K buff, 698288K cached
CPU:  0.4% usr  0.5% sys  0.0% nic 99.0% idle  0.0% io  0.0% irq  0.0% sirq
Load average: 0.00 0.00 0.00 2/573 12
  PID  PPID USER     STAT   VSZ %VSZ CPU %CPU COMMAND
    6     0 root     S     1388  0.0   2  0.0 busybox /bin/sh
    1     0 root     S     1384  0.0   2  0.0 busybox top
   12     6 root     R     1384  0.0   2  0.0 busybox top

自分が動かした覚えのないプロセスが動いてないってのはすごいな.