Dockerfile を書くベスト・プラクティス

Docker は Dockerfile の命令を読み込み、自動的にイメージを構築できます。これはテキストファイルであり、特定のイメージを構築するために必要な全ての命令が入っています。 Dockerfile は個別の命令セットを使い、特定の形式で記述します。基本は Dockerfile リファレンス で学べます。新しく Dockerfile を書こうとしているのであれば、そのリファレンスから出発しましょう。

このドキュメントは Docker 社が推奨するベストプラクティスや手法を扱っており、Docker コミュニティが簡単かつ効率的に Dockerfile を作成できるようにします。この推奨方法に従うことを強く勧めます(実際、公式イメージを作成するには、これらのプラクティスに従う 必要 があります)。

多くのベストプラクティスや推奨する手法は、 buildpack-depsDockerfile でご覧いただけます。

注釈

Dockerfile の命令に関するより詳細な説明は、 Dockerfile リファレンス ページをご覧ください。

一般的なガイドラインとアドバイス

コンテナはエフェメラルであるべき

Dockerfile で定義されたイメージを使って作成するコンテナは、可能ならばエフェメラル(短命;ephemeral)にすべきです。私たちの「エフェメラル」とは、停止・破棄可能であり、明らかに最小のセットアップで構築して使えることを意味します。

.dockerignore ファイルの利用

ほとんどの場合、空のディレクトリに個々の Dockerfile を置くのがベストです。そうしておけば、そのディレクトリには Dockerfile が構築に必要なファイルだけ追加します。構築のパフォーマンスを高めるには、 .dockerignore ファイルを作成し、対象ディレクトリ上のファイルやディレクトリを除外できます。このファイルの除外パターンは .gitignore ファイルに似ています。ファイルを作成するには、 .dockerignore ファイル をご覧ください。

不要なパッケージをインストールしない

複雑さ、依存関係、ファイルサイズ、構築時間をそれぞれ減らすために、余分ないし不必要な「入れた方が良いだろう」というパッケージは、インストールを避けるべきです。例えば、データベース・イメージであればテキストエディタは不要でしょう。

コンテナごとに1つのプロセスだけ実行

ほとんどの場合、1つのコンテナの中で1つのプロセスだけ実行すべきです。アプリケーションを複数のコンテナに分離することは、水平スケールやコンテナの再利用を簡単にします。サービスとサービスに依存関係がある場合は、 コンテナのリンク を使います。

レイヤの数を最小に

Dockerfile の読みやすさと、使用するイメージレイヤ数の最小化は、両者のバランスを見つける必要があります。戦略的に注意深くレイヤ数をお使いください。

複数行の引数

可能であれば常に複数行の引数をアルファベット順にします。これにより、パッケージが重複しないようにし、あるいはリストの更新を容易にします。また、プルリクエストの読み込みとレビューをより簡単にします。バックスラッシュ( \ ) の前に空白を追加するのも、同じく役立つでしょう。

以下は 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 命令と子イメージの単純な比較で十分です。しかし、命令によっては更なる検査や追加検査が必要になります。
  • ADDCOPY 命令は、イメージに含まれるファイルが検査され、各ファイルのチェックサムを計算します。ファイルの最終編集・最終アクセス時間は、チェックサムに影響ありません。キャッシュを探し、既存のイメージのチェックサムと比較します。内容やメタデータのようにファイルに変更があれば、キャッシュを無効化します。
  • ADDCOPY コマンドだけでなく、キャッシュのチェックにおいて、キャッシュが一致すると思われるコンテナ内のファイル状態を確認しません。例えば、 RUN apt-get -y update コマンドによってコンテナ内のファイルに変更を加えたとしても、キャッシュの有無に影響を与えません。この場合、コマンドの文字列自身が一致するかどうかしか見ないためです。
  • キャッシュを無効化すると、以降の Dockerfile 命令ではキャッシュは使われず、新しいイメージを生成します。

Dockerfile 命令

以下では、Dockerfile で様々な命令を使うにあたり、推奨するベストな方法が分かるでしょう。

FROM

Dockerfile リファレンスの FROM 命令

可能であれば、自分のイメージの元として現在の公式リポジトリを使います。私たちは Debian イメージ を推奨します。これは、非常にしっかりと管理されており、ディストリビューションの中でも小さくなるよう(現在は 150 MB 以下に)維持されているからです。

RUN

Dockerfile リファレンスの RUN 命令

常に Dockerfile をより読みやすく、理解しやすく、メンテナンスしやすくします。長ければ分割するか、複雑な RUN 命令はバックスラッシュを使い複数行に分割します。

おそらく RUN の最も一般的な利用例は apt-get アプリケーションです。 RUN apt-get コマンドはパッケージをインストールしますので、気を付けるべきいくつかの注意点があります。

まず、RUN apt-get updatedist-upgrade を避けるべきでしょう。ベース・イメージに含まれる「必須」パッケージの多くが、権限を持たないコンテナの内部で更新されないためです。もし、ベース・イメージに含まれるパッケージが時代遅れになっていれば、イメージのメンテナに連絡すべきでしょう。例えば、 foo という特定のパッケージを知っていて、それを更新する必要があるのであれば、自動的に更新するために apt-get install -y foo を使います。

RUN apt-get updateapt-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 を更新していないため、古いバージョンの curlnginx パッケージを取得する恐れがあります。

そこで、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/lits を削除すると、イメージのサイズを減らします。 RUN 命令は apt-get update から開始しますので、 apt-get install は常に新しいパッケージをインストールします。

CMD

Dockerfile リファレンスの 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 [“パラメータ”, “パラメータ”] 形式のように CMDENTRYPOINT と一緒に使うべきではないでしょう。

EXPOSE

Dockerfile リファレンスの EXPOSE 命令

EXPOSE 命令は、コンテナが接続用にリッスンするポートを指定します。そのため、アプリケーションが一般的に使う、あるいは、伝統的なポートを使うべきです。例えば、Apache ウェブ・サーバのイメージは EXPOSE 80 を使い、MongoDB を含むイメージであれば EXPOSE 27017 を使うでしょう。

外部からアクセスするためには、ユーザが docker run 実行時にフラグを指定し、特定のポートを任意のポートに割り当てられます。コンテナのリンク機能を使えば、Docker はコンテナがソースをたどれるよう、環境変数を提供します(例: MYSQL_PORT_3306_TCP )。

ENV

Dockerfile リファレンスの ENV 命令

新しいソフトウェアを簡単に実行するため、コンテナにインストールされているソフトウェアの PATH 環境変数を ENV を使って更新できます。例えば、 ENV PATH /usr/local/nginx/bin:$PATHCMD ["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 命令

ADDCOPY の機能は似ていますが、一般的には 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 上のパッケージを取得するのは可能な限り避けてください。その代わりに curlwget を使うべきです。この方法であれば、展開後に不要となったファイルを削除でき、イメージに余分なレイヤを増やしません。例えば、次のような記述は避けるべきです。

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

Dockerfile リファレンスの VOLUME 命令

VOLUME 命令はデータベース・ストレージ領域、設定用ストレージ、Docker コンテナによって作成されるファイルやフォルダの公開のために使います。イメージにおいて変わりやすい場所・ユーザによって便利な場所として VOLUME の利用が強く推奨されます。

USER

Dockerfile リファレンスの USER 命令

注釈

イメージ内で得られるユーザとグループは UID/GID に依存しないため、イメージの構築に関係なく次の UID/GID が割り当てられます。そのため、これが問題になるのであれば、UID/GID を明確に割り当ててください。

TTY やシグナル送信を使わないつもりであれば、 sudo のインストールや使用を避けると良いでしょう。使うことで引き起こされる問題の解決は大変だからです。もし、どうしても sudo のような機能が必要であれば(例:root としてデーモンを初期化しますが、実行は root 以外で行いたい時)、 「 gosu 」を利用ができます。

あとは、レイヤの複雑さを減らすため、 USER を頻繁に切り替えるべきではありません。

WORKDIR

Dockerfile リファレンスの WORKDIR 命令

明確さと信頼性のため、常に WORKDIR からの絶対パスを使うべきです。また、 WORKDIRRUN cd ... && 何らかの処理 のように増殖する命令の代わり使うことで、より読みやすく、トラブルシュートしやすく、維持しやすくします。

ONBUILD

Dockerfile リファレンスの ONBUILD 命令

ONBULID 命令は現``Dockerfile`` による構築後に実行します。 ONBUILDFROM から現在に至るあらゆる子イメージで実行できます。 ONBUILD コマンドは親の Dockerfile が子 Dockerfile を指定する命令としても考えられます。

Docker は ONBUILD コマンドを処理する前に、あらゆる子 Dockerfile 命令を実行します。

ONBUILDFROM で指定したイメージを作ったあと、更にイメージを作るのに便利です。例えば、言語スタック・イメージで ONBUILD を使えば、 Dockerfile 内のソフトウェアは特定の言語環境を使えるようになります。これは Ruby の ONBUILD でも 見られます

ONBUILD によって構築されるイメージは、異なったタグを指定すべきです。例: ruby:1.9-onbuildruby:2.0-onbuild

ONBUILDADDCOPY を使う時は注意してください。追加された新しいリソースが新しいイメージ上で見つからなければ、「onbuild」イメージに破壊的な失敗をもたらします。先ほどお勧めしたように、別々のタグを付けておけば、 Dockerfile の書き手が選べるようになります。

公式リポジトリの例

模範的な Dockerfile の例をご覧ください。