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

# 概要

[Docker Documentation](https://docs.docker.com/) にある、[Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) の参考日本語訳です。ドキュメントは、2019年5月31日現在のカレントである `Docker v18.09 (current)` です。

背景 ―― [以前の翻訳](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)から時間が経過し、全体的に手直ししたいものの、差分が大きすぎる状況です。そのため、リファレンスや重要性の高いものから優先的に着手することにしました。

## スライド資料

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


* [Dockerfile を書くためのベストプラクティス解説編](https://www.slideshare.net/zembutsu/explaining-best-practices-for-writing-dockerfiles)

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

* [Dockerfileを改善するためのBest Practice 2019年版](
https://www.slideshare.net/zembutsu/dockerfile-bestpractices-19-and-advice)


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

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

Docker は `Dockerfile` の命令を読み込み、自動的にイメージを構築（ビルド）します。 `Dockerfile` とはテキストファイルであり、あるイメージの構築に必要な、全ての命令を含みます。 `Dockerfile`  は [Dockerfile リファレンス(英語)](https://docs.docker.com/engine/reference/builder/) にある固有のフォーマットと命令群に従います。

Docker イメージは読み込み専用（read-only）のレイヤ群で構成され、各レイヤが Dockerfile 命令に相当します。それぞれのレイヤは積み重なっており、それぞれのレイヤは、以前に存在していたレイヤの変更差分です。次の `Dockerfile` を考えてみます：

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

それぞれの命令ごとに、１つのレイヤを作成します：

- `FROM` は `ubuntu:18.04`  Docker イメージからレイヤを作成します。
- `COPY` は Docker クライアントの現ディレクトリから、ファイルをコピーします。
- `RUN` はアプリケーションを `make` で構築します。
- `CMD` はコンテナ内で実行するコマンドを指定します。

イメージを実行してコンテナの生成時、基礎となるレイヤ上に新しい書き込み可能なレイヤ（ *writable layer*  ) 、（これが、コンテナ・レイヤ "*container layer*" です）を追加します。実行中のコンテナに対するすべての変更、例えば新しいファイルの書き込み、既存ファイルの変更、ファイルの削除といったものは、すべてこの書き込み可能な薄いコンテナ・レイヤに対して書き込まれます。

イメージ・レイヤ（と Docker がイメージをどのように構築および保存するか）については [ストレージ・ドライバについて（英語）](https://docs.docker.com/storage/storagedriver/) をご覧ください。

## 一般的なガイドラインと助言（アドバイス）

### エフェメラル（一時的）なコンテナを作成

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

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

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

`docker build` コマンドを実行時、現在の作業ディレクトリを *ビルド・コンテクスト（build context）* と呼びます。デフォルトでは、そこに Dockerfile があると見なされますが、フラグ（ `-f` ）で別の場所も指定できます。 `Dockerfile` が実際にどこにあるかに関係なく、現在のディレクトリ階層下にある全てのファイルおよびディレクトリが、ビルド・コンテクストとして Docker デーモンに対して送られます。

>  **ビルド・コンテクストの例**
> 
> ビルド・コンテクストのためにディレクトリを作成し、 `cd` で中に入ります。 `hello` という名前のテキストファイルに「hello」と書き、 `cat` の実行によって Dockerfile を作成します。ビルド・コンテクスト（ `.` ）内からイメージを構築します。
> 
> ```sh
> mkdir myproject && cd myproject
> echo "hello" > hello
> echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
> docker build -t helloapp:v1 .
> ```
> 
> `Dockerfile` と `hello` を別々のディレクトリに移動し、イメージの２番目のバージョンを構築します（先ほど構築したキャッシュに依存しません）。 `-f` を使い Dockerfile の場所を示し、ビルド・コンテクストのディレクトリを指定します。
> 
> ```sh
> 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` にパイプしてイメージを構築できます。 `stdin`  で `Dockerfile` にパイプすると便利なのは、Dockerfile をディスクに書く必要がない１度限りの処理や、 `Dockerfile` を生成しても、以後に使用しない場合です。
> 
> **このセクションの例では簡単にするためにヒア・ドキュメントを使っていますが、 `stdin` 上で `Dockerfile` を提供するあらゆる方法が使えます。**
> 
> 例えば、以下のコマンドは処理内容が同じです。
> 
> ```sh
> echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
> ```
> 
> ```sh
> docker build -<<EOF
> FROM busybox
> RUN echo "hello world"
> EOF
> ```
> 
> この例は、自分の好きな手法、あるいは利用例に最も適した手法に書き換えできます。

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

以下の構文を使えば、`stdin` からの `Dockerfile` を使ってイメージを構築するため、ビルド・コンテクストに対して余分なファイルを追加で送信しません。 `PATH` の位置にハイフン（ `-` ）を置くと、 ディレクトリではなく、（ `Dockerfile` だけを含む ） `stdin` からビルド・コンテクストを読むように、 Docker へ指示します。

```sh
docker build [OPTIONS] -
```

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

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

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

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

**メモ： `COPY` や `ADD` を用いる Dockerfile でビルドを試みる場合、この構文を用いると失敗します。以下の例で説明します。**
> 
> ```sh
> # 作業するディレクトリを作成
> 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` を指定します。

```sh
docker build [OPTIONS] -f- PATH
```

以下の例では現在のディレクトリ（ `.` ）をビルド・コンテクストとして用い、イメージの構築に用いる `Dockerfile`  は [ヒア・ドキュメント](http://tldp.org/LDP/abs/html/here-docs.html) を用いた `stdin` を通しています。

```sh
# 作業するディレクトリを作成
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` を指定します。

```sh
docker build [OPTIONS] -f- PATH
```

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

以下の例では `stdin` からの `Dockerfile` を用いてイメージを構築しますが、 [GitHub 上の "hello-world" Git リポジトリ](https://github.com/docker-library/hello-world) にある `README.md` を追加します。

```sh
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 ファイル（英語）](https://docs.docker.com/engine/reference/builder/#dockerignore-file) をご覧ください。

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

マルチ・ステージ・ビルド（[multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/)）により、中間レイヤやファイルを必死に減らそうとしなくても、最終的なイメージ容量を劇的に減らせます。

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

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

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

Go アプリケーションに対する Dockerfile は、このようになるでしょう。

```docker
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

# 最終的に１つのイメージ・レイヤになる
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
```

### 不要なパッケージをインストールしない

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

### アプリケーションの分離

それぞれのコンテナの用途は、１つだけにすべきです。アプリケーションを複数のコンテナに分離すると、水平スケールやコンテナの再利用を行いやすくします。例えば、ウェブ・アプリケーションのスタックであれば、ウェブ・アプリケーション、データベース、インメモリのキャッシュを分離した状態で管理するために、それぞれが自身のユニークなイメージを持つ、３つのコンテナで構成されるでしょう。

各コンテナでプロセスを１つにするのは良い経験則ですが、厳密なルールではありません。例えば、コンテナは [init プロセス](https://docs.docker.com/engine/reference/run/#specify-an-init-process) を生成するだけでなく、プログラムによっては、自身の必要性に応じて追加プロセスも生成します。例えば、 [Celery](http://www.celeryproject.org/) は複数のワーカ・プロセスを生成できますし、 [Apache](https://httpd.apache.org/) はリクエストごとに１つのプロセスを生成します。

コンテナに対しては、可能な限りクリーンかつモジュール型を保つように、ベストな判断をしてください。もしコンテナが相互依存する場合は、 [Docker コンテナ・ネットワーク（英語）](https://docs.docker.com/engine/userguide/networking/)  を使い、コンテナが確実に通信できるようにします。

### レイヤ数の最小化

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

- `RUN`、 `COPY` 、 `ADD` 命令のみレイヤを作成します。他の命令では一時的な中間イメージ（temporary intermediate image）を作成し、ビルド容量（サイズ）の増加はありません。
- 可能であれば [マルチ・ステージ・ビルド](https://docs.docker.com/develop/develop-images/multistage-build/) を使い、最終イメージの中に必要な成果物のみコピーします。これにより、最終イメージの容量は増えずに、中間構築ステージにツールやデバッグ情報を入れられるようになります。

### 複数行引数の順番

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

以下は `buildpack-deps` [イメージ](https://github.com/docker-library/buildpack-deps) の記述例です。

```docker
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion
```

### 構築キャッシュの活用

イメージの構築時、 Docker は `Dockerfile` の命令を順番に処理し、それぞれで指定した命令を実行します。Docker は各命令で既存のイメージにキャッシュがあるかどうか検査します。もしキャッシュあれば、新しい（重複する）イメージを作成するのではなく、再利用します。

もしもキャッシュを一切使わないのであれば、 `docker build` コマンドで `--no-cache=true` オプションが使えます。しかしながら、Docker でキャッシュを使うのであれば、一致するイメージが見つかった場合、キャッシュができる時とできない時の理解が重要です。基本的なルールとして、Docker は以下の概要に従います。

- 開始時に親イメージを既にキャッシュしている場合は、次の命令と、ベース・イメージから派生した全ての子イメージとを比較し、全く同じ命令をつかって構築しているものがないかどうか調べます。もしも一致するものがなければ、キャッシュを無効化します。

- ほとんどの場合、 `Dockerfile` の命令と、子イメージの１つとの単純な比較で十分です。しかし命令によっては、更なる検査や調査が必要になります。

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

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

## Dockerfile 命令

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

### FROM

[Dockerfile リファレンスの FROM 命令（英語）](https://docs.docker.com/engine/reference/builder/#from) 

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

### LABEL

[オブジェクト・ラベルの理解（英語）](https://docs.docker.com/config/labels-custom-metadata/)

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

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

```docker
# １つ又は複数のラベルを各々に指定
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 より前のバージョンは、余分なレイヤを作成するのを防ぐため、全てのラベルを１つの `LABEL` 命令にするのが推奨されていました。これはもう必要ありませんが、今もラベルの連結をサポートしています。

```docker
# １行に複数のラベルを指定
LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
```

また、上の記述は、以下のようにも記述できます。

```docker
# 一度で複数のラベルを設定するには、長い行を行連結文字列で分割します。
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"
```

ラベルとして利用可能なキーおよびバリューについてのガイドラインは、 [オブジェクト・ラベルの理解（英語）](https://docs.docker.com/config/labels-custom-metadata/) をご覧ください。ラベルを確認するための情報については、  [オブジェクト上のラベルを管理（英語）](https://docs.docker.com/config/labels-custom-metadata/#managing-labels-on-objects)  にある関連情報をご覧ください。また、Dockerfile リファレンスの [LABEL（英語）](https://docs.docker.com/engine/reference/builder/#label) もご覧ください。

### RUN

[Dockerfile リファレンスの RUN 命令（英語）](https://docs.docker.com/engine/reference/builder/#run)

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

#### apt-get

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

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

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

```docker
RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo
```

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

```docker
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl nginx
```

Docker は冒頭からファイルを読み込み、命令が同一であると認識すると、以前のステップからのキャッシュを再利用します。その結果、 `apt-get update` は一切実行されません。なぜならば、構築時にキャッシュした（古い）バージョンが使われてしまう可能性があるためです。 `apt-get update` が実行されなければ、時代遅れの `curl` と `nginx` パッケージで構築してしまう可能性があります。

Dockerfile では `RUN apt-get update && apt-get install -y` を用いることで、記述の追加や手作業を行わなくても、最も新しいパッケージ・バージョンを利用できます。これは「cache busting」（キャッシュ破壊）として知られているテクニックです。また、パッケージのバージョンを指定してもキャッシュ破壊ができます。バージョン固定を考えている場合は、次の例のようにします。

```docker
RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    package-foo=1.3.*
```

バージョンを固定しておくと、キャッシュの中に何が入っているかどうかを気にせず、特定のバージョンを取得しての構築を強制します。また、必要なパッケージの予期しない変更に㑦より引き起こされる障害を、このテクニックによって減らします。

以下は熟慮した `RUN` 命令であり、推奨する `apt-get` すべての利用方法を示します。

```docker
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`  を [自動的に実行（英語）](https://github.com/moby/moby/blob/03e2923e42446dbb830c654d0eec323a0b4ef02a/contrib/mkimage/debootstrap#L82-L105) しますので、明示する必要はありません。 

#### パイプを使う

次の例のように、いくつかの `RUN` コマンドはパイプ文字列（ `|` ）を用いて、他のコマンド出力をパイプして利用できます。

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

Docker はこれらのコマンドに `/bin/sh -c` インタプリタを使いますので、成功したかどうかの判断には、パイプによって最後に処理した終了コードのみを評価します。先ほどの例では、たとえ `wget`  コマンドに失敗したとしても、 `wc -l` コマンドが成功するため、新しいイメージをもたらします。

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

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

> ** `-e pipefail` オプションは全てのシェルでサポートされていません** 
> 
> Debian をベースとするイメージ上の `dash` シェルのような場合では、 `pipefail` オプションをサポートしないため、 `RUN` 命令で *exec*  形式を用いたシェルの明示を検討します。以下は記述例です。
> 
> ```docker
> RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
> ```

### CMD

[Dockerfile リファレンスの CMD 命令（英語）](https://docs.docker.com/engine/reference/builder/#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 命令（英語）](https://docs.docker.com/engine/reference/builder/#expose)

`EXPOSE` 命令は、コンテナに接続するためにリッスンするポートを指定します。そのため、アプリケーションが一般的に使うポート番号か、アプリケーションが慣例として使うポート番号を指定すべきです。例えば、 Apache ウェブサーバを含むイメージであれば `EXPOSE 80` を使うべきですし、MongoDB を含むイメージであれば `EXPOSE 27017`  を使う等です。

外部からの接続のためには、利用者が `docker run` の実行時にフラグを指定し、特定のポートを任意のポートに割り当てる（マッピングする）かを明示します。コンテナのリンク機能を使うと、Docker はコンテナがソースに辿り着けるよう、環境変数を提供します（例： `MYSQL_PORT_3306_TCP` ）。

### ENV

[Dockerfile リファレンスの ENV 命令（英語）](https://docs.docker.com/engine/reference/builder/#env)

新しいソフトウェアを簡単に実行できるようにするため、コンテナにインストールされているソフトウェアの `PATH` 環境変数を `ENV` で書き換えられます。例えば、 `ENV PATH /usr/local/nginx/bin:$PATH` の指定により、 `CMD ["nginx"]` だけで動作するようにします。

また、 `ENV`命令は任意のコンテナ化したサービスに対し、実行時に必要となる環境変数を与えるためにも役立ちます。例えば、PostgreSQL の `PGDATA` です。

あとは、 `ENV` を一般的に用いられるバージョン番号の指定にも使えますので、次のように、バージョン特定した保守を行いやすくします。

```docker
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 を作成し、構築してください。

```docker
FROM alpine
ENV ADMIN_USER="mark"
RUN echo $ADMIN_USER > ./mark
RUN unset ADMIN_USER
```

```console
$ docker run --rm test sh -c 'echo $ADMIN_USER'

mark
```

これを避けて、かつ本当に環境変数をアンセットしたい場合は、 `RUN` 命令でシェル・コマンドを１つのレイヤとして使います。ここでセットして使っても、環境変数すべてをアンセットします。コマンドは `;` や `&&`  で分割できます。後者を使う場合、コマンドに１つでも失敗すると、 `docker build` また失敗します。通常はこれが良い方法です。行を連結する文字列として `\`  を使うと、 Linux Dockerfile は可読性が向上します。また、全てのコマンドをシェルスクリプトとして記述しておき、 `RUN` 命令でそのシェルスクリプトを実行する方法もとれます。

```docker
FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh
```

```console
$ docker run --rm test sh -c 'echo $ADMIN_USER'
```

### ADD 又は COPY

- [Dockerfile リファレンスの ADD 命令（英語）](https://docs.docker.com/engine/reference/builder/#add)
- [Dockerfile リファレンスの COPY 命令（英語）](https://docs.docker.com/engine/reference/builder/#copy)

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

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

実行例：

```docker
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/
```

`RUN` 命令を `COPY . /tmp/` よりも前に置けば、結果的にキャッシュ無効化の影響が小さくなります。

イメージ容量の問題があるので、 `ADD` を使ってリモート URL 上のパッケージを取得するのは、絶対に避けてください。そのかわりに、 `curl` や `wget` を使うべきです。この方法であれば、展開後に不要になったファイルを削除できますし、また、イメージに不要なレイヤの追加も避けられます。例えば、次のような記述を避けるべきです。

```docker
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
```

その代わり、次のように記述します。

```docker
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 命令（英語）](https://docs.docker.com/engine/reference/builder/#entrypoint)

`ENTRYPOINT` に対するベストな使い方は、イメージにおいてメインとなるコマンドの指定です。この指定によって、イメージの実行を、まるでコマンドを実行するかのように扱えます（そして、 `CMD` ではデフォルトのフラグを指定します ）。

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

```docker
ENTRYPOINT ["s3cmd"]
CMD ["--help"]
```

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

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

```console
$ docker run s3cmd ls s3://mybucket
```

イメージ名が、先ほどの命令で指定したバイナリ名と重複しているため、扱いやすくなっています。

また、 `ENTRYPOINT` は役に立つスクリプトの組み合わせにも利用できます。そのため、利用するには複数のステップが必要となるかもしれないツールの場合も、先ほどのコマンドと似たような方法が使えます。

例えば、 [Postgres 公式イメージ](https://hub.docker.com/_/postgres/) では、 `ENTRYPOINT`  として以下のスクリプトを使います。

```sh
#!/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 コマンド](http://wiki.bash-hackers.org/commands/builtin/exec) を、最終的にコンテナにおける PID 1 のアプリケーションとして実行します。そのため、コンテナに対して送信される Unix シグナルは、アプリケーションが受信できます。詳細は [`ENTRYPOINT` リファレンス](https://docs.docker.com/engine/reference/builder/#entrypoint) をご覧ください。

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

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

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

単純に Postgres を起動します。

```console
$ docker run postgres
```

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

```console
$ docker run postgres postgres --help
```

又は、Bash のような全く違うツールの実行もできます。

```console
$ docker run --rm -it postgres bash
```

### VOLUME

[Dockerfile リファレンスの VOLUME 命令（英語）](https://docs.docker.com/engine/reference/builder/#volume)

`VOLUME` 命令はデータベース・ストレージ領域、設定用ストレージ、Docker コンテナによって作成されるファイルやフォルダの公開のために使います。イメージにおける変わりやすい場所や、利用者によって便利な場所として `VOLUME` の利用が強く推奨されます。

### USER

[Dockerfile リファレンスの USER 命令（英語）](https://docs.docker.com/engine/reference/builder/#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 ファイルの扱いに関する [未解決なバグ（英語）](https://github.com/golang/go/issues/13548) の影響により、Docker コンテナ内部で極めて大きな UID を持つユーザの作成を試みると、コンテナ・レイヤ内の `/var/log/faiilog` ファイルが NULL (\0) 文字列で溢れかえり、肥大化を引き起こします。回避策としては useradd に `--no-log-init` フラグを追加します。Debian/Ubuntu の `adduser` ラッパーは、このフラグをサポートしません。

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

### WORKDIR

[Dockerfile リファレンスの WORKDIR 命令（英語）](https://docs.docker.com/engine/reference/builder/#workdir)

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

### ONBUILD

[Dockerfile リファレンスの ONBUILD 命令（英語）](https://docs.docker.com/engine/reference/builder/#onbuild)

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

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

`ONBUILD` は `FROM` で指定したイメージを作ったあと、さらにイメージを作るのに便利です。例えば、言語スタック・イメージで `ONBUILD` を使うと、 `Dockerfile` 内のソフトウェアは特定の言語環境を使えるようになります。これは [Ruby の `ONBUILD` ](https://github.com/docker-library/ruby/blob/master/2.4/jessie/onbuild/Dockerfile) でも 見られます 。

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

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

## 公式イメージの例

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

- [Go](https://hub.docker.com/_/golang/)
- [Perl](https://hub.docker.com/_/perl/)
- [Hy](https://hub.docker.com/_/hylang/)
- [Ruby](https://hub.docker.com/_/ruby/)


## 原文

- Best practices for writing Dockerfiles | Docker Documentation
  - https://docs.docker.com/develop/develop-images/dockerfile_best-practices/