Dockerfile のベスト・プラクティス

このドキュメントは、効率的なイメージ構築のために推奨するベストプラクティスを扱います。

Docker は Dockerfile に書かれた命令を読み込み、自動的にイメージを構築します。 Dockerfile はイメージを構築するために必要な全ての命令を、順番通りに記述したテキストファイルです。 Dockerfile は特定の書式と命令群に忠実であり、それらは Dockerfile リファレンス で確認できます。

Dockerfile の命令に相当する読み込み専用のレイヤによって、 Docker イメージは構成されます。それぞれのレイヤは直前のレイヤから変更した差分であり、これらのレイヤは積み重なっています。次の Dockerfile を見ましょう。

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

命令ごとに1つのレイヤを作成します。

  • FROMubuntu:18.04 の Docker イメージからレイヤを作成
  • COPY は現在のディレクトリから Docker クライアントがファイルを追加
  • RUN はアプリケーションを make で構築
  • CMD はコンテナ内で何のコマンドを実行するか指定

イメージを実行し、コンテナを生成すると、元のレイヤ上に新しい 書き込み可能なレイヤ(writable layer) (これが「コンテナ・レイヤ」です)を追加します。実行中のコンテナに対する全ての変更、たとえば新しいファイル書き込み、既存ファイルの編集、ファイルの削除などは、この書き込み可能なコンテナ・レイヤ内に記述されます。

イメージ・レイヤに関する詳しい情報(さらに Docker のイメージ構築と保存の仕方について)は、 ストレージ・ドライバについて をご覧ください。

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

一時的なコンテナを作成

Dockerfile で定義したイメージによって生成するコンテナは、可能な限り一時的(ephemeral)であるべきです。「一時的」が意味するのは、コンテナは停止および破棄可能であり、その後も極めて最小限のセットアップと設定により、再構築および置き換え可能だからです。

ステートレス(状態を保持しない)手法をコンテナ実行で達成するモチベーションを感じ取るには、「The Twelve-factor app」 手法の Process 以下を参照ください。

ビルド・コンテクストの理解

docker build コマンドを発行するとき、現在作業しているディレクトリのことを ビルド・コンテクスト(buid context) と呼びます。デフォルトでは、Dockerfile はここにあると見なされますが、フラグ( -f )の指定によって違う場所も指定できます。 Dockerfile がどこにあるかに関係なく、現在のディレクトリ以下にある再帰的なファイルとディレクトリを、すべて Docker デーモンに対してビルド・コンテクストとして送信します。

注釈

ビルド・コンテクストの例

ビルド・コンテクスト用のディレクトリを作成し、 cd で中に移動します。「hello」を hello という名前のテキストファイルに書き込むため、 cat を実行する Dockerfile を作成します。そして、ビルド・コンテクスト( . )内からイメージを構築します。

mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .

Dockerfilehello を離れたディレクトリに移動し、次のバージョンのイメージを構築します(先ほど構築したイメージから、キャッシュを依存しないようにするためです)。 Dockerfile の場所とビルド・コンテクストのディレクトリを指定するのに -f を使います。

mkdir -p dockerfiles context
mv Dockerfile dockerfiles && mv hello context
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

イメージ構築に不要なファイルをうっかり含んでしまうと、ビルド・コンテクストが肥大化し、イメージの容量が大きくなってしまいます。これによりイメージの構築時間が増えるだけでなく、 pull や push の時間が延び、コンテナランタイムのサイズも大きくなります。ビルド・コンテクストがどれくらいの大きいかを調べるには、 Dockerfile を構築時に表示される次のようなメッセージで確認します:

Sending build context to Docker daemon  187.8MB

stdin を通して Dockerfile をパイプ

ローカルもしくはリモートのビルド・コンテクストを使い、 stdin (標準入力)を通して Dockerfile をパイプすることで、イメージを構築する機能が Docker にはあります。 stdin を通して Dockerfile をパイプすると、Dockerfile をディスクに書き込むことがないので、一回限りの構築を行いたい時に役立ちます。あるいは、 Dockerfile が生成された場所が、後で残らない状況でも役立つでしょう。

注釈

このセクションで扱うのは、ドキュメント向けの分かりやすい例ですが、どのような Dockerfile でも stdin を利用できます

たとえば、以下のコマンドは、どちらも同じ処理をします。

echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF

以上の例は、好きな方法や、利用例に一番あう方法に置き換えられます。

ビルド・コンテクストを送信せず、stdin からの Dockerfile を使ってイメージ構築

以下の構文を使うと、 stdin からの Dockerfile を使ってイメージを構築するため、ビルド・コンテクストとして送信するために、ファイルの追加が不要です。ハイフン( - )が意味するのは PATH に替わるもので、ディレクトリの代わりに stdin からのビルド・コンテクスト( Dockerfile だけを含みます )をが読み込むよう、 Docker に命令します。

docker build [OPTIONS]

以下のイメージ構築例は、 stdin を通して Dockerfile を使います。ビルド・コンテクストとしてデーモンには一切のファイルを送信しません。

docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF

デーモンに対してファイルを一切送信しないため、Dockerfile をイメージの中にコピーする必要がない状況や、構築速度を改善するためには、ビルド・コンテクストの省略が役立ちます。

ビルド・コンテクストから不要なファイルを除外し、構築速度の改善をしたければ、 .dockerignore で除外 を参照ください。

注釈

標準入力の Dockerile で COPYADD 構文を使おうとしても、構築できません。以下の例は失敗します。

# 作業用のディレクトリを作成します
mkdir example
cd example

# ファイル例を作成します
touch somefile.txt

docker build -t myimage:latest -<<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF

# 構築失敗を表示します
...
Step 2/3 : COPY somefile.txt .
COPY failed: stat /var/lib/docker/tmp/docker-builder249218248/somefile.txt: no such file or directory

ローカルのビルド・コンテクストとして、stdin からの Dockerfile を読み込んで構築

ローカル・ファイルシステム上ファイルを使って構築する構文には、 stdin からの Dockerfile を使います。この構文では、 -f (あるいは --file )オプションで、使用する Dockerfile を指定します。そして、ファイル名としてハイフン( - )を使い、Docker には stdin から Dockerfile を読み込むように命令します。

docker build [オプション] -f- PATH

以下の例は、現在のディレクトリ( . )をビルド・コンテクストとして使います。また、Dockerfile は ` ヒア・ドキュメント を使う stdin を経由し、イメージを構築します。

# 作業用のディレクトリを作成します
mkdir example
cd example

# ファイル例を作成します
touch somefile.txt

# build an image using the current directory as context, and a Dockerfile passed through stdin
# イメージ構築のために、現在のディレクトリをコンテクストとして用い、Dockerfile は stdin を通します
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF

リモートのビルド・コンテクストとして、stdin からの Dockerfile を読み込んで構築

リモート git リポジトリにあるファイルを使って構築する構文には、 stdin からの Dockerfile を使います。この構文では、 -f (あるいは --file )オプションで、使用する Dockerfile を指定します。そして、ファイル名としてハイフン( - )を使い、Docker には stdin から Dockerfile を読み込むように命令します。

docker build [OPTIONS] -f- PATH

この構文が役立つ状況は、 Dockerifle を含まないリポジトリにあるイメージを構築したい場合や、自分でフォークしたリポジトリを保持することなく、任意の Dockerfile でビルドしたい場合です。

以下のイメージ構築例は stdin からの Dockerfile を使い、 GitHub 上の "hello-wolrd" Git リポジトリ にあるファイル hello.c を追加します。

docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
FROM busybox
COPY hello.c .
EOF

注釈

中の仕組み

リモートの Git リポジトリをビルド・コンテクストに使ってイメージを構築する時に、 Docker はリポジトリの git clone をローカルマシン上で処理し、これらの取得したファイルをビルド・コンテクストとしてデーモンに送信します。この機能を使うには、 docker build コマンドを実行するホスト上に git をインストールする必要があります。

.dockerignore で除外

(ソース・リポジトリを再構築することなく)イメージの構築と無関係のファイルを除外するには、 .dockerignore ファイルを使います。このファイルは .gitignore と似たような除外パターンをサポートします。ファイルの作成に関する情報は .dockerignore ファイル を参照してください。

マルチステージ・ビルドを使う

マルチステージ・ビルド は、中間レイヤとイメージの数を減らすのに苦労しなくても、最終イメージの容量を大幅に減少できます。

構築プロセスの最終段階のビルドを元にイメージを作成するため、 ビルド・キャッシュの活用 によってイメージ・レイヤを最小化できます。

たとえば、複数のレイヤが入った構築をする時には、(ビルド・キャッシュを再利用可能にしている場合)頻繁に変更しないものから順番に、より頻繁に変更するものへと並べます。

  • アプリケーションの構築に必要なツールをインストール
  • ライブラリの依存関係をインストールまたは更新
  • アプリケーションを生成

Go アプリケーションに対する Dockerfile は、以下のようになります。

FROM golang:1.11-alpine AS build

# プロジェクトに必要なツールをインストール
# 依存関係を更新するには「docker build --no-cache」を実行(キャッシュを無効化するオプション)
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# Gopkg.toml と Gopkg.lock はプロジェクトの依存関係の一覧
# Gopkg ファイルが更新された時のみ、レイヤを再構築
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# ライブラリの依存関係をインストール
RUN dep ensure -vendor-only

# プロジェクト全体をコピーし、構築
# プロジェクトのディレクトリ内でファイルの変更があれば、レイヤを再構築
COPY . /go/src/project/
RUN go build -o /bin/project

# 結果として、1つのレイヤ・イメージになる
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

不要なパッケージのインストール禁止

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

アプリケーションを切り離す

各コンテナはただ1つだけの用途を持つべきです。アプリケーションを複数のコンテナに切り離すことで、水平スケールやコンテナの再利用がより簡単になります。たとえば、ウェブアプリケーションのスタックであれば、3つのコンテナに分割できるでしょう。切り離す方法にすると、ウェブアプリケーションの管理、データベース、メモリ内のキャッシュ、それぞれが独自のイメージを持ちます。

各コンテナに1つのプロセスに制限するのは、経験的には良い方針です。しかしながら、これは大変かつ厳しいルールです。たとえば、コンテナで init プロセスを生成 する時、プログラムによっては、そのプロセスが許容する追加プロセスも生成するでしょう。他にもたとえば、 Celery は複数のワーカ・プロセスを生成しますし、 Apache はリクエストごとに1つのプロセスを作成します。

ベストな判断のためには、コンテナを綺麗(クリーン)に保ち、可能であればモジュール化します。コンテナがお互いに依存する場合は、 Docker コンテナ・ネットワーク を使い、それぞれのコンテナが通信できるようにします。

レイヤの数を最小に

Docker の古いバージョンでは、性能を確保するために、イメージ・レイヤ数の最小化が重要でした。以下の機能は、この制限を減らすために追加されたものです。

  • RUNCOPYADD 命令のみレイヤを作成します。他の命令では、一時的な中間イメージ(temporary intermediate images)を作成し、構築時の容量は増えません。
  • 可能であれば、 マルチステージ・ビルド を使い、必要な最終成果物(アーティファクト)のみ最終イメージにコピーします。これにより、中間構築ステージではツールやデバッグ情報を入れられますし、最終イメージの容量も増えません。

複数行にわたる引数は並びを適切に

可能であれば常に、後々の変更を簡単にするため、複数行にわたる引数はアルファベット順にします。しておけば、パッケージを重複指定することはなくなり、一覧の変更も簡単になります。プルリクエストを読んだりレビューしたりすることも、さらに楽になります。バックスラッシュ( \ ) の前に空白を含めるのも同様です。

以下は 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 が適切なイメージを見つけた上で、どのようなときにキャッシュを利用し、どのようなときには利用しないのかを理解しておくことが必要です。Docker が従っている規則は以下のとおりです。

  • キャッシュ内にすでに存在している親イメージから処理を始めます。そのベースとなるイメージから派生した子イメージに対して、次の命令が合致するかどうかが比較され、子イメージのいずれかが同一の命令によって構築されているかを確認します。そのようなものが存在しなければ、キャッシュは無効になります。
  • ほとんどの場合、 Dockerfile 内の命令と子イメージのどれかを単純に比較するだけで十分です。しかし命令によっては、多少の検査や解釈が必要となるものもあります。
  • ADD 命令や COPY 命令では、イメージに含まれるファイルの内容が検査され、個々のファイルについてチェックサムが計算されます。この計算において、ファイルの最終更新時刻、最終アクセス時刻は考慮されません。キャッシュを探す際に、このチェックサムと既存イメージのチェックサムが比較されます。ファイル内の何かが変更になったとき、たとえばファイル内容やメタデータが変わっていれば、キャッシュは無効になります。
  • ADDCOPY 以外のコマンドの場合、キャッシュのチェックは、コンテナ内のファイル内容を見ることはなく、それによってキャッシュと一致しているかどうかが決定されるわけでありません。たとえば RUN apt-get -y update コマンドの処理が行われる際には、コンテナ内にて更新されたファイルは、キャッシュが一致するかどうかの判断のために用いられません。この場合にはコマンド文字列そのものが、キャッシュの一致判断に用いられます。

キャッシュが無効になると、次に続く Dockerfile コマンドは新たなイメージを生成し、以降ではキャッシュを使いません。

Dockerfile 命令

以下にある推奨項目のねらいは、効率的かつメンテナンス可能な Dockerfile の作成に役立つことです。

FROM

Dockerfile リファレンスの FROM コマンド

可能なら常に、イメージの土台には最新の公式イメージを利用します。私たちの推奨は Alpine イメージ です。これは非常にコントロールされながら、容量が小さい(現時点で 5MB 以下) Linux ディストリビューションです。

LABEL

オブジェクト・ラベルの理解

イメージにラベルを追加するのは、プロジェクト内でのイメージ管理をしやすくしたり、ライセンス情報の記録や自動化の助けとするなど、さまざまな目的があります。ラベルを指定するには、 LABEL で始まる行を追加して、そこにキーと値のペア(key-value pair)をいくつか設定します。以下に示す例は、いずれも正しい構文です。説明をコメントとしてつけています。

注釈

文字列に空白が含まれる場合は、引用符でくくるか あるいは エスケープする必要があります。文字列内に引用符( " )がある場合も、同様にエスケープしてください。

# 個別のラベルを設定
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""

イメージには複数のラベルを設定できます。Docker 1.10 未満では、余分なレイヤが追加されるのを防ぐため、1つの LABEL 命令中に複数のラベルをまとめる手法が推奨されていました。もはやラベルをまとめる必要はありませんが、今もなおラベルの連結をサポートしています。

# 1行でラベルを設定
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"

上の例は以下のように書き換えられます。

# 複数のラベルを一度に設定、ただし行継続の文字を使い、長い文字列を改行する
LABEL vendor=ACME\ Incorporated \
      com.example.is-beta= \
      com.example.is-production="" \
      com.example.version="0.0.1-beta" \
      com.example.release-date="2015-02-12"

ラベルにおける利用可能なキーと値のガイドラインとしては オブジェクトラベルを理解する を参照してください。またラベルの検索に関する情報は オブジェクト上のラベルの管理 のフィルタリングに関する項目を参照してください。

RUN

Dockerfile リファレンスの RUN コマンド

Dockerfile をより読みやすく、理解しやすく、メンテナンスしやすくするためには、長く複雑な RUN 命令を、バックスラッシュで複数行に分けてください。

apt-get

おそらく RUN において一番利用する使い方が apt-get アプリケーションの実行です。RUN apt-get はパッケージをインストールするものであるため、注意点がいくつかあります。

RUN apt-get upgradedist-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

1つの RUN コマンド内で apt-get update だけを使うとキャッシュに問題が発生し、その後の apt-get install コマンドが失敗します。たとえば Dockerfile を以下のように記述したとします。

FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y curl

イメージが構築されると、レイヤーがすべて Docker のキャッシュに入ります。この次に apt-get install を編集して別のパッケージを追加したとします。

FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y curl nginx

Docker は当初のコマンドと修正後のコマンドを見て、同一のコマンドであると判断するので、前回の処理において作られたキャッシュを再利用します。 キャッシュされたものを利用して処理が行われるわけですから、結果として apt-get update は実行 されませんapt-get update が実行されないということは、つまり curl にしても nginx にしても、古いバージョンのまま利用する可能性が出てくるということです。

RUN apt-get update && apt-get install -y というコマンドにすると、 Dockerfile が確実に最新バージョンをインストールしてくれるものとなり、さらにコードを書いたり手作業を加えたりする必要がなくなります。これは「キャッシュ・バスティング(cache busting)」と呼ばれる技術です。この技術は、パッケージのバージョンを指定することによっても利用することができます。これはバージョン・ピニング(version pinning)というものです。 以下に例を示します。

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 \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*

s3cmd のコマンド行は、バージョン 1.1.* を指定しています。以前に作られたイメージが古いバージョンを使っていたとしても、新たなバージョンの指定により apt-get update のキャッシュ・バスティングが働いて、確実に新バージョンがインストールされるようになります。パッケージを各行に分けて記述しているのは、パッケージを重複して書くようなミスを防ぐためです。

apt キャッシュをクリーンアップし /var/lib/apt/lists を削除するのは、イメージサイズを小さくするためです。そもそも apt キャッシュはレイヤー内に保存されません。RUN コマンドを apt-get update から始めているので、 apt-get install の前に必ずパッケージのキャッシュが更新されることになります。

注釈

公式の Debian と Ubuntu のイメージは 自動的に apt-get clean を実行する ので、明示的にこのコマンドを実行する必要はありません。

パイプの利用

RUN コマンドの中には、その出力をパイプを使って他のコマンドへ受け渡すことを前提としているものがあります。そのときにはパイプを行う文字( | )を使います。たとえば以下のような例があります。

RUN wget -O - https://some.site | wc -l > /number

Docker はこういったコマンドを /bin/sh -c というインタープリタ実行により実現します。正常処理されたかどうかは、パイプの最後の処理の終了コードにより評価されます。上の例では、このビルド処理が成功して新たなイメージが生成されるかどうかは、wc -l コマンドの成功にかかっています。 つまり wget コマンドが成功するかどうかは関係がありません。

パイプ内のどの段階でも、エラーが発生したらコマンド失敗としたい場合は、頭に set -o pipefail && をつけて実行します。こうしておくと、予期しないエラーが発生しても、それに気づかずにビルドされてしまうことはなくなります。たとえば以下です。

RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

注釈

すべてのシェルが -o pipefail オプションをサポートしているわけではありません。

その場合(例えば Debian ベースのイメージにおけるデフォルトシェル dash である場合)、RUN コマンドにおける exec 形式の利用を考えてみてください。 これは pipefail オプションをサポートしているシェルを明示的に指示するものです。 たとえば以下です。

RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]

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 というコマンドを実行したときに、指定したシェルの中に入り込んで、処理を進めていくことを意味します。CMDENTRYPOINT を組み合わせて用いる CMD ["引数", "引数"] という実行形式がありますが、これを利用するのはまれです。開発者自身や利用者にとって ENTRYPOINT がどのように動作するのかが十分に理解していないなら、用いないようにしましょう。

EXPOSE

Dockerfile リファレンスの EXPOSE コマンド

EXPOSE コマンドは、コンテナが接続のためにリッスンするポートを指定します。当然のことながらアプリケーションにおいては、標準的なポートを利用します。たとえば Apache ウェブ・サーバを含んでいるイメージに対しては EXPOSE 80 を使います。また MongoDB を含んでいれば EXPOSE 27017 を使うことになります。

外部からアクセスできるようにするため、これを実行するユーザは docker run にフラグをつけて実行します。そのフラグとは、指定されているポートを、自分が取り決めるどのようなポートに割り当てるかを指示するものです。Docker のリンク機能においては環境変数が利用できます。受け側のコンテナが提供元をたどることができるようにするものです(例: MYSQL_PORT_3306_TCP )。

ENV

Dockerfile リファレンスの ENV コマンド

新しいソフトウェアに対しては ENV を用いれば簡単にそのソフトウェアを実行できます。コンテナがインストールするソフトウェアに必要な環境変数 PATH を、この ENV を使って更新します。たとえば ENV PATH /usr/local/nginx/bin:$PATH を実行すれば、 CMD ["nginx"] が確実に動作するようになります。

ENV コマンドは、必要となる環境変数を設定するときにも利用します。たとえば Postgres の 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

プログラムにおける(ハードコーディングではない)定数定義と同じことで、この方法をとっておくのが便利です。ただ1つの ENV コマンドを変更するだけで、コンテナ内のソフトウェアバージョンは、いとも簡単に変えてしまうことができるからです。

RUN 命令のように、各 ENV 行によって新しい中間レイヤを作成します。つまり、以降のレイヤで環境変数をアンセットしても、このレイヤが値を保持するため、値を排除できません。この挙動は以下ような Dockerfile で確認できますので、構築してみましょう。

FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
$ docker run --rm test sh -c 'echo $ADMIN_USER'

mark

この挙動を避けるには、 RUN 命令でシェルのコマンドを使い、環境変数を実際にアンセットします。ただし、レイヤ内の環境変数の指定とアンセットを、1つのレイヤで指定する必要があります。コマンドは ;& で分割できます。ただし、 & を使う場合、どこかの行の1つでも失敗すると、 docker build そのものが失敗します。 \ をライン継続文字として使う方が、 Linux Dockerfile の読み込みやすさを改善します。また、コマンドのすべてをシェルスクリプトにし、そのスクリプトを RUN コマンドとして実行する方法もあります。

FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh
$ docker run --rm test sh -c 'echo $ADMIN_USER'

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 "$@"

注釈

PID 1 としてアプリを設定

このスクリプトは Bash コマンドの exec を用います。 このため最終的に実行されたアプリケーションが、コンテナの PID として 1 を持つことになります。 こうなるとそのアプリケーションは、コンテナに送信された Unix シグナルをすべて受信できるようになります。 詳細は ENTRYPOINT を参照してください。

ヘルパースクリプトはコンテナの中にコピーされ、コンテナ開始時に ENTRYPOINT から実行されます。

COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["postgres"]

このスクリプトを用いると、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 を明示的に割り当ててください。

Go 言語の archive/tar パッケージが取り扱うスパースファイルにおいて 未解決のバグ があります。これは Docker コンテナ内にて非常に大きな値の UID を使ってユーザを生成しようとするため、ディスク消費が異常に発生します。コンテナ・レイヤ内の /var/log/faillog が NUL (\0) キャラクタにより埋められてしまいます。useradd に対して --no-log-init フラグをつけることで、とりあえずこの問題は回避できます。ただし Debian/Ubuntu の adduser ラッパーは --no-log-init フラグをサポートしていないため、利用することはできません。

sudo のインストールとその利用は避けてください。TTY やシグナル送信が予期しない動作をするため、多くの問題を引き起こすことになります。 sudo と同様の機能(たとえばデーモンの初期化を root により行い、起動は root 以外で行うなど)を実現する必要がある場合は、 gosu を使うとよいかもしれません。

レイヤ数を減らしたり複雑にならないようにするためには、 USER の設定を何度も繰り返すのは避けてください。

WORKDIR

Dockerfile リファレンスの WORKDIR コマンド

WORKDIR に設定するパスは、分かり易く確実なものとするために、絶対パス指定としてください。また RUN cd && do-something といった長くなる一方のコマンドを書くくらいなら、 WORKDIR を利用してください。そのような書き方は読みにくく、トラブル発生時には解決しにくく保守が困難になるためです。

ONBUILD

Dockerfile リファレンスの ONBUILD コマンド

ONBUILD コマンドは、 Dockerfile によるビルドが完了した後に実行されます。ONBUILD は、現在のイメージから FROM によって派生した子イメージにおいて実行されます。つまり ONBUILD とは、親の Dockerfile から子どもの Dockerfile へ与える命令であると言えます。

Docker によるビルドにおいては ONBUILD の実行が済んでから、子イメージのコマンド実行が行われます。

ONBUILD は、所定のイメージから FROM を使ってイメージをビルドしようとするときに利用できます。たとえば特定言語のスタックイメージは ONBUILD を利用します。Dockerfile 内にて、その言語で書かれたどのようなユーザ・ソフトウェアであってもビルドすることができます。その例として Ruby's ONBUILD variants があります。

ONBUILD によって構築するイメージは、異なったタグを指定してください。たとえば ruby:1.9-onbuildruby:2.0-onbuild などです。

ONBUILD において ADDCOPY を用いるときは注意してください。"onbuild" イメージが新たにビルドされる際に、追加しようとしているリソースが見つからなかったとしたら、このイメージは復旧できない状態になります。上に示したように個別にタグをつけておけば、 Dockerfile の開発者にとっても判断ができるようになるので、不測の事態は軽減されます。

公式リポジトリの例

以下に示すのは代表的な Dockerfile の例です。