Dockerfileのベストプラクティス

業務やプライベートでのハンズオンを通して得た知見を元に、dockerfileの実践的な書き方を記載いたしました。
軽量なdocker imageを作る観点とセキュリティーの観点を踏まえた内容になっております。なにか付け足す点などあればコメントいただければと思います。

#軽量なimageを作る観点

##軽量なimageの使用
Dockerfileでimageを指定する際に、軽量なimageを使用することが進めれている。

```dockerfile
FROM golang:1.16-alpine AS build
```

[docker docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#from)でも代表的な軽量なimageのalpineをおすすめしている。

> Whenever possible, use current official images as the basis for your images. We recommend the Alpine image as it is tightly controlled and small in size (currently under 5 MB), while still being a full Linux distribution.

###注意点
軽量なimageを使用するのを基本とするのは良いが、
軽量なimageだともともと入っているバイナリやライブラリが少ない。
そのため大量のライブラリをinstallすることになると逆にbuildに時間がかかってしまう。

[仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編](https://future-architect.github.io/articles/20200513/#:~:text=%E3%83%8F%E3%82%9A%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B7%E3%82%99%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%E3%81%AF%E9%80%90%E6%AC%A1%E5%87%A6%E7%90%86%E3%81%AA%E3%81%AE%E3%81%A6%E3%82%99%E9%81%85%E3%81%8F%E3%80%81%E5%87%A6%E7%90%86%E7%B3%BB%E3%81%8B%E3%82%99%E5%85%A5%E3%81%A3%E3%81%9F%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B7%E3%82%99%E3%81%AE%E3%82%BF%E3%82%99%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%88%E3%82%99%E3%81%AE%E6%96%B9%E3%81%8B%E3%82%99%E9%AB%98%E9%80%9F%E3%81%A6%E3%82%99%E3%81%99%E3%80%82)
> パッケージダウンロードは逐次処理なので遅く、処理系が入ったイメージのダウンロードの方が高速です。並列で処理されるし、一度イメージをダウンロードしてしまえば、なんどもビルドして試すときに効率が良いです。また、多くのケースでネイティブのライブラリも最初から入っており、ビルドでトラブルに遭遇することはかなり減るでしょう。

そのためパッケージのinstallが多く必要な場合は、
パッケージの確保がされたimageを使用してMulti stage buildをするのが良い。
最近ではdistrolessというshellのないimageを採用する場合もある。

[#comment-3d0c9055614ed5b61bea](https://qiita.com/Tsuyozo/items/c706a04848c3fbbaf055#comment-3d0c9055614ed5b61bea)
[軽量Dockerイメージに安易にAlpineを使うのはやめたほうがいいという話](https://blog.inductor.me/entry/alpine-not-recommended)

##インストールするpackage listをcacheさせない
Dockerfile内でapk updateなどのコマンドでpackageのリストを更新すると、
packageのリストを/var/cache/apk/などにcacheとして残すようになっている。
[Alpine Linux package management](https://wiki.alpinelinux.org/wiki/Alpine_Linux_package_management#Update_the_Package_list)

```dockerfile:cacheが残る方法
RUN apk update && apk add \
    package-bar \
    package-baz \
    package-foo=1.3.*
```

このpackageのlistをcacheさせると多少imageが大きくなるので、cacheが残らないようにする

```dockerfile:cacheが残らない方法
RUN apk add --update-cache --no-cache
    package-bar \
    package-baz \
    package-foo=1.3.*
```


##Multi stage build

multi stage buildとはimageを複数つくり、片方のimageを、もう片方のimageのディレクトリをCOPYして作成する方法。
imageを2個作成しているので、FROMが2回出てきている。
Goのimageであるbuildの/bin/projectを、scratchのimageの/bin/projectにCOPYをして使用している。

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

Goのimageを容量が大きいので、必要なディレクトリをGoのimageで作成して、
軽量なimageのscratchにディレクトリをコピーしている。
つまりmulti stage buildをすることで、使用しないfileやlibraryなどが減り作成されるimageの容量が小さくなる。
今回の例ではscratchという空のdocker imageに、バイナリファイルのみを配置して動作するようにしている。
デプロイに特化(開発環境用のパッケージなどを持たない)していて、Goなどのコンパイル言語で開発している場合に、このようなimage定義をすることができる。

[docker docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#use-multi-stage-builds)でもmulti stage buildをおすすめしている。

> Multi-stage builds allow you to drastically reduce the size of your final image, without struggling to reduce the number of intermediate layers and files.

##RUNではcommandを&&でつなぐ
Dockerfileでimageを作成する際に、できるだけ容量を小さくするようにすることが大事である。
しかしRUNやCOPYやADDという命令はdocker imageのLayerを増やすことになり、最終的にできるimageの容量がおおきくなってしまう。

[docker docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#minimize-the-number-of-layers)にも「Layerを最小限にしよう」と記載されている

> Minimize the number of layers
> In older versions of Docker, it was important that you minimized the number of layers in your images to ensure they were performant. The following features were added to reduce this limitation:
>>Only the instructions RUN, COPY, ADD create layers. Other instructions create temporary intermediate images, and do not increase the size of the build.



###RUNを複数回使用した場合
コマンドを毎回、RUNで呼び出している場合、最終的にできるimageのサイズは153MBになる

```dockerfile
RUN npm ci 
RUN npm cache clean --force 
RUN mv /app/node_modules /node_modules
```

```bash
$ docker build -t run_command1_run_command2
REPOSITORY                  TAG      IMAGE ID       CREATED          SIZE
run_command1_run_command2   latest   2f66932a5335   10 seconds ago   153MB
```

###RUNを一度だけ使用した場合
コマンドを&&で数珠つなぎにした場合、最終的にできるimageのサイズは146MBになる

```dockerfile
RUN npm ci \
 && npm cache clean --force \
 && mv /app/node_modules /node_modules
```

```bash
$ docker build -t run_command1_&&_command2
REPOSITORY                 TAG      IMAGE ID       CREATED          SIZE
run_command1_&&_command2   latest   a6627748ccc8   10 seconds ago   146MB
```

つまりRUNを多用すると最終的にできるimageの容量が大きくなってしまうので、できるだけ数珠つなぎで実行するのが良い。

###補足
dockerのimageを一度buildすると作成されたLayerはdocker cacheという場所に配置される。
そして再度imageのbuildを行った際に、RUNのcommandの内容が、前回のbuildのRUNのcommandの内容と同じ場合、
前回のimageのbuildで作成されたLayerを使用する。

[docker docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run)にもそのように記載されている

> After building the image, all layers are in the Docker cache.
Docker sees the initial and modified instructions as identical and reuses the cache from previous steps. 

つまり中間のLayerがあったほうがdocker cacheを利用して、
差分buildを行うことができるので早くbuildのトライアンドエラーができる。
そのため開発環境で最初は開発効率を損なわないようにするために、すべてのステップでRUNでcommandを呼び出し、
build内容が決まってきたら、RUNの回数を減らすために&&を使用するようにするのが良い。


##COPY --chownを使用する
ファイルオーナーの権限変更は、通常chownやchmodで行われる。しかしRUN chown && chmodを使用すると作成されるimageの容量が大きくなる。そこでCOPY --chown=を使用してdocker containerにファイルを配置する時に権限の変更を行う。

###RUN chown && chmodを使用した場合
RUN chown && chmodでLayerが5.84MB大きくなっている。

```dockerfile
COPY . .
RUN chown -R node:node /app && \
    chmod -R 744 /app && \
    chown -R node:node /node_modules && \
    chmod -R 777 /node_modules && \
```

```bash
$ docker history run_chown_&&_chmod
IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
.
.
.
4fc46c072028   2 weeks ago   /bin/sh -c chown -R node:node /app &&     ch…   5.84MB
```

###COPY --chown=を使用した場合
COPY --chown=node:node . .の場合Layerは474kBしか大きくなっていない。

```dockerfile
COPY --chown=node:node . .
```

```bash
$ docker history run_chown_&&_chmod
IMAGE          CREATED       CREATED BY                               SIZE      COMMENT
.
.
.
32c46c083921   2 weeks ago   /bin/sh -c COPY --chown=node:nodedir:…   474kB
```


##COPYを複数回に分ける
変更がされにくいpackage管理ファイルのCOPYと、変更されやすいアプリケーションコードのCOPYを分けて行う。

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

これにより、特に必要なファイルが変更された場合にのみ各ステップのビルドキャッシュが無効になる。
package管理ファイルは頻繁に変更されることは少ないので、アプリケーションコードのCOPYと分けて行っている。
package管理ファイルが変更されていない場合のbuildではdocker cacheのLayerを使用するためbuildが早くなる。

##.dockerigonore
COPYやADDの対象にしたくないfileを設定する。
.gitigonoreのように、buildに必要ないfileをimageに配置しないようにする。

[docker docs](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#exclude-with-dockerignore)でも.dockerigonoreの使用をおすすめしている。

> To exclude files not relevant to the build (without restructuring your source repository) use a .dockerignore file. This file supports exclusion patterns similar to .gitignore files. For information on creating one, see the .dockerignore file.

###注意点
.dockerignoreによるbuildの速度低下が発生することがある。
原因としては、.dockerignoreで、指定するファイルをワイルドカードで指定しすぎてパースに時間がかかってしまうため。

```:.dockerignore(良くないパターン)
# archives
*.zip
*.lzh
*.tar.gz
*.tgz
*.bz2
*.dmg

# OSX
.DS_Store #<= 汎用的なものは記載しない
.Spotlight-V100
.Trashes
.AppleDB
.AppleDesktop
.apdisk

# Rails
.rspec
log
tmp
db/*.sqlite3
db/*.sqlite3-journal
vendor/assets

# Node
node_modules
```

.dockerignoreでは最低限のfileの記載、ワイルドカードの多用をしないことを意識する。

```:.dockerignore(良いパターン)
.rspec
log
tmp
db/*.sqlite3
db/*.sqlite3-journal
vendor/assets
node_modules
```

[.dockerignore アンチパターン](https://qiita.com/munisystem/items/b0f08b28e8cc26132212)

#セキュリティーの観点

##rootユーザーでimageを作成しない
dockerfileでUSERの指定を行い、non-rootユーザーの権限を与えるようにする。

```dockerfile
USER node
```

USERの指定がないdockerfileから作られたコンテナに入ると、root権限をもつ状態になる。
そのようになった場合万が一コンテナに侵入された場合にroot権限を与えてしまうことになる。
またそのような事態になった場合に、コンテナを載せいているホストにも侵入される可能性も高まるので、コンテナはnon-rootなuserで起動させなければいけない。
[Run Docker as a non-root user](https://www.thegeekdiary.com/run-docker-as-a-non-root-user/)

##ADDではなく、基本的にCOPYを使用する
ADDはローカルファイルのCOPYに加えて、インターネットに公開されているURLからコード取得できる機能のついている。
[docker docs](https://docs.docker.com/engine/reference/builder/#add)

> The ADD instruction copies new files, directories or remote file URLs from <src> and adds them to the filesystem of the image at the path <dest>.

外部のインターネットにアクセスすると悪意のあるURLからfileを取得しかねないので、基本的にはCOPYを使用する。

#その他
##imageのバージョンではlatestを使用しない

下記のようにimageのバージョンをlatestにしてしまうと、時間が経過するにつれてコンテナのimageのバージョンが変更される。
それはバグを生む原因になりうるので、しっかりバージョンを指定して使用するimageを宣言する。

```dockerfile
FROM golang:latest
```

# 参考資料

##メイン
- [Best practices for writing Dockerfiles](https://docs.docker.com/develop/develop-images/dockerfile_best-practices)
- [Docker/Kubernetes 実践コンテナ開発入門](https://www.amazon.co.jp/Docker-Kubernetes-%E5%AE%9F%E8%B7%B5%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E9%96%8B%E7%99%BA%E5%85%A5%E9%96%80-%E5%B1%B1%E7%94%B0-%E6%98%8E%E6%86%B2/dp/4297100339)
- [米シリコンバレーDevOps監修！超Docker完全入門(2020)【優しい図解説とハンズオンLab付き】](https://www.udemy.com/course/linux-docker-compose-dockerfile-kanzennyumon/)

##サブ
- [.dockerignore アンチパターン](https://qiita.com/munisystem/items/b0f08b28e8cc26132212)
- [Run Docker as a non-root user](https://www.thegeekdiary.com/run-docker-as-a-non-root-user/)
- [軽量Dockerイメージに安易にAlpineを使うのはやめたほうがいいという話](https://blog.inductor.me/entry/alpine-not-recommended)
- [DockerでRUNをまとめた方が良いとは限らない](https://future-architect.github.io/articles/20210121/)
- [仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編](https://future-architect.github.io/articles/20200513/)
- [仕事でPythonコンテナをデプロイする人向けのDockerfile (2): distroless編](https://future-architect.github.io/articles/20200514/)