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 イメージ を推奨します。これは、非常にしっかりと管理されており、ディストリビューションの中でも小さくなるよう(現在は 150 MB 以下に)維持されているからです。
RUN¶
常に Dockerfile
をより読みやすく、理解しやすく、メンテナンスしやすくします。長ければ分割するか、複雑な RUN
命令はバックスラッシュを使い複数行に分割します。
おそらく RUN
の最も一般的な利用例は apt-get
アプリケーションです。 RUN apt-get
コマンドはパッケージをインストールしますので、気を付けるべきいくつかの注意点があります。
まず、RUN apt-get upgrade
や 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.*
を指定します。従来のイメージが古いバージョンを使っていたとしても、新しいイメージは apt-get update
でキャッシュを使いません。そのため、確実に新しいバージョンをインストールします。他の各行はパッケージのリストであり、パッケージの重複という間違いをさせないためです。
付け加えると、apt キャッシュをクリーンにし、 /var/lib/apt/lists
を削除すると、イメージのサイズを減らします。 RUN
命令は apt-get update
から開始しますので、 apt-get install
は常に新しいパッケージをインストールします。
CMD¶
CMD
命令は、イメージに含まれるソフトウェアの実行と、その引数のために使うべきです。また、CMD
は常に CMD [“実行ファイル”, “パラメータ1”, “パラメタ2”…]
のような形式で使うべきです。そのため、イメージがサービス向け(Apache、Rails 等)であれば、 CMD ["apache2","-DFOREGROUND"]
のようにすべきでしょう。実際に、あらゆるサービスのベースとなるイメージで、この命令形式が推奨されます。
その他の多くの場合、 CMD
はインタラクティブなシェル(bash、python、perl 等)で使われます。例えば、 CMD ["perl", "-de0"]
、 CMD ["python"]
、 CMD [“php”, “-a”]
です。この利用形式が意味するのは、 docker run -it python
のように実行したら、そのコマンドを使いやすいシェル上に落とし込み、すぐに使えるようにします。 また、あなたとあなたの想定ユーザが ENTRYPOINT
の動作に慣れていないなら、 CMD [“パラメータ”, “パラメータ”]
形式のように CMD
を ENTRYPOINT
と一緒に使うべきではないでしょう。
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 のサポート)を持ち、一見では処理内容が分かりません(訳者注:ファイルや 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¶
注釈
イメージ内で得られるユーザとグループは 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/