コンテンツにスキップ

データの永続化

コンテナ上のデータはコンテナを削除するとなくなってしまいます1。今回は、データを Docker イメージや Docker コンテナとは別に、保存する方法を学びます。これを、データの永続化(Data persistent)と呼びます。

バインドマウントの利用 | Docker ドキュメント

※本稿では、「コンテナ」は実行された Docker コンテナ、「イメージ」は実行されていない Docker イメージを指しています。

実行環境とデータの分離

コンテナ上のデータはコンテナを削除するとなくなってしまいます1。このように Docker が設計されている のは、アプリケーションの実行環境(=イメージ)とそのデータを分離するほうが都合が良いからです。たとえば、再利用性の向上やコンテナのパフォーマンス向上、イメージの容量削減などです。これを実現するために Docker では、Docker イメージはあくまでもアプリケーションの実行環境のテンプレートであり、実行に必要なデータや設定は別途保存し、実行時にそれを読み込む形を取っています。

たとえば、Dockerを使ってみるでは、Flask という Python ベースの Web サーバアプリケーションのコンテナを利用し、そこに自分で作った設定ファイルをコピーしました。具体的には、WebサーバのDockerイメージの作成 - 1. 作成したイメージのコミットで、イメージをcommitコマンドで更新し、Flaskの環境構築とWebサーバの起動 - 6. ファイルをコンテナへコピーで、docker container cp コマンドを利用して、app.pyをコンテナ上にコピーしました。このようにして、実行環境とデータが分離されていました。

  • 実行環境(イメージ、コンテナ): Flask
  • データ: app.py

実行環境とデータが分離されていない場合、どちらか一方のみを利用できません。Flask だけを再利用したり、データだけを別の人に渡したりする場合に、不必要なものまで渡すことになってしまいます。また、複数のコンテナからデータを共有・更新する場合にも、ネットワークを介さなければならないなど、非効率的な動作となってしまいます。このように、実行環境とデータは利用シーンが異なり、混ぜるべきではないと考えれています。これを「コンテナとデータのライフサイクルが違う」などと言います。

アプリケーションとコンテナのライフサイクルの違いとデータの永続化
アプリケーションとコンテナのライフサイクルの違いとデータの永続化(出典:コンテナにもエンジンが必要! ―その代表格と言える「Docker」とは | Think IT(シンクイット)

また、再利用を考慮しても、とあるデータがその実行環境に必要な場合は、そのデータを含めてイメージとして保存しておくことも可能です。たとえば、研究室で使う Flask のイメージには、研究室独自の設定データを同梱しておくほうが利便性は高いでしょう。

Docker におけるデータ管理 | Docker ドキュメントにより専門的な内容があります。

データの永続化

データの永続化とは、データをコンテナとは別のところに保存しておく方法のことを言います。具体的には、バインドマウントやボリュームという Docker のしくみを利用して、ホスト OS 上にデータを保持し、それを適宜コンテナで読み込みます。

以下の図は、公式ページのバインドマウントのページにあるデータの永続化の方法のイメージです。バインドマウントでは、コンテナがホスト OS 側のファイルシステムにアクセスします。 (出典:Use bind mounts _ Docker Documentation


Bind mounts | Docker Docs

バインドマウントによるデータの永続化

ここでは、バインドマウントを利用してデータを永続化する方法について学んでいきます。具体的には、Dockerを使ってみる - Flaskの環境構築とWebサーバの起動を変更し、バインドマウントを利用してデータの読み書きを行います。前提として、Flask の環境構築と Web サーバの起動 - 5. 実行ファイルの作成まで終了していることとします。なお、バインドマウントの利用 | Docker ドキュメントに、バインドマウントとコマンドの詳しい説明があります。

1. 環境設定

まず、Flaskの環境構築とWebサーバの起動 - 5. 実行ファイルの作成でホスト OS 上で作成した、app.pyファイルを再利用します。pwdコマンドでファイルが置いてあるディレクトリを一応確認してください。

$ ls
app.py
$ pwd
/tmp/semi1_docker/

2. バインドオプションの付与と実行

以下のコマンドを実行し、--mount type=bindオプションでホスト OS 上のapp.pyが入ったディレクトリを、コンテナ上の/bind_host_dirディレクトリにバインドマウントします。分かりやすいように、コンテナの名前もbase_bindに変更します。

docker container run --rm --mount type=bind,source="$(pwd)",target=/bind_host_dir --name base_bind -it -p 8080:80 python:3.7.5-slim bash

全体の意味: ホスト OS 上の現在のディレクトリ(pwdコマンド)を、コンテナ上の/bind_host_dirディレクトリにバインドマウント(type=bind)するものです。
また、それぞれのオプションの詳しい意味は、以下の通りです。

  • docker container run:新しいコンテナを作成して起動するコマンド
  • --rm:コンテナ停止時に自動的に削除する
  • --mount type=bind,source="$(pwd)",target=/bind_host_dir:ホストのカレントディレクトリ($(pwd))をコンテナ内の/bind_host_dirにバインドマウントする。ホストとコンテナ間でファイルを共有できる
  • --name base_bind:コンテナに「base_bind」という名前を付ける
  • -it:-i(標準入力を開いたままにする)と -t(擬似ターミナルを割り当てる)の組み合わせ。対話的なシェル操作が可能になる
  • -p 8080:80:ホストの8080番ポートをコンテナの80番ポートに割り当てる(ポートフォワーディング)
  • python:3.7.5-slim:使用するDockerイメージ名(Python 3.7.5の軽量版)
  • bash:コンテナ起動時に実行するコマンド(bashシェルを起動)

このコマンドは、Flaskの環境構築とWebサーバの起動 - 1. Pythonコンテナの実行で実行したコマンドに、バインドマウントオプションを追加しただけになります。なお、/bind_host_dirディレクトリは自動的にコンテナ上に作成されます。

なお、--volumeというオプションでもバインドマウントを行いことができます。こちらは、後方互換性を維持するために引き続き用意されているもので、--mountとは微妙に挙動が異なります。

3. ファイルの確認

lsコマンドで/bind_host_dirディレクトリが存在し、そこにapp.pyファイルが格納されていることを確認してください。

root@851a057916dd:/# ls /bind_host_dir/
app.py

また、catコマンドでファイルの中身を一応確認します。

root@851a057916dd:/# cat /bind_host_dir/app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')

def hello_world():
    return "[JIBUN NO SUKINA BUNSHOU]"

app.run(debug=True, host='0.0.0.0', port=80)

4. Flaskの起動と確認

まず、以下の 2 つのコマンドをそれぞれ実行し、Flask のインストールと Web サーバの起動を行いましょう。これは、Flaskの環境構築とWebサーバの起動 - 1. Pythonコンテナの実行Flaskの環境構築とWebサーバの起動 - 8. Webサーバの起動と同様です。

  • Flask のインストール: pip install flask

  • Web サーバの起動: python -u /bind_host_dir/app.py

Web サーバが起動したら、Flaskの環境構築とWebサーバの起動 - 8. Webサーバの起動と同じ手順で、メッセージが正しく表示されているかを確認してみましょう。

5. ホストOS側からのファイルの変更

app.pyの内容をホスト OS 側からの変更してみます。まずは、ホスト OS 側でgedit app.pyコマンドをたたき、app.pyを編集します。具体的には、以下のようにreturnメッセージの部分を変更します。ここでは、"This file has been changed from CentOS at 12:11"として、ファイルを変更した時間も入れてみました。

from flask import Flask
app = Flask(__name__)

@app.route('/')

def hello_world():
    return "This file has been changed from CentOS at 12:11"

app.run(debug=True, host='0.0.0.0', port=80)

6. コンテナ上でのファイルの確認

バインドマウントされたディレクトリのデータに変更があると、OS を介してその変更はコンテナ上にも即座に反映されます。コンテナでファイルが変更されたかどうかを確認してみましょう。

root@851a057916dd:/# cat /bind_host_dir/app.py
from flask import Flask
app = Flask(__name__)

@app.route('/')

def hello_world():
    return "This file has been changed from CentOS at 12:11"

app.run(debug=True, host='0.0.0.0', port=80)

7. Webページのリロードと確認

ホスト OS のブラウザをリロードし、メッセージが変更されることを確認してみましょう。

チェックポイント:できたら見せてください

ボリュームによるデータの永続化

ボリュームを用いたデータの永続化の目的は、バインドマウントと同様に、ファイルをホスト OS 側に置くことでデータの消失を防ぐものです。バインドマウントとユースケースが少し異なり、ホスト側でデータを読み書きしない場合にとても便利です。また、Docker がボリュームを管理してくれるため、メンテナンス性やデータの共有の点でも優れています。なお、ボリュームの利用 | Docker ドキュメントに、バインドマウントとコマンドの詳しい説明があります。

以下の図は、公式ページのバインドマウントのページにあるデータの永続化の方法のイメージです。ボリュームでは、Docker が事前にホスト OS 側のファイルシステム上に専用のデータ領域(図中では"Docker area")を作成し、実行時にそこにアクセスします。
(出典:Use bind mounts _ Docker Documentation

ここでは、Flask のバージョンを変更するというユースケースで、ボリュームによるデータの永続化を試します。

1. 環境設定

まず、ホスト OS 上で以下のような、display_python_version.pyを用意します。 hello_world関数のreturn文を変更し、Python のバージョンが表示できるようにします。

from flask import Flask
import sys
app = Flask(__name__)

@app.route('/')

def hello_world():
    return ("Python version: " + sys.version) ## ここを変更

app.run(debug=True, host='0.0.0.0', port=80)

2. ボリュームの作成

docker volume createコマンドでボリュームを作成し、docker volume lsコマンドでまさしく作成されたかを確認します。

semi1_test_volumeボリュームを作成

$ docker volume create semi1_test_volume
semi1_test_volume

ホスト OS 上での Docker が管理するボリュームを確認

$ docker volume ls
DRIVER    VOLUME NAME
local     semi1_test_volume

3. ボリュームオプションの付与と実行

以下のコマンドをホスト OS 上で実行し、--mount type=volumeオプションで先程作成したボリュームを、コンテナ上の/volume_host_dirディレクトリにボリュームマウントします。新しい Python を利用するというユースケースですので、Python のバージョンも 3.8 を利用します。分かりやすいように、コンテナの名前もbase_volume_v3.8にしてみます。

docker container run --mount type=volume,source=semi1_test_volume,target=/volume_host_dir --name base_volume_v3.8 -it -p 8080:80 python:3.8-slim bash

全体の意味: 作成したsemi1_test_volumeボリュームを、コンテナ上の/volume_host_dirディレクトリにボリュームマウント(type=volume)するものです。 また、それぞれのオプションの詳しい意味は、以下の通りです。

  • docker container run:新しいコンテナを作成して起動するコマンド
  • --mount type=volume,source=semi1_test_volume,target=/volume_host_dir:Dockerボリュームsemi1_test_volumeをコンテナ内の/volume_host_dirにマウントする。ボリュームを使ってデータを永続化・共有できる
  • --name base_volume_v3.8:コンテナに「base_volume_v3.8」という名前を付ける
  • -it:-i(標準入力を開いたままにする)と -t(擬似ターミナルを割り当てる)の組み合わせ。対話的なシェル操作が可能になる
  • -p 8080:80:ホストの8080番ポートをコンテナの80番ポートに割り当てる(ポートフォワーディング)
  • python:3.8-slim:使用するDockerイメージ名(Python 3.8の軽量版)
  • bash:コンテナ起動時に実行するコマンド(bashシェルを起動)

このコマンドは、バインドマウントによるデータの永続化 - バインドオプションの付与と実行typebindからvolumeに変更し、マウント位置を変更しただけになります。

コマンドの実行時は以下のようなログが流れるかと思います。

$ docker container run --rm --mount type=volume,source=semi1_test_volume,target=/volume_host_dir --name base_volume_v3.8 -it -p 8080:80 python:3.8-slim bash
Unable to find image 'python:3.8-slim' locally
3.8-slim: Pulling from library/python
b4d181a07f80: Pull complete
de8ecf497b75: Pull complete
6ea9cb124572: Pull complete
9a8aa9d08ec5: Pull complete
360b2e4ced96: Pull complete
Digest: sha256:9b0d7419e2811710aacee87c40a2c94693e2b6810c3e7e466b8c7fc5bde4cd66
Status: Downloaded newer image for python:3.8-slim
root@923782d5cd44:/#

また、lsコマンドでコンテナ上に/volume_host_dirディレクトリが存在し、そこにファイルが何もないことを確認してください。

root@851a057916dd:/# ls /volume_host_dir/

4. ファイルをコンテナへコピーし確認

ボリュームでは、ホスト側からファイルを変更することはできないので、docker container cpコマンドでホスト OS 上のファイルをコピーします。具体的には、以下のコマンドで、display_python_version.pybase_volumeコンテナへコピーします。このコマンドは、Flaskの環境構築とWebサーバの起動 - 6. ファイルをコンテナへコピーで利用したものと同じです。

docker container cp ./display_python_version.py base_volume_v3.8:/volume_host_dir

また、lsコマンドでコンテナ上の/volume_host_dirに、display_python_version.pyが存在することを確認してください。

root@923782d5cd44:/# ls /volume_host_dir/
display_python_version.py

5. Flaskの起動と確認

まず、以下の 2 つのコマンドをそれぞれ実行し、Flask のインストールと Web サーバの起動を行いましょう。これは、Flaskの環境構築とWebサーバの起動 - 1. Pythonコンテナの実行Flaskの環境構築とWebサーバの起動 - 8. Webサーバの起動と同様です。

  • Flask のインストール: pip install flask
  • Web サーバの起動: python -u /volume_host_dir/display_python_version.py

Web サーバが起動したら、Flaskの環境構築とWebサーバの起動 - 8. Webサーバの起動と同じ手順で、Python のバージョンが表示されているかを確認してみましょう。 以下のように、"Python version: 3.8.11 (default, Jun 29 2021, 20:05:22) [GCC 8.3.0]"というメッセージが出ているはずです。

ここまで、Web サーバで上記のメッセージが出ていることを確認したら、 exit でコンテナを終了してください。

--rmオプションを付け忘れた場合は、最後にこのコンテナを以下のコマンドで破棄します。 docker container ls -aコマンドで、base_volume_v3.8という名前のコンテナがないことを確認できます。

$ docker container rm base_volume_v3.8
base_volume_v3.8

6. 別のコンテナの起動

以下のコマンドで、python:3.7.5-slimイメージを利用し、Python のバージョンを3.7.5の新しいコンテナを起動します。 たとえば、検証やテストの一貫として、同じデータを使って古い Python のバージョンでの動作を確認しなければなくなったとしましょう。 Python のバージョンを3.7.5の新しいコンテナを起動し、分かりやすいように、nameオプションでコンテナ名をbase_volume_v3.7.5にしましょう。

docker container run --rm --mount type=volume,source=semi1_test_volume,target=/volume_host_dir --name base_volume_v3.7.5 -it -p 8080:80 python:3.7.5-slim bash

コンテナ起動後に、lsコマンドで/volume_host_dirdisplay_python_version.pyが存在することを確認してください。また、catコマンドで中身が Python のバージョンを確認するものになっていることも確認してみましょう。 このように、作成したボリュームの中身は、コンテナを破棄しても削除されません。これがボリュームによるデータの永続化です。

root@1f8f8b3223cb:/# ls /volume_host_dir/
display_python_version.py
root@1f8f8b3223cb:/# cat /volume_host_dir/display_python_version.py
from flask import Flask
import sys
app = Flask(__name__)

@app.route('/')

def hello_world():
    return ("Python version: " + sys.version)

app.run(debug=True, host='0.0.0.0', port=80)

7. Flaskの起動と確認

以下の 2 つのコマンドをそれぞれ実行し、Flask のインストールと Web サーバの起動を行いましょう。

  • Flask のインストール: pip install flask
  • Web サーバの起動: python -u /volume_host_dir/display_python_version.py

Web サーバが起動したら、Flaskの環境構築とWebサーバの起動 - 8. Webサーバの起動と同じ手順で、メッセージが正しく表示されているかを確認してみましょう。

以下のように、"Python version: 3.7.5 (default, Nov 23 2019, 06:10:46) [GCC 8.3.0]"にメッセージが変わっているはずです。

チェックポイント:できたら見せてください

このように、ボリュームを利用することで、データを別のコンテナと共有できます。
これでボリュームによるデータの永続化は終了です。


  1. コンテナの一時停止や停止だけでは、データは保たれます。