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

このトピックでは、効率的なイメージ構築を目的とした、ベストプラクティスと手法についてのアドバイスを扱います。

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

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

# syntax=docker/dockerfile:1
FROM ubuntu:22.04
COPY . /app
RUN make /app
CMD python /app/app.py

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

  • FROMubuntu:22.04 の Docker イメージからレイヤを作成

  • COPY は Docker クライアントで操作しているディレクトリから、ファイルを(コンテナのレイヤに)追加

  • RUN はアプリケーションを make で構築

  • CMD はコンテナ内で何のコマンドを実行するか指定

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

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

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

一時的なコンテナを作成

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

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

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

詳しい情報は ビルド コンテクスト を御覧ください。

標準入力を通して Dockerfile をパイプ

ローカル若しくはリモートのビルド・コンテクストを使い、標準入力(stdin)を通した Dockerfile のパイプにより、イメージを構築する機能が Docker にはあります。標準入力を通して 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 を使ってイメージ構築

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

docker build [OPTIONS]

以下のイメージ構築例は、標準入力を通して渡された 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 を読み込んで構築

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

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

以下の例は、現在のディレクトリ( . )をビルド・コンテクストとして使います。また、イメージの構築には、標準入力の ` ヒア・ドキュメント <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

リモートのビルド・コンテクストから構築するため標準入力から読み込む Dockerfile を使う

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

docker build [OPTIONS] -f- PATH

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

以下のイメージ構築例は、標準入力から読み込む Dockerfile を使い、 GitHub 上の "hello-wolrd" リポジトリ にあるファイル 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 ファイル を参照してください。

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

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

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

例えば、複数のレイヤを含む構築を行おうとしていて、ビルド・キャッシュを確実に再利用可能にしたい場合は、余り頻繁に変更しないものから、より頻繁に変更するものへと順番を並べます。以下のリストは命令の順番例です。

  1. アプリケーションの構築に必要なツールをインストール

  2. ライブラリの依存関係をインストール又は更新

  3. アプリケーションを生成

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

可能なら常に、イメージの基礎として最新の公式イメージを利用します。Docker の推奨は Alpine イメージ です。これはしっかりと管理されながら、容量が小さい(現時点で 6 MB 以下) Linux ディストリビューションです。

FROM 命令についての詳しい情報は、 Dockerfile リファレンスの FROM 命令 を御覧ください。

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"

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

RUN

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

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:22.04
RUN apt-get update
RUN apt-get install -y curl

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

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

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 がどのように動作するのかを十分に理解していないなら、使うべきではありません。

CMD 命令についての詳しい情報は、 Dockerfile リファレンスの CMD 命令 を御覧ください。

EXPOSE

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

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

EXPOSE 命令についての詳しい情報は、 Dockerfile リファレンスの EXPOSE 命令 を御覧ください。

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'

ENV 命令についての詳しい情報は、 Dockerfile リファレンスの ENV 命令 を御覧ください。

ADD と 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 を使うべきです。

ADDCOPY についての詳しい情報は以下を御覧ください:

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

このスクリプトは 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

ENTRYPOINT 命令についての詳しい情報は、 Dockerfile リファレンスの ENTRYPOINT 命令 を御覧ください。

VOLUME

VOLUME コマンドは、データベース・ストレージ領域、設定用ストレージ、Docker コンテナによって作成されるファイルやフォルダの公開に使います。イメージ内であらゆる可変的な部分、あるいはユーザが設定可能な部分では、 VOLUME の利用が強く推奨されます。

VOLUME 命令についての詳しい情報は、 Dockerfile リファレンスの VOLUME 命令 を御覧ください。

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 の設定を何度も繰り返すのは避けてください。

USER 命令についての詳しい情報は、 Dockerfile リファレンスの USER 命令 を御覧ください。

WORKDIR

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

WORKDIR 命令についての詳しい情報は、 Dockerfile リファレンスの WORKDIR 命令 を御覧ください。

ONBUILD

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

Docker によるビルドは、 子 Dockerfile 内のどの命令よりも先に ONBUILD 命令を実行します。

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

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

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

ONBUILD 命令についての詳しい情報は、 Dockerfile リファレンスの ONBUILD 命令 を御覧ください。

Docker 公式リポジトリの例

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