はじめに
前提
Dockerの基本的な操作等については触れません。
バインドマウントを利用して、アプリのソースコードをコンテナ内にマウントして開発している状況を想定しています。
バージョン情報
Docker Engine: 19.03.13
大まかな流れ
- まず結論
- バインドマウントとは
- 順を追ってビルド方法を確認していく
- おまけ
- 感想
当記事のゴール
バインドマウントの特徴を理解し、アプリのソースごとビルドして、イメージ化出来るようになる。
まず結論
開発時にソースコードをコンテナにバインドマウントするのはよくある手法だが、デプロイなどの際にソースコードを含めてイメージ化するには一工夫する必要がある。
その方法は拍子抜けするほどシンプルだが、別途ビルド用のDockerfileを用意し、明示的にソースをCOPY
すればいい。
(自分で調べていて、「ほんとにこんなシンプルな話なの・・・?」と思わず疑ってしまった。)
バインドマウントとは
Dockerで使えるボリュームのマウントタイプは主に3つある。
それぞれの違いを完結に説明すると
- ボリューム
- Dockerによって管理される領域に対してマウントする
- 複数のコンテナが共有可能
- バインドマウント
- ホスト上のパスに対してマウントさせる
- ホストとコンテナ、相互に読み書きが出来てしまう
- tmpfsマウント
- ホストのメモリ上に保存される
- コンテナが起動している間のみ、コンテナが利用するもの
- 一時的な状態や機密情報などを保存する
また、ボリュームとバインドマウントには以下のような特徴もある
公式のストレージ概要から引用
バインドマウントとボリュームを使う際のヒント
バインドマウントとボリュームのどちらかを用いる場合には、以下のことを忘れないでください。
コンテナー内のディレクトリに 空のボリューム をマウントしようとしていて、そのディレクトリ内にファイルやディレクトリが存在する場合、そのファイルやディレクトリはボリューム内にコピーされます。 コンテナー起動時に指定したボリュームがまだ存在していなかった場合は、空のボリュームが生成されます。 コンテナーの求めに応じて事前にデータを提供しておく方法として用いられます。
コンテナー内のディレクトリに バインドマウントか、空ではないボリューム をマウントしようとしていて、そのディレクトリ内にファイルやディレクトリが存在する場合、マウントによってそのファイルやディレクトリは隠れてしまいます。 それはたとえば、Linux マシン上の /mnt にファイルを保存した後に、/mnt に対して USB ドライブをマウントしたような場合と同じです。 /mnt に存在していた内容は USB ドライブの内容によって隠されてしまい、USB ドライブがアンマウントされるまで続きます。 隠されてしまったファイルは、削除されるわけでなく変更もされません。 しかしバインドマウントやボリュームがアンマウントされない限り、アクセスすることはできません。
順を追ってビルド方法を確認していく
シチュエーション
シンプルにNginxコンテナを構築し、ソースコードをコンテナにマウントさせ開発を行っていると想定する。
下記にサンプルのディレクトリ構成とDockerfileを示す。
project-directory/
└html/
└index.html
└Dockerfile
FROM nginx
COPY ./html /usr/share/nginx/html
EXPOSE 80
<!DOCTYPE html>
<head>
<title>ContainerA</title>
</head>
<body>
<h1>ContainerA!</h1>
</body>
下記のコマンドでソースコードがバインドマウントされたNginxコンテナを作る
docker run --name containerA --mount type=bind,source=(pwd)/html,target=/usr/share/nginx/html -d -p 81:80 nginx
コマンド解説
DockerHub公式リポジトリのnginxイメージをベースに、以下のオプションでコンテナを作成する
-
--name
でコンテナ名を指定 -
--mount
でボリュームをマウント- ボリュームには旧来のオプションとして
-v or --volume
もあるが、公式で--mount
を推奨しているので従う -
type
はマウントタイプを指定(今回はバインドマウントなのでbind
) -
source
はマウント元のパスを指定(ホスト) -
target
はマウント先のパスを指定(コンテナ内)
- ボリュームには旧来のオプションとして
-
-d
でデタッチモード(バックグラウンドで起動) -
-p
でポートフォワードを設定- ホストの
localhost:81
に対して、コンテナ内のloclahost:80
を紐付ける
- ホストの
http://localhost:81 にアクセスして、画面を表示してみる
文言を追加して、コンテナ内に反映されることを確認する
<!DOCTYPE html>
~省略~
<body>
<h1>ContainerA!</h1>
<p>Add message at Host.</p>
</body>
再度読み込むと、追加した文言が表示されている。
これで、ホストとコンテナが確実にバインドマウントされていることも確認できた。
Dockerfileをビルドし、ソースごとイメージ化する
docker build -t build_with_bind_data ./
※Dockerfile
が存在するディレクトリで実行する
↓は実行結果
ビルドされたイメージを確認する
docker image ls
下記のようにbuild_with_bind_data
というイメージがビルドされている
ビルドしたイメージを元にコンテナを作成し、ソースも含められているか確認する
docker run --name containerB -d -p 82:80 build_with_bind_data
http://localhost:82 にアクセスすると
ContaierAで表示させていたものと全く一緒なのが確認できた。
一応、containerB内部のソースも確認しておくと
docker exec containerB bash -c "cat /usr/share/nginx/html/index.html"
ビルド時に、COPY
コマンドによって、指定のディレクトリ/ファイルがコピーされていることが分かる。
おまけ
COMMIT
コマンドでコンテナはイメージ化出来るが、ボリュームマウントのデータは含まれない
COMMIT
コマンドでイメージを固めたくなるが、これはあくまでコンテナ内部での設定や変更をイメージ化するものであり、コンテナにボリュームマウント1されたデータは、コミットで作成されたイメージには含まれない、という点に注意する。
なので、ビルド時にCOPY
コマンドでアプリのソースコードを配置する必要がある。
感想
多分内容としては初歩的なものです。
今までのDockerを使ってきた経験として、ビルドしてイメージを管理する機会が無かったので、ソースを含めたイメージ化の具体的な手法がわからず、苦戦しました。
COMMIT
コマンドの特徴に気づかず、「なんでコミットしてもソースが含まれないんだ!?」なんて風に驚いていました。
が、公式の解説等をじっくり読んで自分なりに理解を深められたので、忘れないように記事に残しました。
-
マウントタイプが、ボリューム/バインドマウント/tmpfsマウントのいずれか。 ↩