Dockerfileを書く時の注意とかコツとかハックとか
目次
なぜDockerfileを使うのか?
ADDとDockerfileにおいてのコンテキストを理解する
CMDでコンテナをバイナリのように扱う
CMDとENTRYPOINTの違い
ビルド時のキャッシュについて: キャッシュが有効なときと無効なとき
- ある一行でキャッシュが使われなかったらそれ以降のすべての行でキャッシュは使われない
- 何もしないコマンドを追加してもキャッシュは無効になる
- コマンドと引数の間に意味のないスペースの入れてもキャッシュは無効となる
- Dockerfileの行に意味のないスペースを入れてもキャッシュは有効
- 冪等ではない命令でもキャッシュは効いてしまう
- ADD以降にある命令はキャッシュされない (ただし、0.7.3以前のバージョンを使っている場合のみ)
コンテナをバックグラウンドで動かすハック
なぜDockerfileを使うのか?
DockerfileはYet Anotherシェルではありません。Dockerfileは特別なミッションを持っています。それは、Dockerイメージ作成の自動化です。
一度Dockerfileにイメージ作成の手順を記述すれば、それ以降はdocker build
コマンド一つで同じイメージを作ることができます。
Dockerfileはコンテナが何をしているかを別の人の伝える手段でもあります。Dockerfileにはイメージを作って動かすまでのすべてが書かれているので、Dockerfileを読むだけでそのコンテナのやるべき仕事がすぐにわかります。こうすれば、コンテナが何をしているかを調べるためにわざわざログインしてpsコマンドを駆使しなくてもいいというわけです。
簡単に述べましたが、これらの理由で、Dockerイメージを作るなら必ずDockerfileを使ってください。しかし、Dockerfileを書くのは時々嫌になってしまうことがあるのも事実です。このポストではDockerfileを書く時に注意することわかりにくいことを解説します。この記事を読んでDockerfileに慣れてもらえればと思います。
ADDとDockerfileにおいてのコンテキストを理解する
ADD is the instruction to add local files to Docker image. The basic usage is very simple. Suppose you want to add a local file called myfile.txt to /myfile.txt of image. ADDはローカルファイルシステムのファイルやディレクトリをDockerイメージにコピーするために使います。使い方は至って簡単。もし、ローカルにあるmyfile.txtをイメージの/myfile.txtにコピーしたい場合を説明します。
$ ls
Dockerfile mydir myfile.txt
上記のようなディレクトリ構成の場合、Dockerfileは次のようになります。
ADD myfile.txt /
簡単ですね。しかし、/home/vagrant/myfile.txtを追加しようとすると失敗します。
# 以下の行がDockerfileにあるとします
# ADD /home/vagrant/myfile.txt /
$ docker build -t blog .
Uploading context 10240 bytes
Step 1 : FROM ubuntu
---> 8dbd9e392a96
Step 2 : ADD /home/vagrant/myfile.txt /
Error build: /home/vagrant/myfile.txt: no such file or directory
no such file or directory
と言われてしまいました。確かにファイルは存在するのになぜでしょうか?理由は /home/vagrant/myfile.txt がDockerfileのコンテキスト外だからです。DockerfileでのコンテキストとはDockerfile内の命令からアクセス可能なファイルやディレクトリの範囲のことです。そして、コンテキスト内のファイルとディレクトリしかイメージに追加することはできません。
カレントディレクトリ配下のファイルとディレクトリは自動的にコンテキストに追加されます。これは build
コマンドを実行した時に確認できます。
$ docker build -t blog .
Uploading context 10240 bytes
ここで build
コマンドが何をしているかというと、Dockerクライアントがカレントディレクトリ配下のファイルとディレクトリをtarballにまとめてDockerデーモンへ送信しています。なぜわざわざ送信する必要があるかというと、DockerクライアントとDockerデーモンは違うホストで動いている可能性があるからです。これが上記のコマンドを実行した時に Uploading と表示されている理由です。
ひとつ落とし穴があります。Dockerは自動的にカレントディレクトリ配下のファイルとディレクトリをコンテキストに追加するので、もし巨大なファイルやディレクトリを間違っておいておくと使う必要もないのにそれらのtarballを作ろうとします。
$ ls
Dockerfile very_huge_file
$ docker build -t blog .
Uploading context xxxxxx bytes
..... # すごく時間がかかる、、、
ベストプラクティスとしては、イメージに追加したいファイルとディレクトリのみをbuildを実行するディレクトリに置くべきです。
CMDでコンテナをバイナリのように扱う
CMDをDockerfileで使うことで、コンテナを一つのバイナリのように扱うことができます。以下のようなDockerfileがあるとします。
# run.shがDockerfileと同じディレクトリにあるとします
ADD run.sh /usr/local/bin/run.sh
CMD ["/usr/local/bin/run.sh"]
このDockerfileからコンテナを作って、docker run -i run_image
で起動すると/usr/local/bin/run.sh
スクリプトを実行してコンテナは終了します。
もし、CMD
を使わなかった場合、毎回起動する度に、docker run -i run_image /usr/local/bin/run.sh
とコマンドラインで指定しないといけません。
これは、面倒なだけではなく、コンテナの運用の観点からもバッドプラクティスです。
もし、CMD
がDockerfileにあればそのコンテナが何をするのか明確になります。
しかし、もしなかった場合コンテナを作った人以外の人がこのコンテナを正しく起動するためには外部のドキュメントに頼らなければいけません。
なので一般的には常にCMD
はDockerfileに指定すべきです。
CMDとENTRYPOINTの違い
コマンドラインから引数として渡されたものでも```CMD```から指定されたものでも、すべてのコマンドは```ENTRYPOINT```で指定されたバイナリの引数として渡されます。
```/bin/sh -c``` はデフォルトのエントリーポイントです。もし、エントリーポイントなしで```CMD date```と書いた場合、Dockerはこれを```/bin/sh -c date```として実行します。
エントリーポイントを使うことによってコンテナの挙動を実行時に変えることができるので、運用を柔軟にすることができます。
ENTRYPOINT [“/bin/date”]
上記のようなエントリーポイントがあった場合、このコンテナは現在時刻を違うフォーマットで出力することができます。
```bash
$ docker run -i clock_container +"%s"
1404214000
$ docker run -i clock_container +"%F"
2014-07-01
exec format error
デフォルトにエントリーポイントに関して、一つ注意することがあります。例えば以下のようなシェルスクリプトを実行したいとします。
/usr/local/bin/run.sh
echo "hello, world"
Dockerfile
ADD run.sh /usr/local/bin/run.sh
RUN chmod +x /usr/local/bin/run.sh
CMD ["/usr/local/bin/run.sh"]
このコンテナを起動すると、hello, world
と出力することをあなたは期待すると思いますが、実際は意味のわからないエラーになります。
$ docker run -i hello_world_image
2014/07/01 10:53:57 exec format error
これは、シェルスクリプトにシェバングを忘れたため、デフォルトのエントリーポイントである/bin/sh -c
がどのようにしてスクリプトを実行したらいいわからないためエラーになりました。
これを修正するには、単にシェバングを足すか、
/usr/local/bin/run.sh
#!/bin/bash
echo "hello, world"
またはコマンドラインから指定することができます。
$ docker run -entrypoint="/bin/bash" -i hello_world_image
ビルド時のキャッシュについて: キャッシュが有効なときと無効なとき
DockerはDockerfileの各一行毎にコミットを作成していきます。行の記述を変更しない限り、Dockerは新しいイメージを作る必要がないと判断してキャッシュを使って次の行の元になるイメージを作成します。
これが初めて docker build
を実行した時には時間がかかるのに、2回目からは一瞬でビルドが完了する理由です。
$ time docker build -t blog .
Uploading context 10.24 kB
Step 1 : FROM ubuntu
---> 8dbd9e392a96
Step 2 : RUN apt-get update
---> Running in 15705b182387
Ign http://archive.ubuntu.com precise InRelease
Hit http://archive.ubuntu.com precise Release.gpg
Hit http://archive.ubuntu.com precise Release
Hit http://archive.ubuntu.com precise/main amd64 Packages
Get:1 http://archive.ubuntu.com precise/main i386 Packages [1641 kB]
Get:2 http://archive.ubuntu.com precise/main TranslationIndex [3706 B]
Get:3 http://archive.ubuntu.com precise/main Translation-en [893 kB]
Fetched 2537 kB in 7s (351 kB/s)
---> a8e9f7328cc4
Successfully built a8e9f7328cc4
real 0m8.589s
user 0m0.008s
sys 0m0.012s
$ time docker build -t blog .
Uploading context 10.24 kB
Step 1 : FROM ubuntu
---> 8dbd9e392a96
Step 2 : RUN apt-get update
---> Using cache
---> a8e9f7328cc4
Successfully built a8e9f7328cc4
real 0m0.067s
user 0m0.012s
sys 0m0.000s
しかし、いつキャッシュが使われていつキャッシュが使われないのかはあまり明確ではありません。ここでは、いくつかのケースを紹介します。
ある一行でキャッシュが使われなかったらそれ以降のすべての行でキャッシュは使われない
これは一番基本のルールです。もし、Dockerfile内のある一行でキャッシュが使われない書き方をしていまうと、それ以降の行でキャッシュは全く使われなくなってしまいます。
# Before
From ubuntu
Run apt-get install ruby
Run echo done!
# After
From ubuntu
Run apt-get update
Run apt-get install ruby
Run echo done!
Run apt-get update という行を追加したことでベースのイメージを変更してしまったので、 それ以降の すべての 行で使うイメージも初めから作り直されないといけません。 Dockerfileはひとつ前の行で作られたイメージをベースにして行に書かれている命令を実行するので、これは当然の挙動だと言えます。
何もしないコマンドを追加してもキャッシュは無効になる
以下の例ではキャッシュは効きません。
# Before
Run apt-get update
# After
Run apt-get update && true
true
コマンドは実際には何もしないコマンドですが、Dockerはキャッシュを使ってはくれません。
コマンドと引数の間に意味のないスペースの入れてもキャッシュは無効となる
以下の例ではキャッシュは効きません。
# Before
Run apt-get update
# After
Run apt-get update
Dockerfileの行に意味のないスペースを入れてもキャッシュは有効
以下の例ではキャッシュは有効になります。
# Before
Run apt-get update
# After
Run apt-get update
冪等ではない命令でもキャッシュは効いてしまう
これはどちらかというとキャッシュの落とし穴についてです。 冪等ではない命令 とは実行する度に結果が変わる可能性のあるコマンドを実行する行のことです。
例えば、 apt-get update
は実行する度にアップデートされる内容が変わる可能性があるので冪等ではありません。
From ubuntu
Run apt-get update
上記のDockerfileを作ってイメージを作成したとします。3ヶ月後、Ubutnuがセキュリティアップデートをあるリポジトリにリリースしたので、同じDockerfileを使ってイメージの再作成をしたとします。(apt-get update がセキュリティアップデートを拾ってくれると思って)
しかし、この方法でイメージを再作成してもセキュリティアップデートはインストールされません。Dockerfileの記述自体は全く変更されていないので、たとえ apt-get update
の実行結果が変わっていたとしてもDockerはキャッシュを使うからです。
もし、これ避けたければ、-no-cache
オプションを使えます。
$ docker build -no-cache .
ADD以降にある命令はキャッシュされない (ただし、0.7.3以前のバージョンを使っている場合のみ)
もし、0.7.3以前のバージョンを使っている場合、注意してください!
From ubuntu
Add myfile /
Run apt-get update
Run apt-get install openssh-server
もしこのようなDockerfileだと、Run apt-get update と Run apt-get install openssh-server は絶対にキャッシュされません。
この挙動は0.7.3で改善されました。ADD以降の行でも、ADDの書き方自身やADDする対象が変更されない限りキャッシュが使われます。
$ echo "Jeff Beck" > rock.you
From ubuntu
Add rock.you /
Run add rock.you
$ echo "Eric Clapton" > rock.you
From ubuntu
Add rock.you /
Run add rock.you
ここでは、rock.you ファイルの内容を変更したので、ADD以降の行ではキャッシュは使われません。
コンテナをバックグラウンドで動かすハック
もし、コンテナの起動方法をシンプルにしたければ、docker run -d image your-command
を使ってコンテナをバックグラウンドで起動するべきです。
docker run -i -t image your-command
の代わりに -d
を使うことを勧める理由はコンテナの起動をたった一つのコマンドで行うことができ、かつ Ctrl + P + Q
を入力してコンテナをターミナルから切り離す作業をしなくていいからです。
しかし、-d
オプションには問題があります。コマンドがフォアグラウンドで実行されていないとコンテナはすぐに終了してしまいます。
apacheをサービスとして起動するコンテナを例に説明しましょう。直感的に次のようにやりたくなるでしょう。
$ docker run -d apache-server apachectl start
しかし、これだとコンテナは起動した瞬間に終了します。これは、 apachectl
がapacheをデーモン化した瞬間に自身は終了してしまうからです。
Dockerはこのようなコマンドが嫌いです。Dockerはコマンドがフォアグラウンドで起動し続けることを期待しているからです。 もしそうでなければ、Dockerはアプリケーションは終了したと考えてコンテナを終了してしまいます。
この問題はapacheの実行バイナリを直接フォアグラウンドで動かすことで解決できます。
$ docker run -e APACHE_RUN_USER=www-data \
-e APACHE_RUN_GROUP=www-data \
-e APACHE_PID_FILE=/var/run/apache2.pid \
-e APACHE_RUN_DIR=/var/run/apache2 \
-e APACHE_LOCK_DIR=/var/lock/apache2 \
-e APACHE_LOG_DIR=/var/log/apache2 \
-d apache-server /usr/sbin/apache2 -D NO_DETACH -D FOREGROUND
ここでしていることは、apachectl
がやっていることを手動で行ってapacheを起動しています。このやり方だとapacheはフォアグラウンドで動き続けることができます。
問題はアプリケーションによってはフォアグラウンドで起動する方法がない場合があることです。また、apachectl
の例のようにヘルパープログラムがやってくれることを分解して手動でやらないといけないのは大変です。どうすればいいでしょう?
このような場合、tail -f /dev/null
を実行したいコマンドに追加すればコンテナはメインのコマンドがバックグラウンドで実行されても tail
がフォアグラウンドで起動し続けてくれるので終了しません。このテクニックをさっきのapacheの例で使ってみましょう。
$ docker run -d apache-server apachectl start && tail -f /dev/null
ずっと良くなりました。 tail -f /dev/null
は無害なコマンドなのでこのテクニックはどんな場合にも使うことができるのでおすすめです。