docker.svg

【Docker】データ管理(データマウント)

Docker

データ管理について

コンテナ内に作成したデータ(ファイル)は、基本的にはそのコンテナ内でのみ操作・利用できます。 つまりコンテナを削除すればコンテナ内のデータも一緒に削除されます。 またコンテナ内のデータをホストマシンや他のコンテナに移動させることは簡単ではありません。

そこでDockerでは、「ボリューム」と「バインドマウント」という方法を使用し、ホストマシン上にデータを保存することでデータの永続化及び共有を行います。 また一時的なデータの保存を目的とし、ホストマシンのメモリにデータを保存する「tmpfs マウント」と呼ばれる方法もあります。

以下よりこれらデータ管理に関することについて説明をしていきます。

ボリューム

ボリュームはDockerによって作成・管理するデータ領域です。 ホストマシン上のデータ領域ではありますが、基本的にはホストマシンからのアクセスは行わず(できず)、Dockerで管理するものとなります。

ボリュームの管理

ボリュームはdocker volume createコマンドによってあらかじめデータ領域を作成しておく必要があります。 イメージとしては、保存先のディレクトリを作成している感じです。

volume create
$ docker volume create test_volume
test_volume

作成したボリュームは、docker volume lsで一覧表示でき、docker volume inspectで詳細情報を表示できます。

volume ls
$ docker volume ls
DRIVER    VOLUME NAME
local     test_volume
volume inspect
$ docker volume inspect test_volume
[
    {
        "CreatedAt": "2024-02-18T13:08:07Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/test_volume/_data",
        "Name": "test_volume",
        "Options": null,
        "Scope": "local"
    }
]

詳細情報のMountpointは、ボリュームのデータが保存されているホストマシン上のパスになりますが、前述の通り基本的にはホストマシンからアクセスできません。

ボリュームを削除する場合は、docker volume rmまたはdocker volume pruneを使用します。 docker volume pruneは未使用のボリュームを一括で削除します。

ボリュームの削除
$ docker volume rm test_volume
test_volume
未使用ボリュームの一括削除
$ docker volume prune

ボリュームのマウント

コンテナにボリュームをマウントするには、docker container run-v(--volume)または--mountオプションを使用します。 基本的な機能は同じですが、--mountの方がより詳細な設定が可能です。 以下コマンドは同じ結果となります。

-vオプション
$ docker container run -it -v test_volume:/data ubuntu:20.04 bash
# {ボリューム:マウント先のコンテナのパス} を指定
--mountオプション
$ docker container run -it --mount type=volume,src=test_volume,dst=/data ubuntu:20.04 bash
# type: マウントの種類(volume)
# src: ボリューム名
# dst: マウント先のコンテナのパス

マウントした情報はdocker container inspectMountsで確認できます。

マウントの確認
$ docker container inspect --format "{{json .Mounts}}" 3076aeee0e8d 
[{"Type":"volume","Name":"test_volume","Source":"/var/lib/docker/volumes/test_volume/_data","Destination":"/data","Driver":"local","Mode":"z","RW":true,"Propagation":""}]

試しにマウントしたコンテナからボリュームにデータを保存し、別のコンテナからボリュームを参照してみます。

データの作成
$ docker container run -it -v test_volume:/data ubuntu:20.04 bash
root@4b7cbe2513f7:/# ls
bin  boot  data  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@4b7cbe2513f7:/# touch data/test.txt
root@4b7cbe2513f7:/# echo "Hello World" > data/test.txt
データの確認
$ docker container run -it -v test_volume:/data2 ubuntu:20.04 bash
root@ab5efdb8e84f:/# ls data2 
test.txt
root@ab5efdb8e84f:/# cat data2/test.txt
Hello World

結果の通り、コンテナ間でデータ共有ができることがわかります。

マウントは複数可能で、その数だけオプションを指定します。

複数マウント
$ docker container run -it --mount type=volume,src=test_volume,dst=/data --mount type=volume,src=test1_volume,dst=/data1 ubuntu:20.04 bash

また必要に応じてボリュームを読み取り専用に設定できます。 -vオプションはマウント先のディレクトリに:roを付与し、--mountオプションの場合はreadonlyを加えます。

-v 読み取り専用
$ docker container run -it -v test_volume:/data:ro ubuntu:20.04 bash
--mount 読み取り専用
$ docker container run -it --mount type=volume,src=test_volume,dst=/data,readonly ubuntu:20.04 bash

ボリュームのバックアップ

ボリュームのバックアップは次の手順で行います。

  1. バックアップ対象のボリュームをマウントする
  2. バックアップの保存先としてホストマシンのディレクトリをマウントする(バインドマウント)
  3. 1でマウントしたディレクトリを圧縮し、できた圧縮ファイルを2で作成したディレクトリに保存する

この手順を踏まえ、以下のコマンドでバックアップを行います。

ボリュームのバックアップ
$ docker container run --rm --mount type=volume,src=db,dst=/data --mount type=bind,src=$(pwd),dst=/backup ubuntu tar cvf /backup/backup.tar /data

dbというボリュームを/dataにマウントし、tarコマンドによって圧縮しています。 圧縮結果は、ホストマシンのカレントディレクトリをマウントした/backupbackup.tarとして保存しています。 結果的に、ボリュームdbbackup.tarとしてバックアップされたことになります。

復元についても考え方は同じです。

  1. 復元先のボリュームをマウントする
  2. バックアップファイルがあるホストマシンのディレクトリをマウントする(バインドマウント)
  3. 2でマウントしてディレクトリにある圧縮ファイルを、1でマウントしたディレクトリに展開する
バックアップの復元
$ docker container run --rm --mount type=volume,src=db2,dst=/data --mount type=bind,src=$(pwd),dst=/backup ubuntu bash -c "cd /data && tar xvf /backup/backup.tar --strip 1"

圧縮ファイルを保存したカレントディレクトリを/backupにマウントし、そこにあるbackup.tarを展開先であるdb2ボリュームをマウントした/dataに展開しています。

バインドマウント

バインドマウントはホストマシン上のディレクトリをコンテナにマウントします。 ボリュームとは異なり、ホストマシン上のディレクトリを直接指定するため、ホストマシンから干渉できます。 マウント方法はボリュームと同様にdocker container run-vまたは--mountオプションを使用します。

-v オプション
$ docker container run -it -v ./data:/data ubuntu:20.04 bash
# {ホストマシンのディレクトリ:マウント先のコンテナのパス}
--mount オプション
$ docker container run -it --mount type=bind,src=./data,dst=/data ubuntu:20.04 bash
# type: マウントの種類(bind)
# src: ホストマシンのディレクトリ
# dst: マウント先のコンテナのパス

以下がバインドマウントの実行例です。 コンテナからホストマシンのデータを参照・更新できていることがわかると思います。

bバインドマウントの実行例
$ mkdir data
$ touch data/test.txt
$ echo "Hello World" > data/text.txt
$ docker container run -it -v ./data:/data ubuntu:20.04 bash
root@14fa8687b465:/# cat data/test.txt
Hello World
root@14fa8687b465:/# echo "Hello New World" > data/test.txt
root@14fa8687b465:/# exit
exit
$ cat data/test.txt
Hello New World

ボリュームとバインドマウントの違い

ボリュームとバインとマウント、どちらもホストマシン上のデータ領域ではありますが、前述の通りボリュームはホストマシンからアクセスできず、Dockerによって管理されます。 つまり、ホストマシンから直接干渉されない点が大きな違いです。 バインドマウントはホストマシン上のディレクトリを直接マウントするため、どうしてもホストマシンからの影響を受けます。 そのためデータの共有や永続化を目的とする場合、一般的にはボリュームを使用することが推奨されています。

バインドマウントのユースケースとして、例えば開発時にビルドした結果をマウントし、実行はコンテナ内で行うなど、ホストマシン側からもコンテナ側からもアクセスが必要な場合に限ります。 前述のように、ホストマシン側からのアクセスが必要ない場合は、ボリュームを使用することが推奨されています。

tmpfs

ボリュームとバインドマウントはホストマシンのストレージをマウントしましたが、tmpfsはメモリにマウントし、一時的なデータ領域として使用します。 他の2つとは異なり、コンテナ停止時に削除されます。 またコンテナ間での共有もできません。

tmpfsのマウントは--mountオプションまたは--tmpfsオプションを利用します。 以下はどちらも/appディレクトリにマウントしています。

--mount オプション
$ docker container run -it --mount type=tmpfs,dst=/app ubuntu:20.04 bash
# type: マウントの種類(tmpfs)
# dst: マウント先のコンテナのパス
--tmpfs オプション
$ docker container run -it --tmpfs /app --name ubuntu1 ubuntu:20.04 bash

以下はコンテナを停止することで、データが削除されていることを確認しています。

動作確認
$ docker container run -it --tmpfs /app --name ubuntu1 ubuntu:20.04 bash
root@8b602e98dbed:/# ls
app  bin  boot  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@8b602e98dbed:/# touch app/test.txt
root@8b602e98dbed:/# ls app
test.txt
root@8b602e98dbed:/# exit
exit
$ docker container start ubuntu1
ubuntu1
$ docker container exec -it ubuntu1 bash
root@8b602e98dbed:/# ls app
# データなし

tmpfsの利用目的は、コンテナ内のデータに影響を与えずに、一時的なデータを保存することです。 例えば一連の処理の流れで作成して削除するような中間ファイルは、tmpfsに保存することでコンテナ内の他のデータに影響を与えず、かつ処理速度の面からもメリットがあります。

MySQLの構築

ここまでの内容をもとに、データの永続化を行なったMySQLのコンテナを作成します。 永続化にはdb-storeというボリュームを使用することとします。

ボリュームの作成
$ docker volume create db-store

MySQLイメージ:https://hub.docker.com/_/mysql

MySQLイメージの概要にある通り、コンテナ作成時に以下の設定を行います。

  1. 環境変数MYSQL_ROOT_PASSWORDにルートユーザーのパスワードを設定
  2. ボリュームは/var/lib/mysqlにマウント
MySQLコンテナの作成
$ docker container run --name test-mysql -e MYSQL_ROOT_PASSWORD=password --mount type=volume,src=db-store,dst=/var/lib/mysql -d mysql

コンテナを作成したら、実際にコンテナに接続してデータを作成してみます。

データの作成
$ docker container exec -it test-mysql bash
bash-4.4# mysql --password=password
mysql> create database test;
mysql> use test;
mysql> create table user(id int primary key, name varchar(10));
mysql> insert into user (id, name) values
    -> (1, 'Taro'),
    -> (2, 'Jiro'),
    -> (3, 'Saburo');
mysql> select * from user;
+----+--------+
| id | name   |
+----+--------+
|  1 | Taro   |
|  2 | Jiro   |
|  3 | Saburo |
+----+--------+
3 rows in set (0.00 sec)

簡単な例ではありますが、testというデータベースにuserテーブルを作成し、3つのデータを登録しました。 ではこのコンテナを削除して、同様に別のコンテナを作成します。

コンテナの削除・再作成
$ docker container stop test-mysql
$ docker container rm test-mysql
$ docker container run --name test-mysql2 -e MYSQL_ROOT_PASSWORD=password --mount type=volume,src=db-store,dst=/var/lib/mysql -d mysql

作成したtest-mysql2コンテナには先ほどと同じdb-storeボリュームをマウントしています。 ここから先ほど作成したデータが参照できるか確認をします。

データ永続化の確認
$ docker container exec -it test-mysql2 bash
bash-4.4# mysql --password=password
mysql> select * from test.user;
+----+--------+
| id | name   |
+----+--------+
|  1 | Taro   |
|  2 | Jiro   |
|  3 | Saburo |
+----+--------+
3 rows in set (0.01 sec)

結果の通り、データがしっかりとボリュームに反映され、永続化できていることが確認できました。

ここでは予め作成したボリュームをマウントしましたが、実はMySQLイメージではコンテナ作成時にボリュームが自動的に作成され、マウントされます。

MySQLマウント未指定の場合
$ docker container run --name test-mysql -e MYSQL_ROOT_PASSWORD=password -d mysql

$ docker volume ls
DRIVER    VOLUME NAME
local     282ecdb80caf7f7cd121f530f6357a84383e75b99f82c184e54a039df719d1cf

$ docker container inspect --format '{{json .Mounts}}' test-mysql
[
  {
    "Type":"volume",
    "Name":"282ecdb80caf7f7cd121f530f6357a84383e75b99f82c184e54a039df719d1cf",
    "Source":"/var/lib/docker/volumes/282ecdb80caf7f7cd121f530f6357a84383e75b99f82c184e54a039df719d1cf/_data",
    "Destination":"/var/lib/mysql",
    "Driver":"local",
    "Mode":"",
    "RW":true,
    "Propagation":""
  }
]

しかし、ボリューム名がランダムな上に長いため、コンテナとの関連性を考えた時に自身で作成したボリュームを使用するほうがよいでしょう。