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

このドキュメントで扱うのは、効率的なイメージ構築を目的とした、ベストプラクティスと手法のアドバイスです。

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

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

# syntax=docker/dockerfile:1
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) (これが「 コンテナ・レイヤ(container layer) 」です)を追加します。実行中のコンテナに対する全ての変更、たとえば新しいファイル書き込み、既存ファイルの編集、ファイルの削除などは、この書き込み可能なコンテナ・レイヤ内に記述されます。

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

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

一時的なコンテナを作成

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

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

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

docker build コマンドを実行するとき、 現在作業しているディレクトリ(current working directory)ビルド・コンテクスト(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 を離れたディレクトリに移動し、第2バージョンのイメージを構築します(先ほどのイメージ構築による、キャッシュに依存しないようにするためです)。 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 で除外 を参照ください。

注釈

標準入力の Dockerfile で 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

以下の例は、現在のディレクトリ( . )をビルド・コンテクストとして使います。また、イメージの構築には、 stdin の ` ヒア・ドキュメント <https://tldp.org/LDP/abs/html/here-docs.html>`_ を経由する Dockerfile を使います。

# 作業用のディレクトリを作成します
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

この構文が役立つ状況は、 Dockerfile を含まないリポジトリにあるイメージを構築したい場合や、自分でフォークしたリポジトリを保持することなく、任意の 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 は、以下のようになります。

# syntax=docker/dockerfile:1
FROM golang:1.16-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 \
  && rm -rf /var/lib/apt/lists/*

ビルド・キャッシュの活用

イメージの構築時、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 イメージ です。これはしっかりと管理されながら、容量が小さい(現時点で 6 MB 以下) 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 updateapt-get install は、同一の RUN 命令内にて同時実行するようにしてください。たとえば以下のようにします。

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo \
    && rm -rf /var/lib/apt/lists/*

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

# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl

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

# syntax=docker/dockerfile:1
FROM ubuntu:18.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.*

バージョン・ピニングでは、キャッシュにどのようなイメージがあろうとも、指定されたバージョンを使って構築します。この手法を使えば、そのパッケージの最新版に、思いもよらない変更が加わっていたとしても、ビルド失敗を回避できることもあります。

以下は、 apt-get の推奨する利用方法で整えられた RUN 命令です。

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 https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && …
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH

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

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

# syntax=docker/dockerfile:1
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 命令として実行する方法もあります。

# syntax=docker/dockerfile:1
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 https://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 https://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 に割り当てられます。これが問題となる場合は、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 の開発者にとっても判断ができるようになるので、不測の事態は軽減されます。

Docker 公式リポジトリの例

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

その他の情報