Dockerfileを書く時の注意とかコツとかハックとか

目次

なぜDockerfileを使うのか?

ADDとDockerfileにおいてのコンテキストを理解する

CMDでコンテナをバイナリのように扱う

CMDとENTRYPOINTの違い

ビルド時のキャッシュについて: キャッシュが有効なときと無効なとき

コンテナをバックグラウンドで動かすハック

なぜ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にコピーしたい場合を説明します。

1
2
$ ls
Dockerfile  mydir  myfile.txt

上記のようなディレクトリ構成の場合、Dockerfileは次のようになります。

1
ADD myfile.txt /

簡単ですね。しかし、/home/vagrant/myfile.txtを追加しようとすると失敗します。

1
2
3
4
5
6
7
8
9
# 以下の行が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 コマンドを実行した時に確認できます。

1
2
$ docker build -t blog .
Uploading context 10240 bytes

ここで build コマンドが何をしているかというと、Dockerクライアントがカレントディレクトリ配下のファイルとディレクトリをtarballにまとめてDockerデーモンへ送信しています。なぜわざわざ送信する必要があるかというと、DockerクライアントとDockerデーモンは違うホストで動いている可能性があるからです。これが上記のコマンドを実行した時に Uploading と表示されている理由です。

ひとつ落とし穴があります。Dockerは自動的にカレントディレクトリ配下のファイルとディレクトリをコンテキストに追加するので、もし巨大なファイルやディレクトリを間違っておいておくと使う必要もないのにそれらのtarballを作ろうとします。

1
2
3
4
5
6
$ ls
Dockerfile  very_huge_file

$ docker build -t blog .
Uploading context xxxxxx bytes
..... # すごく時間がかかる、、、

ベストプラクティスとしては、イメージに追加したいファイルとディレクトリのみをbuildを実行するディレクトリに置くべきです。

CMDでコンテナをバイナリのように扱う

CMDをDockerfileで使うことで、コンテナを一つのバイナリのように扱うことができます。以下のようなDockerfileがあるとします。

1
2
3
# 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の違い

CMDENTRYPOINTはとても紛らわしいです。

コマンドラインから引数として渡されたものでもCMDから指定されたものでも、すべてのコマンドはENTRYPOINTで指定されたバイナリの引数として渡されます。

/bin/sh -c はデフォルトのエントリーポイントです。もし、エントリーポイントなしでCMD dateと書いた場合、Dockerはこれを/bin/sh -c dateとして実行します。

エントリーポイントを使うことによってコンテナの挙動を実行時に変えることができるので、運用を柔軟にすることができます。

1
ENTRYPOINT ["/bin/date"]

上記のようなエントリーポイントがあった場合、このコンテナは現在時刻を違うフォーマットで出力することができます。

1
2
3
4
5
$ docker run -i clock_container +"%s"
1404214000

$ docker run -i clock_container +"%F"
2014-07-01

exec format error

デフォルトにエントリーポイントに関して、一つ注意することがあります。例えば以下のようなシェルスクリプトを実行したいとします。

/usr/local/bin/run.sh

1
echo "hello, world"

Dockerfile

1
2
3
ADD run.sh /usr/local/bin/run.sh
RUN chmod +x /usr/local/bin/run.sh
CMD ["/usr/local/bin/run.sh"]

このコンテナを起動すると、hello, worldと出力することをあなたは期待すると思いますが、実際は意味のわからないエラーになります。

1
2
$ docker run -i hello_world_image
2014/07/01 10:53:57 exec format error

これは、シェルスクリプトにシェバングを忘れたため、デフォルトのエントリーポイントである/bin/sh -cがどのようにしてスクリプトを実行したらいいわからないためエラーになりました。

これを修正するには、単にシェバングを足すか、

/usr/local/bin/run.sh

1
2
#!/bin/bash
echo "hello, world"

またはコマンドラインから指定することができます。

1
$ docker run -entrypoint="/bin/bash" -i hello_world_image

ビルド時のキャッシュについて: キャッシュが有効なときと無効なとき

DockerはDockerfileの各一行毎にコミットを作成していきます。行の記述を変更しない限り、Dockerは新しいイメージを作る必要がないと判断してキャッシュを使って次の行の元になるイメージを作成します。 これが初めて docker build を実行した時には時間がかかるのに、2回目からは一瞬でビルドが完了する理由です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ 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内のある一行でキャッシュが使われない書き方をしていまうと、それ以降の行でキャッシュは全く使われなくなってしまいます。

1
2
3
4
5
6
7
8
9
10
# 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はひとつ前の行で作られたイメージをベースにして行に書かれている命令を実行するので、これは当然の挙動だと言えます。

何もしないコマンドを追加してもキャッシュは無効になる

以下の例ではキャッシュは効きません。

1
2
3
4
5
# Before
Run apt-get update

# After
Run apt-get update && true

true コマンドは実際には何もしないコマンドですが、Dockerはキャッシュを使ってはくれません。

コマンドと引数の間に意味のないスペースの入れてもキャッシュは無効となる

以下の例ではキャッシュは効きません。

1
2
3
4
5
# Before
Run apt-get update

# After
Run apt-get               update

Dockerfileの行に意味のないスペースを入れてもキャッシュは有効

以下の例ではキャッシュは有効になります。

1
2
3
4
5
# Before
Run apt-get update

# After
Run                apt-get update

冪等ではない命令でもキャッシュは効いてしまう

これはどちらかというとキャッシュの落とし穴についてです。 冪等ではない命令 とは実行する度に結果が変わる可能性のあるコマンドを実行する行のことです。 例えば、 apt-get update は実行する度にアップデートされる内容が変わる可能性があるので冪等ではありません。

1
2
From ubuntu
Run apt-get update

上記のDockerfileを作ってイメージを作成したとします。3ヶ月後、Ubutnuがセキュリティアップデートをあるリポジトリにリリースしたので、同じDockerfileを使ってイメージの再作成をしたとします。(apt-get update がセキュリティアップデートを拾ってくれると思って) しかし、この方法でイメージを再作成してもセキュリティアップデートはインストールされません。Dockerfileの記述自体は全く変更されていないので、たとえ apt-get update の実行結果が変わっていたとしてもDockerはキャッシュを使うからです。

もし、これ避けたければ、-no-cache オプションを使えます。

1
$ docker build -no-cache .

ADD以降にある命令はキャッシュされない (ただし、0.7.3以前のバージョンを使っている場合のみ)

もし、0.7.3以前のバージョンを使っている場合、注意してください!

1
2
3
4
From ubuntu
Add myfile /
Run apt-get update
Run apt-get install openssh-server

もしこのようなDockerfileだと、Run apt-get updateRun apt-get install openssh-server は絶対にキャッシュされません。

この挙動は0.7.3で改善されました。ADD以降の行でも、ADDの書き方自身やADDする対象が変更されない限りキャッシュが使われます。

1
2
3
4
5
6
7
8
9
10
11
$ 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をサービスとして起動するコンテナを例に説明しましょう。直感的に次のようにやりたくなるでしょう。

1
$ docker run -d apache-server apachectl start

しかし、これだとコンテナは起動した瞬間に終了します。これは、 apachectl がapacheをデーモン化した瞬間に自身は終了してしまうからです。

Dockerはこのようなコマンドが嫌いです。Dockerはコマンドがフォアグラウンドで起動し続けることを期待しているからです。 もしそうでなければ、Dockerはアプリケーションは終了したと考えてコンテナを終了してしまいます。

この問題はapacheの実行バイナリを直接フォアグラウンドで動かすことで解決できます。

1
2
3
4
5
6
7
$ 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の例で使ってみましょう。

1
$ docker run -d apache-server apachectl start && tail -f /dev/null

ずっと良くなりました。 tail -f /dev/null は無害なコマンドなのでこのテクニックはどんな場合にも使うことができるのでおすすめです。

Comments

« Gotchas in Writing Dockerfile Dashingのカスタムウィジェットを作成する »