Dockerfileチートシートv19.03

# 概要
`Dockerfile`命令をまとめました。
※すべて網羅しているわけではありません。

- Dockerバージョン：v19.03

# 早見表
|命令|概要|
|:---|:---|
|FROM|ベースイメージを指定|
|RUN|指定コマンドを実行|
|ENTRYPOINT|コンテナ実行時のコマンドを指定|
|CMD|コンテナ実行時のコマンドを指定(上書き可)|
|COPY|ホストマシンからコンテナイメージへファイル/ディレクトリを単純コピー|
|ADD|COPY+解凍 / URLからダウンロード(**非推奨**)|
|ENV|環境変数を追加|
|EXPOSE|指定ポートを開ける|
|WORKDIR|カレントディレクトリを変更|
|MAINTAINER|**deprecated**<br>現在は`LABEL maintainer="maintainer@example.com"`と指定すべき

## FROM
ベースイメージを指定して取得します。1行目はこのコマンド。

[Docker Hub](https://hub.docker.com/)やプライベートDockerリポジトリからイメージを指定します。

- タグ指定なし　：`latest`でpull

```dockerfile:Dockerfile
FROM centos
ENTRYPOINT ["/bin/bash"]
```
```bash
$ docker image build --file Dockerfile -t testrepo:test .
$ docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY          TAG                 SIZE
testrepo            test                220MB # <- 今回作成されたイメージ
centos              latest              220MB # <- ベースイメージ
```


- タグ指定あり　：指定したタグでpull、基本的には指定すべき

```dockerfile:Dockerfile
FROM centos:7.7.1908
ENTRYPOINT [ "/bin/bash" ]
```
```bash
$ docker image build --file Dockerfile -t testrepo:test .
$ docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
REPOSITORY          TAG                 SIZE
testrepo            test                204MB # <- 今回作成されたイメージ
centos              7.7.1908            204MB # <- ベースイメージ
```

## RUN
指定したコマンドを実行します。
Tipsで後述していますが、できる限りまとめて記載すべきです。

```dockerfile:Dockerfile
FROM centos:8
RUN yum install -y httpd iproute && \
    echo "Test" > /var/www/html/index.html
ENTRYPOINT ["/usr/sbin/httpd"]
```

## ENTRYPOINT / CMD
コンテナを起動した際に実行されるコマンドを規定します。
Dockerfileの最後の方にそれぞれ1回ずつ記述されるイメージ。

```dockerfile:Dockerfile
FROM centos:8

ENTRYPOINT ["df"]
CMD ["-H", "."]
```

指定するコマンドについては、半角スペースごとにコンマで区切って記述します。例えば`go run main.go`を指定したい場合は以下のようになります。

```dockerfile:Dockerfile
ENTRYPOINT ["go", "run", "main.go"]
```

### ENTRYPOINTとCMDの違い

`ENTRYPOINT`は必ずそのまま実行され、
`CMD`はコンテナ起動時に引数を指定すると上書き可能です。
この特性を生かして、`ENTRYPOINT`にはコマンド部分、`CMD`にはコマンドのオプション部分を記載するという書き方ができます。

*e.g.*
上記のDockerfileでイメージをビルドしたとして、引数ありでコンテナ起動する場合と引数なしでコンテナ起動する場合の、具体的な実行結果の違いを以下に示しておきます。

- 引数なし : `df -H .`が実行されます。

```bash:Terminal
$ docker run override_test
Filesystem      Size  Used Avail Use% Mounted on
overlay          63G  2.1G   58G   4% /
```

- 引数あり(`--help`) : `df --help`が実行される。CMDの部分がコマンドラインに指定したオプションで上書きされていることがわかります。

```bash:Terminal
$ docker run docker_test --help
Usage: df [OPTION]... [FILE]...
Show information about the file system on which each FILE resides,
or all file systems by default.

Mandatory arguments to long options are mandatory for short options too.
  -a, --all             include pseudo, duplicate, inaccessible file systems
# 以下略
```

## COPY
ホストマシンのファイルまたはディレクトリをコンテナ内にコピーします。

```bash:ホストマシンのディレクトリ構成
.
├── Dockerfile
├── copydir
│   ├── subfile.txt
│   └── subfile2.txt
└── copyfile.txt
```
```dockerfile:Dockerfile
FROM centos:latest

RUN mkdir -p /app
# ファイル単位
COPY ./copyfile.txt /app
# ディレクトリ単位 ※コピー先のディレクトリ名称を指定する必要あり
COPY ./copydir /app/subdir
```
```bash:Terminal
$ docker image build --file Dockerfile -t centos:copytest .
$ docker container run centos:copytest ls -ltR /app
/app:
total 8
drwxr-xr-x 2 root root 4096 Jan 12 13:27 subdir
-rw-r--r-- 1 root root    6 Jan 12 13:18 copyfile.txt

/app/subdir:
total 4
-rw-r--r-- 1 root root 0 Jan 12 13:25 subfile2.txt
-rw-r--r-- 1 root root 8 Jan 12 13:19 subfile.txt
```

## ADD
`COPY`同様、ホストマシンのファイルをコンテナ内へコピーします。(上記Dockerfileの`COPY`を`ADD`としても同様の結果となります)
ただし、`ADD`を利用すると、コピーファイルが圧縮ファイルであった場合、自動的に解凍が行われます。

```dockerfile:Dockerfile
FROM centos:8

RUN mkdir -p /app
# addtest.tarはcopydirを圧縮したファイル
ADD ./addtest.tar /app
```
```bash:Terminal
$ docker image build --file add/Dockerfile -t centos:addtest .
$ docker container run centos:addtest ls -lR /app
/app:
total 4
drwxr-xr-x 2 501 games 4096 Jan 12 13:25 copydir

/app/copydir:
total 4
-rw-r--r-- 1 501 games 8 Jan 12 13:19 subfile.txt
-rw-r--r-- 1 501 games 0 Jan 12 13:25 subfile2.txt
# tarファイルそのものはなく、解凍されたものが存在している
```

*e.f.* `COPY`の場合

```bash:Terminal
$ docker container run testrepo:copy ls -lR /app
/app:
total 4
-rw-r--r-- 1 root root 177 Jan 12 13:31 addtest.tar # 圧縮ファイルはそのまま
```

### COPYとの棲み分け

- 単純にコピーのみ行う場合 => COPY
- コピーして解凍も行う場合 => ADD

## ENV
実行コンテナに環境変数を追加します。

```docker:Dockerfile
FROM centos:8                                                                                                                                                              
ENV NEW_ENV='env-value'

ENTRYPOINT ["/bin/bash"]
```
```bash:Terminal
$ docker image build --file Dockerfile -t centos:envtest .
$ docker container run -it centos:envtest
[root@de6dc0f2577e /]# env  
LANG=en_US.UTF-8
HOSTNAME=de6dc0f2577e
NEW_ENV=env-value   # fileで追加した環境変数
```


## EXPOSE
指定したポートをLISTEN状態とします。

```docker:Dockerfile
FROM python:3.8-slim-buster

COPY . /
RUN pip install -r /app/requirements.txt

EXPOSE 9876
ENTRYPOINT [ "gunicorn", "flask_app:app" ]
CMD [ "-c", "/app/config/gunicorn_settings.py" ]
```
```bash:Terminal
$ docker image build --file Dockerfile -t python:flask .
$ docker container run --name exposetest python:flask
$ docker container ps --format "table {{.Image}}\t{{.Ports}}"
IMAGE             PORTS               NAMES
flask             9876/tcp            exposetest
a2556b3a812                           worktest
# PORTSにLISTENEDのポート番号が表示される
```


## WORKDIR
作業ディレクトリを変更します。
Dockerfileにおける`cd`。

```dockerfile:Dockerfile
FROM centos:8
RUN mkdir -p /test
RUN echo "before workdir" > before.txt

WORKDIR /test
RUN echo "after workdir" > after.txt

ENTRYPOINT ["/bin/bash"]
```
```bash:Terminal
$ docker image build --file Dockerfile -t centos:workdir .
$ docker container run -it --name workdirtest centos:workdir 
[root@4cd5accab742 test]# pwd
/test   # 最後にWORKDIRしたパスがコンテナ起動時のベースパスとなる
[root@4cd5accab742 test]# ls .. | grep before
before.txt    # beforeはWORKDIR前に作成したのでルートディレクトリに存在
[root@4cd5accab742 test]# ls .
after.txt     # afterはWORKDIR後に作成したため/testディレクトリに存在
```

# Tips
## マルチステージビルドで軽量化
マルチステージビルドとは、最終完成イメージの軽量化をはかるためのテクニックのようなものです。
一例として`Go`アプリケーションをマルチステージビルドを適用した場合とそうでない場合とのイメージサイズを比較してみます。

マルチステージビルドなし

```dockerfile:Dockerfile
FROM golang:1.13-alpine
COPY ./main.go ./
RUN go build -o /app ./main.go
ENTRYPOINT ["/app"]
```

マルチステージビルドあり

```dockerfile:Dockerfile
# 1段階め処理(ビルドを行う)
FROM golang:1.13-alpine as builder
COPY ./main.go ./
RUN go build -o /app ./main.go

# 2段階め(これが完成イメージ)
# ベースに最低限の要件を満たす軽量イメージを選択する
FROM alpine:3.11
# 1段階めのコンテナから必要物をコピー
COPY --from=builder /app .
ENTRYPOINT ["./app"]
```

サイズ確認

```bash:Terminal
REPOSITORY     TAG                 SIZE
golang         nonmultistage       361MB
golang         multistage          7.6MB # 圧倒的に軽い
```

## 命令はできるだけまとめる
Dockerfileの命令１つにつき、イメージキャッシュがそれぞれ構築されていきます。このイメージキャッシュレイヤー数を最小化することはベストプラクティスとされており、それにより軽量化やビルド高速化につながることがあります。具体的には以下の処置を行います。

- RUN命令は「バックスラッシュ + &&」でつなげて記載
- COPY/ADD命令もできるだけ少なくまとめる

特に`RUN`に関してはビルド時キャッシュの問題もあって、`yum update`と`yum install`は`&&`でつなげて書かないとワナにはまる等といったことがあります。(キャッシュについては本記事では記載しません。)


## ADD命令にはURLも指定可能でもそうは書かない
レイヤ数を少なくし、イメージ軽量化につなげるために、URLからリソースを取得する場合は`curl`または`wget`を利用します。

Example([Best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#add-or-copy)より抜粋)

```dockerfile:Dockerfile
# Good
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

# Bad
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
```

# Reference
- [Docker Docs](https://docs.docker.com/engine/reference/builder/)
- [Dockerfileベストプラクティス](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)