Docker
dockerfile

Dockerfileを書くためのベストプラクティス【参考訳】v18.09


概要

Docker Documentation にある、Best practices for writing Dockerfiles の参考日本語訳です。ドキュメントは、2019年5月31日現在のカレントである Docker v18.09 (current) です。

背景 ―― 以前の翻訳から時間が経過し、全体的に手直ししたいものの、差分が大きすぎる状況です。そのため、リファレンスや重要性の高いものから優先的に着手することにしました。


スライド資料

背景やヒント、図解などを追加した補足用スライドを作成しました

BuildKitなどの最新機能や Dockerfile の記述例については、こちらのスライドをご覧ください。


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

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

Docker は Dockerfile の命令を読み込み、自動的にイメージを構築(ビルド)します。 Dockerfile とはテキストファイルであり、あるイメージの構築に必要な、全ての命令を含みます。 DockerfileDockerfile リファレンス(英語) にある固有のフォーマットと命令群に従います。

Docker イメージは読み込み専用(read-only)のレイヤ群で構成され、各レイヤが Dockerfile 命令に相当します。それぞれのレイヤは積み重なっており、それぞれのレイヤは、以前に存在していたレイヤの変更差分です。次の 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 ) 、(これが、コンテナ・レイヤ "container layer" です)を追加します。実行中のコンテナに対するすべての変更、例えば新しいファイルの書き込み、既存ファイルの変更、ファイルの削除といったものは、すべてこの書き込み可能な薄いコンテナ・レイヤに対して書き込まれます。

イメージ・レイヤ(と Docker がイメージをどのように構築および保存するか)については ストレージ・ドライバについて(英語) をご覧ください。


一般的なガイドラインと助言(アドバイス)


エフェメラル(一時的)なコンテナを作成

Dockerfile で定義したイメージで作成するコンテナとは、可能であればエフェメラル(訳者注:「永続的」に対する「一時的」な利用の意味)であるべきです。私たちにとっての「エフェメラル」が意味するコンテナの挙動は、停止および破棄可能であり、圧倒的に最小限のセットアップと設定ファイルによって、再構築および置き換えが可能です。

Twelve-factor App 手法の プロセス(英語) 以下を参照すると、このようにステートレスな方式(訳者注:状態を常に保持・把握し続けない、という意味)でコンテナを実行する動機について、感触が得られるでしょう。


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

docker build コマンドを実行時、現在の作業ディレクトリを ビルド・コンテクスト(build 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番目のバージョンを構築します(先ほど構築したキャッシュに依存しません)。 -f を使い Dockerfile の場所を示し、ビルド・コンテクストのディレクトリを指定します。

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 のパイプ

Docker はローカル又はリモートのビルド・コンテクストを stdin (訳者注:「標準入力」のこと)を通し、 Dockerfile にパイプしてイメージを構築できます。 stdinDockerfile にパイプすると便利なのは、Dockerfile をディスクに書く必要がない1度限りの処理や、 Dockerfile を生成しても、以後に使用しない場合です。

このセクションの例では簡単にするためにヒア・ドキュメントを使っていますが、 stdin 上で Dockerfile を提供するあらゆる方法が使えます。

例えば、以下のコマンドは処理内容が同じです。

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 の位置にハイフン( - )を置くと、 ディレクトリではなく、( Dockerfile だけを含む ) stdin からビルド・コンテクストを読むように、 Docker へ指示します。

docker build [OPTIONS] -

以下の例では、 stdin を通した Dockerfile を使ってイメージを構築します。ビルド・コンテクストからデーモンに対して送信するファイルはありません。

docker build -t myimage:latest -<<EOF

FROM busybox
RUN echo "hello world"
EOF

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

構築速度を上げるため、何らかのファイルをビルド・コンテクストから除外したい場合には、 .dockerignore で除外 をご覧ください。

メモ: COPYADD を用いる Dockerfile でビルドを試みる場合、この構文を用いると失敗します。以下の例で説明します。


# 作業するディレクトリを作成

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 を使います。この構文では stdin から Dockerfile を読み込むファイルを Docker に命令するため、 -f (又は --file )オプションで使用する Dockerfile を指定します。

docker build [OPTIONS] -f- PATH

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

# 作業するディレクトリを作成

mkdir example
cd example

# サンプル・ファイルを作成
touch somefile.txt

# 現在のディレクトリをコンテクストして使用し、 stdin を通した Dockerfile でイメージを構築
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt .
RUN cat /somefile.txt
EOF


stdin からの Dockerfile を使い、リモートのビルド・コンテクストでイメージを構築

以下の構文を使えば、リモートの git リポジトリからのファイルを使ってイメージを構築しますが、 stdin からの Dockerfile を使います。この構文では stdin から Dockerfile を読み込むファイルを Docker に命令するため、 -f (又は --file )オプションで使用する Dockerfile を指定します。

docker build [OPTIONS] -f- PATH

この構文が役立つ状況は、リポジトリにあるイメージからビルドしたいものの Dockerfile を含んでいない場合や、調整した Dockerfile で構築したいものの、自分のリポジトリでフォークし続けたくない場合です。

以下の例では stdin からの Dockerfile を用いてイメージを構築しますが、 GitHub 上の "hello-world" Git リポジトリ にある README.md を追加します。

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

FROM busybox
COPY README.md .
EOF

内部の処理

リモートの Git リポジトリをビルド・コンテクストとして用いてイメージを作る場合、Docker はローカルマシン上でリポジトリの git clone を実行し、得られたファイルをビルド・コンテクストとしてデーモンに送信します。この機能には docker build コマンドを実行するホスト上に、 git のインストールが必要です。


.dockerignore で除外

構築に関係ないファイルを(ソース・リポジトリに手を加えることなく)除外するには、 .dockerignore ファイルを使います。このファイルは .gitignore ファイルに近い除外パターンをサポートします。ファイルの作成に関する情報は .dockerignore ファイル(英語) をご覧ください。


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

マルチ・ステージ・ビルド(multi-stage build)により、中間レイヤやファイルを必死に減らそうとしなくても、最終的なイメージ容量を劇的に減らせます。

構築手順(プロセス)の最終段階(ステージ)でイメージをビルドするため、ビルド・キャッシュの効果によってイメージ・レイヤを最小化できます。

例えば、複数のレイヤを含む構築時に、(ビルド・キャッシュの再利用を確実に行うため)頻繁に変更していないものから、頻繁に変更しているものへと、順番を並び替えられます。


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

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

  • アプリケーションの生成

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 image)を作成し、ビルド容量(サイズ)の増加はありません。

  • 可能であれば マルチ・ステージ・ビルド を使い、最終イメージの中に必要な成果物のみコピーします。これにより、最終イメージの容量は増えずに、中間構築ステージにツールやデバッグ情報を入れられるようになります。


複数行引数の順番

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

以下は 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 の命令と、子イメージの1つとの単純な比較で十分です。しかし命令によっては、更なる検査や調査が必要になります。


  • ADD および COPY 命令では、イメージ内でのファイル内容を検査し、各ファイルのチェックサムを計算します。ファイルの最終編集時間、最終アクセス時間は、チェックサムに影響ありません。キャッシュを探し、既存イメージのチェックサムと比較します。もしファイル内でコンテンツやメタデータの何かが変わっていれば、キャッシュは無効になります。


  • ADDCOPY 命令に限らず、キャッシュの確認では、キャッシュが一致するかどうか決めるために、コンテナ内のファイルに対する確認は行いません。例えば、 RUN apt-get -y update というコマンドを処理する場合、キャッシュが存在しているかどうかを判断するために、コンテナ内に更新されたファイルがあるかどうかを調査しません。キャッシュの有無は、コマンドの文字列そのものが一致するかどうかしか見ていません。



Dockerfile 命令

効率的かつ保守性のある Dockerfile を作成するためには、以下のアドバイスが設計に役立つでしょう。


FROM

Dockerfile リファレンスの FROM 命令(英語)

可能であれば、自分のイメージの基礎として、現在の公式イメージを使います。私たちが推奨するのは Alpine イメージ です。完全な Linux ディストリビューションでありながら、厳密に管理され、容量は小さいもの(現時点で 5MB 以下)です。


LABEL

オブジェクト・ラベルの理解(英語)

イメージにはラベルを追加できるため、プロジェクトでイメージを整理するために役立つでしょう。ライセンス情報の記録や、自動化のためや、その他の理由によります。各ラベルには、行の冒頭に LABEL の追加と、1つ又は複数のキー・バリューの組み(ペア)になります。以下の例では、異なるフォーマットを許容しているのを表しています。説明のためのコメントを、インラインに含めています。


スペース(空白)を含む文字列は必ずクォートで囲むか、 又は 、スペースをエスケープする必要があります。また、内部でのクォート文字列( '"' )もエスケープする必要があります。


# 1つ又は複数のラベルを各々に指定

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 命令(英語)

Dockerfile をより読みやすく、理解しやすく、保守しやすくするには、 RUN 命令の長い行をバックラッシュで複数行に分割して記述します。


apt-get

おそらく RUN 命令で最も一般的な利用例とは、 apt-get の利用でしょう。これはパッケージをインストールするためであり、 RUN apt-get コマンドには気を付けるべき注意点がいくつかあります。

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

RUN apt-get updateapt-get install は、同じ RUN 命令文の中で常に連結されます。以下は実行例です。

RUN apt-get update && apt-get install -y \

package-bar \
package-baz \
package-foo

イメージの構築後、全てのレイヤは Docker キャッシュ内にあります。次に、 apt-get install を編集し、他のパッケージを追加するものと仮定します。

FROM ubuntu:18.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 を用いることで、記述の追加や手作業を行わなくても、最も新しいパッケージ・バージョンを利用できます。これは「cache busting」(キャッシュ破壊)として知られているテクニックです。また、パッケージのバージョンを指定してもキャッシュ破壊ができます。バージョン固定を考えている場合は、次の例のようにします。

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 のキャッシュ破壊によって新しいものが指定されるため、確実に新しいバージョンをインストールします。また、それぞれの行に書いたパッケージの一覧は、パッケージの重複ミスを避けるためです。

付け加えると、 /var/lib/apt/lists を削除して apt キャッシュを処分しておくと、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 インタプリタを使いますので、成功したかどうかの判断には、パイプによって最後に処理した終了コードのみを評価します。先ほどの例では、たとえ wget コマンドに失敗したとしても、 wc -l コマンドが成功するため、新しいイメージをもたらします。

パイプ中のあらゆる段階でエラーがあれば失敗とするには、 set -o pipepail && を先頭に追加して、予期しないエラーが発生しても、不本意な構築に成功しないようにできます。以下は記述例です。

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


** -e pipefail オプションは全てのシェルでサポートされていません**

Debian をベースとするイメージ上の dash シェルのような場合では、 pipefail オプションをサポートしないため、 RUN 命令で exec 形式を用いたシェルの明示を検討します。以下は記述例です。

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"] として実行できるでしょう。実際に、あらゆるサービスのベースとなるイメージでは、この形式での命令が推奨されています。

大部分の他のケースでは、bash、python、perl のように、 CMD で対話型シェルを使うために使われます。例えば、 CMD ["perl", "-de0"]CMD ["python"]CMD ["php", "-a"] です。 docker run -it python のような形式で実行すると、利用可能なシェル上に落とし込み、すぐに利用できるようになるのを意味します。なお CMD 命令を使うにあたり、あなたや想定している利用者が ENTRYPOINT の挙動に慣れていない段階では、 CMD ["パラメータ", "パラメータ"]ENTRYPOINT とを一緒に使うべきではないでしょう。


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:$PATH の指定により、 CMD ["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 命令を使うだけで、コンテナ内のソフトウェアのバージョンを、魔法のように自動で上げることもできます。

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

ADDCOPY は機能が似ていますが、一般的には COPY が望ましいと言われています。その理由は ADD に比べて透明性があるからです。 COPY がサポートするのは、ローカルのファイルをコンテナ内にコピーするという、基本的な機能のみです。一方の ADD は複数の機能(例えばローカル上のみの tar 展開や、リモートの URL サポート)を持ちますが、一見しては分かりません。したがって、 ADD のベストな使い方はローカルの tar ファイルをイメージの中へ自動展開することです。そのためには ADD rootfs.tar.gz / . のようにします。

処理内容によっては、一度にすべてのファイルを取り込むよりも、 Dockerfile の複数のステップで個々に COPY を指定し、別々のファイルを読み込めます。これにより、何らかのファイル変更が発生しても、各ステップのうち、対象となった構築キャッシュのみが無効化されます(そのステップの再実行を強制します。)。

実行例:

COPY requirements.txt /tmp/

RUN pip install --requirement /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 ではデフォルトのフラグを指定します )。

例として、コマンドライン・ツールの s3md イメージを見てみましょう。

ENTRYPOINT ["s3cmd"]

CMD ["--help"]

それから、次のようにコマンドを実行すると、コマンドのヘルプを表示します。

あるいは、適切なパラメータを使ったコマンド実行もできます。

$ 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 としてアプリケーションを設定

このスクリプトでは exec Bash コマンド を、最終的にコンテナにおける PID 1 のアプリケーションとして実行します。そのため、コンテナに対して送信される Unix シグナルは、アプリケーションが受信できます。詳細は ENTRYPOINT リファレンス をご覧ください。


ヘルパー・スクリプトをコンテナの中にコピーし、コンテナの実行時に ENTRYPOINT で実行します。

COPY ./docker-entrypoint.sh /

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

このスクリプトによって、利用者はいくつかの方法で Postgres とやりとりできます。

単純に Postgres を起動します。

$ docker run postgres

あるいは、Postgres 実行時、サーバに対してパラメータを渡せます。

$ 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 --no-log-init -r -g postgres postgres のようにします。


UID/GID 明示の検討

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

Go アーカイブと tar パッケージにおける、sparse ファイルの扱いに関する 未解決なバグ(英語) の影響により、Docker コンテナ内部で極めて大きな UID を持つユーザの作成を試みると、コンテナ・レイヤ内の /var/log/faiilog ファイルが NULL (\0) 文字列で溢れかえり、肥大化を引き起こします。回避策としては useradd に --no-log-init フラグを追加します。Debian/Ubuntu の adduser ラッパーは、このフラグをサポートしません。


TTY やシグナル転送を使わないつもりであれば、 sudo のインストールも避けた方が良いでしょう。使うことで引き起こされる問題の解決が大変なためです。もし、例えば root としてデーモンの初期化をするものの、実行は root 以外で行いたい場合、どうしても sudo のような機能が必要であれば、「"gosu"」 の使用をご検討ください。


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-onbuildruby:2.0-onbuild

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


公式イメージの例

これらの公式イメージには、模範的な Dockerfile があります。


原文