Dockerfileを書く際のベストプラクティスを学ぶ

## はじめに
アプリケーションを作成していく上で、dockerを用いることは多いと思います。
今回はそんなdockerを使用していく上でも`Dockerfile`がどう書かれているべきかを勉強するために[Docker Documentation](https://docs.docker.com/)の[Best practice for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)を自分なりに和訳しつつ記載してみました。
よければ参考にしてもらえたらと思います。
(Dockerfile instructions以下は時間のある時に追記します、、)

# Dockerfileを作成するためのベストプラクティス
Dockerは特定のイメージをビルドするために必要な全てのコマンドを順番に記述されたテキストファイル(Dockerfile)からの指示を読み取ることによりイメージを自動的にビルドします。
Dockerイメージは、それぞれがDockerfile命令を表す読み取りレイヤーで構成されます。レイヤーは積み重ねられ、各レイヤーは前のレイヤーからの差分となリます。

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

各命令は1つのレイヤーを作成します。

- `FROM` : `ubuntu:18.04`のDockerイメージからレイヤーを作成します
- `COPY` : Dockerクライアントの現在のディレクトリからファイルを追加します  
          （COPY .(クライアントのディレクトリ) /app(Dockerイメージ内のディレクトリ)）
- `RUN` : `make`コマンドでアプリケーションをビルドします
- `CMD` : コンテナ内で実行するコマンを指定します

イメージを実行してコンテナを生成するときは、下にあるレイヤーの上に新しい書き込み可能なレイヤー(コンテナレイヤー)を追加します。新しいファイルの書き込み、既存のファイルの変更、ファイルの削除など、実行中のコンテナに加えられた全ての変更はこの書き込み可能なコンテナ層に書き込まれます。  
  
イメージレイヤー(およびDockerがイメージを構築、保存する方法)については[ストレージドライバーについて](https://docs.docker.com/storage/storagedriver/)を参照してください。

# 一般的なガイドラインと推奨事項
##  一時的なコンテナ作成
Dockerfileによって定義されたイメージは、`可能な限り一時的なコンテナ(ephemeral)`として生成される必要がある。ephemeral(エフェメラル)とはコンテナを停止して破棄し、再構築して、最小限のセットアップと構成に置き換えることができることを意味しています。  
[The Twelve-factor Appの方法論](https://12factor.net/processes)を参照することで、このようなステートレスなコンテナを動かすことの動機付けを感じることができます。

## build context(ビルドコンテキスト)を理解する
`docker build`コマンドを発行すると、コマンドを実行した作業ディレクトリは*ビルドコンテキスト*と呼ばれます。デフォルトの設定ではDockerfileはこのビルドコンテキスト内にあると認識するようになってますが、`-f`のフラグを用いることで別の場所を指定することもできます。
例：

```bash
docker build -f ./Dockerfile.test
```

実際にDockerfileがどこに存在するかに関わらず、現在ディレクトリ内のファイルとディレクトリに全ての再起的な内容は、ビルドコンテキストとしてDockerデーモンに送信されます。  
> ## コンテキストの例を作成する
>ビルドコンテキスト用のディレクトリを作成し、その中に`cd`コマンドで入ります。`hello`と名付けたテキストファイル内に`hello`と書き込み、そのファイルを`cat`した内容でDockerfileを作成します。そしてビルドコンテキスト内(.)からイメージのビルドを行います。
>
> ```bash
>  $ mkdir myproject && cd myproject
>  $ echo "hello" > hello
>  $ echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
>  $ docker build -t helloapp:v1 .
> ```
>
>`Dockerfile`と`hello`のそれぞれのファイルをdockerfilesとcontextの2つのディレクトリに移動し、2つ目のバージョンのイメージをビルドします(最後のビルドのキャッシュには依存しません)。`-f`のオプションを使用してDockerfileの場所を指定し、ビルドコンテキストの場所を指定します。
>
> ```bash
> $ mkdir -p dockerfiles context
> $ mv Dockerfile dockerfiles && mv hello context
> $ docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context
> ```  
>
上記の例のようにビルドに必要のないファイルを誤って含めてしまうと、ビルドコンテキストが大きくなってしまい、イメージサイズが大きくなります。これにより、イメージをビルドする時間、イメージをプルおよびプッシュする時間、コンテナのランタイムサイズが増加する可能性があります。ビルドコンテキストの大きさを確認するにはビルド時に以下のようなメッセージを探してください。

```bash
Sending build context to Docker daemon  187.8MB
```

## 標準入力とパイプ(`|`)を用いてDockerfileを構築する。
DockerではstdinをパイプすることでDockerfileを構築することができます。
例：

```bash
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
```
これを用いることでDockerfile自体を作成する(ディスクに書き込む)ことなくイメージを作成することができ、さらに一回きりのイメージ作成を実現できます。ただしこれは永続化するべきではありません。
> ## このセクションの例では、便宜上[このドキュメント](https://tldp.org/LDP/abs/html/here-docs.html)を使用していますが、`stdin`上で`Dockerfile`を提供するあらゆる方法を可能にします。
> 例えば、以下の２つのコマンドは同等の意味になります。
>
> ```bash
> echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
> ```
>
>```
> docker build -<<EOF
> FROM busybox
> RUN echo "hello world"
> EOF
>```
>上記の例は、好みのユースケースまたはアプローチに置き換えることができます。

### ビルドコンテキストを送信せずに、stdinからDockerfileを使用してイメージを構築する
以下の構文を使用することで`stdin`から`Dockerfile`を使用したイメージのビルドが実現できるため、不必要なファイルをビルドコンテキストに送信することがありません。
本来ビルドコンテキストを指定するパスが書かれる場所にハイフン(-)を使用すると、ディレクトリの代わりに`stdin`からビルドコンテキスト(含まれるのはDockerファイルのみ)を読み取るようにDockerに指示します。

```bash
docker build [OPTIONS] -
```

次の例では`stdin`から`Dockerfile`を使用してイメージをビルドします。ビルドコンテキストからDockerデーモンにファイルは送信されません。  

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

```
ビルドコンテキストを省略することでDockerfileをイメージにコピーする必要がない場合に役立ち、Dockerデーモンにファイルが送信されないためビルド速度が向上します。  
ビルドコンテキストから一部のファイルを除外してビルド速度を向上させたい場合は、[.dockerignoroで除外](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#exclude-with-dockerignore)を確認してください。

> ### `COPY`や`ADD`を使用するDockerfileでビルドを試みる場合、この構文を使用すると失敗します。以下の例でそれを示します。
>
> ```bash
> # create a directory to work in
> mkdir example
> cd example
> 
> # create an example file
> touch somefile.txt
> 
> docker build -t myimage:latest -<<EOF
> FROM busybox
> COPY somefile.txt ./
> RUN cat /somefile.txt
> EOF
> 
> # observe that the build fails
> ...
> 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`を用いたものです。この構文はDockerfileを特定するために`-f`もしくは`--file`オプションを使用し、`-`(ハイフン)を使用して`stdin`から`Dockerfile`を読み込ませるようにDockerに指示しています。

```bash
docker build [OPTIONS] -f- PATH
```
以下の例では、現在ディレクトリ(.)をビルドコンテキストとして使用し、イメージをビルドする`Dockerfile`は[このドキュメント](https://tldp.org/LDP/abs/html/here-docs.html)を用いた`stdin`を使っています。

```bash
# create a directory to work in
mkdir example
cd example

# create an example file
touch somefile.txt

# build an image using the current directory as context, and a Dockerfile passed through stdin
docker build -t myimage:latest -f- . <<EOF
FROM busybox
COPY somefile.txt ./
RUN cat /somefile.txt
EOF
```

### stdinでDockerfileを使用して、リモートののビルドコンテキストから構築する
以下の構文を使用することで`stdin`で`Dockerfile`を用いて、リモートの`git`リポジトリからのファイルを使用したイメージのビルドができます。この構文はDockerfileを特定するために`-f`もしくは`--file`オプションを使用し、`-`(ハイフン)を使用して`stdin`から`Dockerfile`を読み込ませるようにDockerに指示しています。

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

上記の構文は`Dockerfile`を含まないリポジトリからイメージをビルドする、または独自のフォークしたリポジトリのメンテナンスをしないでカスタムなDockerfileでビルドするという状況で役に立ちます。  
以下の例では`stdin`から`Dockerfile`を用いてビルドを行い、["hello-world" Git repository on GitHub](https://github.com/docker-library/hello-world)から`hello.c`ファイルを追加します。

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

> ### memo
>リモートのGitリポジトリをビルドコンテキストとして使用してイメージをビルドする場合、Dockerはローカルマシン上で`git clone`を実行し、その結果ビルドコンテキストとしてファイルをデーモンに送信します。この機能は`docker build`コマンドを実行するホスト上に`git`がインストールされている必要があります。

## .dockerignoreで除外する
ビルドに関係ないファイルを(ソースリポジトリを構築せずに)除外するには、`.dockerignore`ファイルを使用します。このファイルは`.gitignore`ファイルと同様の除外パターンをサポートしています。作成時の詳細については[こちら](https://docs.docker.com/engine/reference/builder/#dockerignore-file)を確認してください。

## マルチステージビルドを使用する
[マルチステージビルド](https://docs.docker.com/develop/develop-images/multistage-build/)を使用すると、中間レイヤーとファイル数を減らすことに力を入れなくても最終的なイメージサイズを大幅に減らことができます。  
イメージはビルドプロセスの最終段階でビルドされるため、[ビルドキャッシュ](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache)を利用してイメージレイヤーを最小限に抑えることができます。  
例えば、ビルドに複数のレイヤーが含まれている場合、変更頻度の低いものから(ビルドキャッシュが再利用されることを確実にするために)頻繁に変更されているものに並び替えることができます。
例えば以下のような順番です。

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

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

```Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.16-alpine AS build

# Install tools required for project
# Run `docker build --no-cache .` to update dependencies
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock
# These layers are only re-built when Gopkg files are updated
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only

# Copy the entire project and build it
# This layer is rebuilt when a file changes in the project directory
COPY . /go/src/project/
RUN go build -o /bin/project

# This results in a single layer image
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
```

## 不要なパッケージをインストールしない
複雑さ、依存関係、ファイルサイズ、およびビルドの時間を削減するためには、「あったほうが良い」という理由だけで余分であったり不要なパッケージをインストールしないようにします。例えば、データベースのイメージにテキストエディタを含める必要はありません。

## アプリケーションを切り離す
各コンテナでは一つの意図のみをもたせるべきです。アプリケーションを複数のコンテナに分離すると、水平スケールやコンテナの再利用をしやすくなります。例えば、Webアプリケーションスタックは、Webアプリケーション、データベース、インメモリキャッシュのそれぞれが独自のイメージを持った3つのコンテナで構成されるべきでしょう。  
  
各コンテナでプロセスを一つに制限することは良い経験則ですが、それは厳格なルールではありません。例えば、コンテナは[initプロセス](https://docs.docker.com/engine/reference/run/#specify-an-init-process)を生成できるだけではなく、一部のプログラムは独自の方法で追加のプロセスを生成する場合があります。例えば、[Celery](https://docs.celeryproject.org/en/stable/)は複数のワーカープロセスを生成でき、[Apache](https://httpd.apache.org/)はリクエストごとに1つのプロセスを作成できます。  
  
コンテナをできるだけ綺麗に、基本単位に保つためにベストな判断をしてください。もしコンテナがそれぞれに依存する場合は、[Dockerコンテナネットワーク](https://docs.docker.com/network/)を使用して、これらのコンテナが通信できるようにすることができます。  

## レイヤーの数を最小限に抑える
古いバージョンのDockerでは、イメージの中のレイヤーの数を最小化することが性能を出すために大切なことでした。この制限を減らすために以下の機能が追加されました。

- `RUN`,`COPY`,`ADD`の命令のみがレイヤーを作成します。他の命令は一時的な中間イメージを作成し、ビルドサイズを増加させることはありません。
- 可能な場所では、[マルチステージビルド](https://docs.docker.com/develop/develop-images/multistage-build/)を使用し、最終的なイメージ内で必要なもののみをコピーします。これによって、最終的なイメージサイズを増加させることなく、中間ビルドステージにツールやデバック情報を入れることができます。

## 複数行の引数を並び替える
可能な時には常に複数行の引数をアルファベット順で並び替えることで後の変更を容易にします。これはパッケージの被りを避けたり、リストをより簡単に更新することの助けになります。これはプルリクエストが読みやすくなったりレビューしやすくなることの大きな助けにもなります。バックスラッシュ(`\`)の前にスペースを追加することも役立ちます。  
これが[buildpack-deps](https://github.com/docker-library/buildpack-deps)のイメージ例です。

```Dockerfile
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-chace=true`オプションを使用することができます。しかし、Dockerにキャッシュを使用させる場合には、イメージが見つかった時にキャッシュできるかできないかを理解することが大切です。基本的なルールとしてDockerは以下の内容に従います。

- 既にキャッシュに存在する親イメージから始めて、次の命令とそのベースイメージから派生した全ての子イメージと比較され、全く同じ命令を使って構築しているものがないかどうかチェックします。もし一致しているものがなければ、キャッシュは無効化されます。
- ほとんどの場合、`Dockerfile`の命令を一つの子イメージと比較するだけで十分です。ただし、特定の指示にはさらなる調査と説明が必要です。
- `ADD`と`COPY`の命令では、イメージ内のファイルの中身を検査し、各ファイルのチェックサムが計算されます。これらのチェックサムではファイルの最終変更時間と最終アクセス時間は考慮されません。キャッシュが探している間、そのチェックサムは既存のイメージのチェックサムと比較されます。内容やメタデータなど、もしファイルに何かしらの変更があれば、キャッシュは無効化されます。
- `ADD`と`COPY`の命令を除いて、キャッシュが行うチェックはキャッシュが一致するかどうか判断するためにコンテナ内のファイルまで見ることはありません。例えば、`RUN apt-get -y update`コマンドが処理される時、コンテナ内でアップデートされたファイルについてはキャッシュが存在するかどうか判断するために調べられません。その場合、コマンド文字列自体はコマンドが一致するものを見つけるためにのみ使用されます。  

キャッシュが無効になると、全ての後続の`Dockerfile`コマンドはキャッシュを使用することなく新しいイメージを生成します。
## 宣伝
パーソルプロセス＆テクノロジー株式会社（以下パーソルP&T）、システムソリューション（SSOL）事業部所属の戸田です。

私はモビリティソリューションデザインチームに所属しており、モビリティ（ここでは移動手段全般）に関するサービスを考えたり、アプリを構築したりしております。

いわゆる「MaaS」に取り組んでおります。

私たちが「MaaS」に取り組む中で、現在活用している、もしくは活用する予定の技術やサービスやとりあえず発信したいことなどなど、幅広くチームメンバーと共に執筆していきたいと思います。
メンバーごとに違った内容を発信していきますので、お楽しみに！

また、「MaaS」について詳しく知りたい方は、チームメンバーの吉田が記事を掲載しておりますので、
ぜひそちらをご覧ください。

[「MaaSとは」でたどり着いて欲しい記事　（1/3 前編）](https://note.com/ppt_msdg_maas/n/naae427dbc37c)
[「MaaSとは」でたどり着いて欲しい記事　（2/3 中編）](https://note.com/ppt_msdg_maas/n/na45c90b2f02f)
[「MaaSとは」でたどり着いて欲しい記事　（3/3 後編）](https://note.com/ppt_msdg_maas/n/n8fd724f7db6c)

最後まで読んでいただき、ありがとうございました！