Dockerfile を書くベスト・プラクティス¶
Docker は Dockerfile
の命令を読み込み、自動的にイメージを構築できます。これはテキストファイルであり、特定のイメージを構築するために必要な全ての命令が入っています。 Dockerfile
は個別の命令セットを使い、特定の形式で記述します。基本を Dockerfile リファレンス で学べます。新しく Dockerfile
を書こうとしているのであれば、ここから始めましょう。
このドキュメントは Docker 社が推奨するベストプラクティスや手法を扱っており、Docker コミュニティが簡単かつ効率的に Dockerfile
を作成できるようにします。この推奨方法に従うことを強く勧めます(実際、公式イメージを作成するには、これらのプラクティスに従う 必要 があります)。
多くのベストプラクティスや推奨する手法は、 buildpack-deps の Dockerfile
でご覧いただけます。
注釈
Dockerfile コマンドに関するより詳細な説明は、 Dockerfile リファレンス ページをご覧ください。
一般的なガイドラインと推奨¶
コンテナはエフェメラルであるべき¶
Dockerfile
で定義されたイメージを使って作成されるコンテナは、可能ならばエフェメラル(短命;ephemeral)にすべきです。私たちの「エフェメラル」とは、停止・破棄可能であり、明らかに最小のセットアップで構築して使えることを意味します。
.dockerignore ファイルを使う¶
ほとんどの場合、空のディレクトリに個々の Dockerfile を置くのがベストです。そうしておけば、そのディレクトリには Dockerfile が構築に必要なファイルだけ追加します。構築のパフォーマンスを高めるには、 .dockerignore
ファイルを作成し、対象ディレクトリ上のファイルやディレクトリを除外できます。このファイルの除外パターンは .gitignore
ファイルに似ています。ファイルを作成するには、 .dockerignore ファイル をご覧ください。
不要なパッケージのインストールを避ける¶
複雑さ、依存関係、ファイルサイズ、構築階数をそれぞれ減らすために、余分ないし不必要な「入れた方が良いだろう」というパッケージは、インストールを避けるべきです。たとえば、データベース・イメージであればテキストエディタは不要でしょう。
コンテナごとに1つのプロセスだけ実行¶
ほとんどの場合、1つのコンテナの中で1つのプロセスだけ実行すべきです。アプリケーションを複数のコンテナに分離することは、水平スケールやコンテナの再利用を簡単にします。サービスとサービスに依存関係がある場合は、 コンテナのリンク を使います。
複数行の引数¶
可能であれば常に複数行の引数をアルファベット順にします。これにより、パッケージが重複しないようにし、あるいはリストの更新を簡単します。また、プルリクエストの読み込みとレビューをより簡単にします。バックスラッシュ( \ ) の前に空白を追加するのも、同じく役立つでしょう。
以下は buildpack-deps
イメージ の記述例です。
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion
構築キャッシュ¶
Docker イメージ構築のプロセスとは、 Dockerfile
で指定した各命令を順番に実行していきます。それぞれの命令ごとに、新しい(あるいは重複した)イメージを作成するのではなく、Docker によって既存イメージのキャッシュがないか検査されます。もし全てのキャッシュを使いたくなければ、 docker build
コマンドで --no-cache=true
オプションを使います。
しかしながら、Docker がキャッシュを使うにあたり、マシンイメージを探すときに、何を行い何を行わないかを理解するのは非常に重要です。Docker の基本的な処理の概要は、次の通りです。
- 開始にあたり、ベース・イメージが既にキャッシュにあれば、次の命令を対象のベース・イメージから派生した全ての子イメージと比較します。同じ命令があれば構築にそのイメージを使います。もし同じ命令がなければ、キャッシュは無効化されます。
- ほとんどの場合は、
Dockerfile
命令と子イメージの単純な比較で十分です。しかし、命令によっては更なる検査や追加検査が必要になります。
ADD
とCOPY
命令は、イメージに含まれるファイルが検査され、各ファイルごとのチェックサムを計算します。ファイルの最終編集・最終アクセス時間は、チェックサムに影響ありません。キャッシュを探し、既存のイメージのチェックサムと比較します。内容やメタデータのようにファイルに変更があれば、キャッシュは無効化されます。
ADD
とCOPY
コマンドだけでなく、キャッシュのチェックにおいて、キャッシュが一致すると思われるコンテナ内のファイル状態を確認しません。たとえば、RUN apt-get -y update
コマンドによってコンテナ内のファイルに変更を加えたとしても、キャッシュの有無に影響を与えません。この場合、コマンドの文字列自身が一致するかどうかしか見ないためです。
- キャッシュが無効化されると、以降の
Dockerfile
命令ではキャッシュは使われず、新しいイメージを生成します。
Dockerfile 命令¶
以下では、Dockerfile
で様々な命令を使うにあたり、推奨するベストな方法が分かるでしょう。
FROM¶
可能であれば、自分のイメージの元として現在の公式リポジトリを使います。私たちは Debian イメージ を推奨します。これは、非常にしっかりと管理されており、ディストリビューションの中でも最小(現在は 100 MB 以下)になるよう維持されているからです。
RUN¶
いつものように、 Dockerfile
をより読みやすく、理解しやすく、メンテナンスしやすくします。長ければ分割するか、複雑な RUN
命令はバックスラッシュを使い複数行に分割します。
おそらく RUN
の最も一般的な利用例は apt-get
アプリケーションです。 RUN apt-get
コマンドは、パッケージをインストールするので、気をつけるべきいくつかの了解事項があります。
RUN apt-get update
や dist-upgrade
を避けるべきでしょう。ベース・イメージに含まれる「必須」パッケージの多くが、権限を持たないコンテナの内部で更新されないためです。もし、ベース・イメージに含まれるパッケージが時代遅れになっていれば、イメージのメンテナに連絡すべきでしょう。たとえば、 foo
という特定のパッケージを知っていて、それを更新する必要があるのであれば、自動的に更新するために apt-get install -y foo
を使います。
RUN apt-get update
と apt-get install
は常に同じ RUN
命令文で連結します。以下は実行例です。
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo
RUN
命令文で apt-get update
だけを使うとキャッシュ問題を引き起こし、その後の apt-get install
命令が失敗します。例えば、次のように Dockerfile を記述したとします。
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl
イメージを構築すると、全てのレイヤは Docker にキャッシュされます。次に、別のパッケージを追加する apt-get install
を編集したとします。
FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y curl nginx
Docker は冒頭からファイルを読み込み、命令の変更を認識すると、前のステップで作成したキャッシュを再利用します。しかし、 apt-get update
は 決して 実行されず、キャッシュされたバージョンを使います。これは apt-get update
が実行されていないためです。そのため、古いバージョンの curl
と nginx
パッケージを取得する恐れがあります。
そこで Dockerfile でのインストールには RUN apt-get update && apt-get install -y
を使うことで、最新バージョンのパッケージを、追加の記述や手動作業なく利用できます。
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo=1.3.*
バージョンを指定すると、何がキャッシュされているか気にせずに、特定バージョンを取得した上での構築を強制します。このテクニックは、必要なパッケージの予期しない変更によって引き起こされる失敗を減らします。
以下は よく練られた RUN
命令であり、推奨する apt-get
の使い方の全てを示すものです。
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
lxc=1.0* \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
s3cmd
命令行は、バージョン 1.1.0*
を指定します。従来のイメージが古いバージョンを使っていたとしても、新しいイメージは apt-get update
でキャッシュを使わないので、確実に新しいバージョンをインストールします。各行はパッケージのリストであり、パッケージの重複という間違いをさせないためです。
付け加えると、apt キャッシュをクリーンにし、 /var/lib/apt/lits
を削除することで、イメージのサイズを減らします。 RUN
命令は apt-get update
から開始されるので、 apt-get install
でインストールされるパッケージは、常に新鮮なものです。
CMD¶
CMD
命令は、イメージに含まれるソフトウェアの実行と、その引数のために使うべきです。また、CMD
は常に CMD [“executable”, “param1”, “param2”…]
のような形式で使うべきです。そのため、イメージがサービス向け(Apache、Rails 等)であれば、 CMD ["apache2","-DFOREGROUND"]
のようにすべきでしょう。実際に、あらゆるサービスのベースとなるイメージで、この命令形式が推奨されます。
大部分の他のケースでは、 CMD
はインタラクティブなシェル(bash、python、perl 等)で使われます。たとえば、 CMD ["perl", "-de0"]
、 CMD ["python"]
、 CMD [“php”, “-a”]
です。この利用形式が意味するのは、 docker run -it python
のように実行すると、それを使いやすいシェル上に落とし込み、すぐに使えるようにします。 あなたとあなたの想定ユーザが ENTRYPOINT
の動作になれていない限り、CMD
を ENTRYPOINT
と一緒に CMD [“パラメータ”, “パラメータ”]
の形式で使うべきではないでしょう。
EXPOSE¶
EXPOSE
命令は、コンテナが接続用にリッスンするポートを指定します。そのため、アプリケーションが一般的に使う、あるいは、伝統的なポートを使うべきです。例えば、Apache ウェブ・サーバのイメージは、 EXPOSE 80
を使い、MongoDB を含むイメージは EXPOSE 27017
を使うでしょう。
外部からアクセスするためには、ユーザが docker run
実行時にフラグを指定し、特定のポートを任意のポートに割り当てられます。コンテナのリンク機能を使うと、Docker はコンテナがソースをたどれるよう、環境変数を提供します(例: MYSQL_PORT_3306_TCP
)。
ENV¶
新しいソフトウェアを簡単に実行するため、コンテナにインストールされているソフトウェアの PATH
環境変数を ENV
を使って更新できます。例えば、 ENV PATH /usr/local/nginx/bin:$PATH
は CMD ["nginx"]
を動くようにします。
ENV
命令はまた、PostgreSQL の PGDATA
のような、コンテナ化されたサービスが必要な環境変数を指定するのにも便利です。
あとは、 ENV
は一般的に使うバージョン番号の指定にも使えますので、バージョンを特定したメンテナンスを次のように簡単にします。
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH
プログラムにおける恒常的な変数に似ていますが(ハード・コーディングされた値とは違い)、この手法は ENV
命令を使うことにより、コンテナ内のソフトウェアのバージョンを自動的に選べるようにします。
ADD と COPY¶
Dockerfile リファレンスの ADD 命令 Dockerfile リファレンスの COPY 命令
ADD
と COPY
は機能が似ていますが、 COPY
が望ましいと一般的に言われています。これは、 ADD
よりも明白なためです。 COPY
はローカルファイルをコンテナの中にコピーするという、基本的な機能しかサポートしていません。一方の ADD
は複数の機能(ローカル上での tar 展開や、リモート URL のサポート)を持ち、一見では分かりません。したがって ADD
のベストな使い方は、ローカルの tar ファイルをイメージに自動展開( ADD rootfs.tar.xz /
)することです。
内容によっては、一度にファイルを取り込むよりも、 Dockerfile
の複数ステップで COPY
することもあるでしょう。これにより、何らかのファイルが変更された所だけ、キャッシュが無効化されます(ステップを強制的に再実行します)。
実行例:
COPY requirements.txt /tmp/
RUN pip install /tmp/requirements.txt
COPY . /tmp/
RUN
ステップはキャッシュ無効化の影響が少なくなるよう、 COPY . /tmp/
の前に入れるべきでしょう。
イメージ・サイズの問題のため、 ADD
でリモート URL 上のパッケージを取得するのは可能な限り避けてください。その代わりに curl
や wget
を使うべきです。この方法であれば、展開後に不要となったファイルを削除でき、イメージに余分なレイヤを増やしません。例えば、次のような記述は避けるべきです。
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things
RUN make -C /usr/src/things all
そのかわり、次のように記述します。
RUN mkdir -p /usr/src/things \
&& curl -SL http://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all
他のアイテム(ファイルやディレクトリ)は ADD
の自動展開機能を必要としませんので、常に COPY
を使うべきです。
ENTRYPOINT¶
Dockerfile リファレンスの ENTRYPOINT 命令
ENTRYPOINT
のベストな使い方は、イメージにおけるメインコマンドの設定です。これによりイメージを指定したコマンドを通して実行します(そして、 CMD
がデフォルトのフラグとして使われます)。
コマンドライン・ツールの s3cmd
のイメージを例にしてみましょう。
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
このイメージを使って次のように実行すると、コマンドのヘルプを表示します。
$ docker run s3cmd
あるいは、適切なパラメータを指定すると、コマンドを実行します。
$ docker run s3cmd ls s3://mybucket
イメージ名が先ほどの命令で指定したコマンドのバイナリも兼ねているため、使いやすくなります。
また、ENTRYPOINT
命令は役に立つスクリプトの組みあわせにも利用できます。そのため、ツールを使うために複数のステップが必要になるかもしれない場合も、先ほどのコマンドと似たような方法が使えます。
例えば、 Postgres 公式イメージは次のスクリプトを ENTRYPOINT
に使います。
#!/bin/bash
set -e
if [ "$1" = 'postgres' ]; then
chown -R postgres "$PGDATA"
if [ -z "$(ls -A "$PGDATA")" ]; then
gosu postgres initdb
fi
exec gosu postgres "$@"
fi
exec "$@"
注釈
このスクリプトは exec
Bash コマンド をコンテナの PID 1 アプリケーションとして実行します。これにより、コンテナに対して送信される Unix シグナルは、アプリケーションが受信します。詳細は ENTRYPOINT
のヘルプをご覧ください。
ヘルパー・スクリプトをコンテナの中にコピーし、コンテナ開始時 ENTRYPOINT
で実行します。
COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
このスクリプトは Postgres とユーザとの対話に利用できます。
単純な postgres の起動に使います。
$ docker run postgres
あるいは、PostgreSQL 実行時、サーバに対してパラメータを渡せます。
$ docker run postgres postgres --help
または、Bash のように全く異なったツールのためにも利用可能です。
$ docker run --rm -it postgres bash
VOLUME¶
VOLUME
命令はデータベース・ストレージ領域、設定用ストレージ、Docker コンテナによって作成されるファイルやフォルダの公開のために使います。イメージにおいて変わりやすい場所・ユーザによって便利な場所として VOLUME の利用が強く推奨されます。
USER¶
サービスは特権ユーザで実行せずに、 USER
を使えば非 root ユーザで実行できます。利用するには Dockerfile
でユーザとグループを RUN groupadd -r postgres && useradd -r -g postgres postgres
のように作成します。
注釈
イメージ内で得られるユーザとグループは UID/GID に依存しないため、イメージの構築に関係なく次の UID/GID が割り当てられます。そのため、これが問題になるのであれば、UID/GID を明確に割り当ててください。
TTY やシグナル転送を使わないつもりであれば、 sudo
のインストールや使用を避けたほうが良いでしょう。使うことで引き起こされる問題の解決は大変だからです。もし、どうしても sudo
のような機能が必要であれば(例:root としてデーモンを初期化するが、実行は root 以外で行いたい時)、 「 gosu 」を使うことができます。
あとは、レイヤの複雑さを減らすため、 USER
を頻繁に切り替えるべきではありません。
WORKDIR¶
明確さと信頼性のため、常に WORKDIR
からの絶対パスを使うべきです。また、 WORKDIR
を RUN cd ... && 何らかの処理
のように増殖する命令の代わり使うことで、より読みやすく、トラブルシュートしやすく、維持しやすくします。
ONBUILD¶
ONBULID
コマンドは Dockerfile
による構築後に実行されます。 ONBUILD
は FROM
から現在に至るあらゆる子イメージで実行できます。 ONBUILD
コマンドは親の Dockerfile
が子 Dockerfile
を指定する命令としても考えられます。
Docker は ONBUILD
コマンドを処理する前に、あらゆる子 Dockerfile
を実行します。
ONBUILD
は FROM
で指定したイメージを作ったあと、さらにイメージを作るのに便利です。例えば、言語スタック・イメージで ONBUILD
を使うと、 Dockerfile
内のソフトウェアは特定の言語環境を使えるようになります。これは Ruby の ONBUILD
でも 見られます 。
ONBUILD
によって構築されるイメージは、異なったタグを指定すべきです。例: ruby:1.9-onbuild
または ruby:2.0-onbuild
。
ONBUILD
で ADD
や COPY
を使う時は注意してください。追加された新しいリソースが新しいイメージ上で見つからなければ、「onbuild」イメージに破壊的な失敗をもたらします。先ほどお勧めしたように、別々のタグを付けることにより、 Dockerfile
の作者が選べるようになります。
さらなるリソース情報¶
参考
- Best practices for writing Dockerfiles
- https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/