Dockerfile のベストプラクティス

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

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

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

注釈

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

一般的なガイドラインと推奨

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

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

.dockerignore ファイルを使う

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

不要なパッケージのインストールを避ける

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

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

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

レイヤの数を最小に

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

複数行の引数

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

以下は buidpack-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 イメージ を推奨します。これは、非常にしっかりと管理されており、ディストリビューションの中でも最小(現在は 100 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 updaateapt-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.0* を指定します。従来のイメージが古いバージョンを使っていたとしても、新しいイメージは apt-get update でキャッシュを使わないので、確実に新しいバージョンをインストールします。各行はパッケージのリストであり、パッケージの重複という間違いをさせないためです。

付け加えると、apt キャッシュをクリーンにし、 /var/lib/apt/lits を削除することで、イメージのサイズを減らします。 RUN 命令は apt-get update から開始されるので、 apt-get install でインストールされるパッケージは、常に新鮮なものです。

CMD

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

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 のサポート)を持ち、一見では分かりません。したがって 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 命令

サービスは特権ユーザで実行せずに、 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

Dockerfile リファレンスの WORKDIR 命令

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

ONBUILD

Dockerfile リファレンスの ONBUILD 命令

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

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

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

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

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

公式レポジトリの例

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