Dockerfile ベストプラクティス

# この記事は
自分のために[公式ドキュメント](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)を自分がほしいものだけを簡単にまとめたものです。


# Dockerfile ベストプラクティス
Dockerのイメージはレイヤーというものが積み重なってできているものだと考えればよいです。Dockerfileの各行が前の行でつくったレイヤーに新しいレイヤーを重ねていきます。

# 基本的にするべきこと
## できるかぎりエフェメラルにする
エフェメラルとは簡単に壊せて簡単に作成できること。（たとえば、ホスト側に○○のファイルがあることが条件とかがあるとエフェメラルではなくなっていきます）

## ビルドコンテキストのファイルがすべてデーモンに送られることを意識する
ビルドコンテキストのファイルがDockerデーモンに送られる。-fでDockerfileを指定してもこれは変わらない。docker build PATH の PATH がビルドコンテキストになる。ビルドに必要ないファイルはビルドコンテキストを変えて送らないようにするのが良い。
どのくらいの容量がDockerデーモンに送られるのかはビルドするときのログを見ること。以下のようなログがでるはずである。
```
Sending build context to Docker daemon  187.8MB
```

# 標準入力からビルドするとビルドコンテキストとして送られるファイルが0になる
ファイルを作らずに標準入力からDockerfileの内容を入力して docker build することもできる。

フォーマットは
```
docker build [OPTIONS] -
```
な感じで、

```
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
```
とか、

```
docker build -t myimage:latest -<<EOF
FROM busybox
RUN echo "hello world"
EOF
```
とかのように書ける。

これをすると、ビルドコンテキストとして送られるファイルがなくなる。ビルドするのにホスト上のファイルが不要であればこれでビルドが速くなる。

これをすると、ファイルが何も送られなくなるので`COPY`や`ADD`は失敗する。

いくつかのファイルだけをビルドコンテキストのファイルとして贈りたくないときは (.dockerignore)[https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#exclude-with-dockerignore] を使うこと。

## ビルドコンテキストはローカルディレクトリーにしてDockerfileは標準入力から読みたい場合

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

## ビルドコンテキストをリモートの場所にしてDockerfileを標準入力から読みたい場合

ほぼ同じだが PATH が URL になる。

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

```

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

```
# 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"]
```

## 不要なモジュールはインストールするな
そのまま

## アプリケーションを疎にする
役割でちゃんと分ける。アプリとDBとキャッシュのコンテナに分けるとか。

## レイヤーの数を減らすことについて
（よくわからないから省略）


## 引数が複数行になる場合にはソートする
半角スペースと\で分けて、各引数が英数字順になるようにすると読みやすくてよい
```
RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion \
  && rm -rf /var/lib/apt/lists/*
```

## ビルドキャッシュを有効に使え
`--no-cache=true`を`docker build`で使えばキャッシュを使わないようにできる。でも遅くなる。どういうときにキャッシュを使って、どういうときにキャッシュを使わないほうが良いのかを知っておくのがよい。

- `ADD`と`COPY`はファイルの中身のキャッシュを見て更新されているかを判定してキャッシュを使うかどうかを決める
- その他のDockerfileのコマンドはファイルの中身を見ない。コマンドの文字列が変わっているかどうかだけを見る

# Dockerfile内の命令について
## FROM
できるだけ公式のイメージを使え。[Alpine](https://hub.docker.com/_/alpine/)がおすすめ。

## LABEL
ラベルつけたほうがいいよってこと
```
# Set one or more individual labels
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=""
```

## RUN

### apt-get
よく使う。よくハマるのは以下。

```
# syntax=docker/dockerfile:1
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl nginx
```
ってすると `RUN apt-get update` がキャッシュを使われてしまい実行されないので、

```
RUN apt-get update && apt-get install -y
```
のようにするか、以下のようにバージョン固定（バージョンピニング）する。以下の例ではapt cacheを消しているのでイメージが小さくなって良い。

```
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/*
```

※公式DebianとUbuntuは自動で`apt-get clean`をするので`rm`は不要

## パイプ
`/bin/sh -c`を使って実行するのパイプの最後のコマンドの終了コードしか見ない。それ以前のコマンドが失敗しても成功とみなすので注意すること。
この動作を変えたいのであれば`set -o pipefail &&`を使うこと
```
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
```

dashなどは`-o pipefail`を使えないので

```
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
```
などとする

## CMD
- サービスを提供するコンテナであれば`CMD`は`CMD ["executable", "param1", "param2"…]`の形式で使うのがほとんどだと思う
- `CMD ["param", "param"] `は`ENTRYPOINT`の使い方を知っている人が少ないのでやめておくのが良さそう
- サービス提供のコンテナでなければ`CMD ["perl", "-de0"], CMD ["python"], or CMD ["php", "-a"] `みたいなインタラクティブシェルにするのがよい

## EXPOSE
アプリケーションが使うデフォルトのポートを使うのが良い。Apacheウェブサーバーであれば80など。

## ENV
（難しそうなので省略。ビルド時の変数であればARG使えば良さそう）

## ADD or COPY
`ADD`より`COPY`を使うのがよい。`ADD`は単純なコピーではなくtar解凍だったりリモートURLが使えたりするので機能が複雑なので。例えば、`ADD rootfs.tar.xz /`とかでtar解凍ができる。

また、`ADD`するとレイヤーが増えるのでパッケージのダウンロードとインストールをするのならば、

```
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
```
のようにするのがよい

## ENTRYPOINT
これがデフォルトで実行されるコマンドの意味。これに`CMD`で引数を指定することができる。

## VOLUME
変更可能なファイル、ユーザーがサービスの一部として提供するファイル部分にはボリュームを使えって話


## USER
権限が必要ないのであればUSERをつかってrootじゃないユーザーを使おうって話

## WORKDIR
`WORKDIR`をいつも使おう、絶対パスで使おうって話。

## ONBUILD
（自分の作ったイメージが他のイメージのベースを作るときの`FROM`に使われるときの話で、自分はやらなそうなので省略）