[Docker] Dockerfileを書く際の知見まとめ

# はじめに
こんにちは，Docker使用歴一週間のものです．最近Dockerについてばかり調べています．
[Docker](https://www.docker.com)は，コンテナ型の軽量な仮想環境を構築するソフトウェアです．
個人的なポイントは，
- 綺麗な環境がすぐに構築できる．
- 環境が汚くなれば，捨てれば良い．
- 環境の保存，再現が容易．
この辺りでしょうか．
詳しい基本的な説明は別の方にお任せして，この記事では，Dockerfileを書いているうちに気になったことを備忘録がてらにまとめていきます．（詳しい方には当たり前のことかと思うかもしれませんが，ご容赦ください．．．）
今回は，Ubuntuのイメージを利用して，Dockerfileから新しいイメージを作ることを想定しています．

### 環境

```console
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.4
BuildVersion:	18E226

$ docker -v
Docker version 18.09.2, build 6247962
```

# `sudo`は使えない
以下のようなDockerfileからイメージを作成するとします．

```dockerfile:Dockerfile
FROM ubuntu:latest

ENV USER hoge

# Add User and Change User
RUN useradd -m ${USER}
RUN gpasswd -a ${USER} sudo
RUN echo "${USER}:pass" | chpasswd
USER ${USER}

RUN sudo apt-get update
```

当たり前かもしれませんが，buildする際，一番最後の`RUN`でエラーが起きます．
というのも，一般ユーザーからsudoする時，パスワードの入力が求められますが，build中はパスワードの入力は不可能であるからです．
どうしても`sudo`の必要なコマンドを実行する際は，ユーザーを変更する前にrootで行うか，最後にシェルスクリプトを実行するように`CMD ["./startup.sh"]`を付け足すなどが考えられます．（ただし，シェルスクリプトは，イメージではなく，コンテナ作成時に実行されます．）

# コンテナのログインシェルの変更（`SHELL`などについて）
まず，公式のUbuntuのイメージのログインシェルを確認してみます．

```console
$ docker run --rm -it ubuntu
root@e406d43e91vr:/# echo $SHELL
/bin/bash
root@e406d43e91vr:/# grep $(whoami) /etc/passwd
root:x:0:0:root:/root:/bin/bash
```

ログインシェルはbashのようです．では，ここからzshに変更したいと思います．

## `SHELL`命令を使ってみる

```dockerfile:Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y --no-install-recommends zsh \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

SHELL ["/bin/zsh", "-c"]
```

Dockerには`SHELL`命令があるので，これを使えば変更できるでしょう．
と思っていました．．．
では，上記のDockerfileからイメージを作成します．

```console
$ docker build -t test .
...
$ docker run --rm -it test
root@406d41e91vbe:/# echo $SHELL
/bin/bash
root@406d41e91vbe:/# grep $(whoami) /etc/passwd
root:x:0:0:root:/root:/bin/bash
```

．．．変わっていません．．．
どうやら`SHELL`コマンドはビルド時のシェルを変更するもので，コンテナ内のログインシェルを変更するものではないっぽい？
なので，普通のコマンドで変更します．

## `chsh`で変更してみる

```dockerfile:Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y --no-install-recommends zsh \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

# SHELL ["/bin/zsh", "-c"]
RUN chsh -s /bin/zsh
```

```console
$ docker build -t test2 .
...
$ docker run --rm -it test2
root@06ve40d193e9:/# echo $SHELL
/bin/zsh
root@06ve40d193e9:/# grep $(whoami) /etc/passwd
root:x:0:0:root:/root:/bin/zsh
```

変更できたようですが，プロンプトの形式が先ほどと変わっていません．
色々調べてみると，`$SHELL`はログインシェルらしく，現在のシェルは`$0`だそうです．今まで現在のシェルだと勘違いしてました．．．
そこで，`$0`を見てみます．

```console
root@06ve40d193e9:/# echo $0
/bin/bash
```

変更されてない！！！
ログインシェルとはその名の通り，ログインした際に起動されるシェルということだと思うのですが，コンテナを作成するということは，ログインするということではないということでしょうか．この点は，階層の話にも繋がってきそうですが，まだまだ勉強不足です．

とりあえず，この節の結論は，「`chsh`でも”コンテナ起動時のシェル”は変更できない」です．
しかし，ログインシェルの変更はできているので，ビルド時にログインし直すような処理があれば，できそうな気がしなくもないのですが，どうすればよいかはまだわかりません．．．

## `CMD`もしくは`ENTRYPOINT`を使ってみる
GitHubなんかでDockerfileと検索すると，Dockerfileの最後で`CMD`や`ENTRYPOINT`でシェルを指定しているのをよく見かけます．その方法を用います．（`CMD`と`ENTRYPOINT`についてまだ詳しくないので使いたくなかったのですが．．．）

```dockerfile:Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y --no-install-recommends zsh \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/*

CMD ["/bin/zsh"]
```

```console
$ docker build -t test3 .
...
$ docker run --rm -it test3
e40d10e406ve# echo $SHELL

e40d10e406ve# echo $0    
/bin/zsh
e40d10e406ve# grep $(whoami) /etc/passwd
root:x:0:0:root:/root:/bin/bash
e40d10e406ve# exit
```

シェルが変更されました！確かに`$0`がzshになっていますし，プロンプトも先程と異なります．
でも，ログインシェルの変更はうまくいってませんね．この点は，先程の`chsh`で変更することができます．（ログインシェルを使わないので，設定する意味は定かではありませんが．）
ちなみにDockerfileの`CMD`の部分を`ENTRYPOINT`に変更しても同様に動きます．
ただし，両者で役割が異なりますし，厳密に扱いたい場合は詳しく調べることをお勧めします．

# hadolintを使っていると`apt-get`で何か言われるが．．．
[hadolint](https://github.com/hadolint/hadolint)は，Haskellで実装された静的解析ツールです．
hadolintのルールに従って，エラーとなる書き方や非推奨な書き方をビルド前に教えてくれます．

```console
$ brew install hadolint
$ hadolint Dockerfile
```

さて，このhadolintのルールの中にこういうものがあります．
[DL3009](https://github.com/hadolint/hadolint/wiki/DL3009): Delete the apt-get lists after installing something.
`apt-get`で何かインストールした後はリストを消せという感じの意味でしょうか．
下記のDockerfileをhadolintにかけます．

```dockerfile:Dockerfile
FROM ubuntu:latest

RUN apt-get update
RUN apt-get install -y --no-install-recommends zsh
```

```
$ hadolint Dockerfile
Dockerfile:1 DL3007 Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile:3 DL3009 Delete the apt-get lists after installing something
Dockerfile:4 DL3008 Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
```

Dockerfileの3行目で先程述べたルールが出てきていますね．では，これをWikiに則って真面目に直します．（他のエラーは今回は無視します．無視するオプションもありますが，今回は付けていません．）

```dockerfile:Dockerfile
FROM ubuntu:latest

RUN apt-get update \
　 && apt-get clean \
　 && rm -rf /var/lib/apt/lists/*
RUN apt-get install -y --no-install-recommends zsh
```

```console
$ hadolint Dockerfile
Dockerfile:1 DL3007 Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Dockerfile:6 DL3008 Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
```

`DL3009`は消えました．では，ビルドしてみましょう．

```console
$ docker build -t test4 .
...
Step 3/3 : RUN apt-get install -y --no-install-recommends zsh
 ---> Running in 006ve4db0ae7
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package zsh
The command '/bin/sh -c apt-get install -y --no-install-recommends zsh' returned a non-zero code: 100
```

エラーが生じてしまいました．最後で`/var/lib/apt/lists/*`を消してしまったのが悪かったのでしょうか．
また，この`DL3009`については，似たような問題かはわかりませんが，[issue](https://github.com/hadolint/hadolint/issues/311)もありました．
これを解決する方法としては，`apt-get`を1ラインで済ますなどを挙げています．

```dockerfile:Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y --no-install-recommends zsh \
　 && apt-get clean \
　 && rm -rf /var/lib/apt/lists/*
```

もしくは，エラーは無視して最後に`rm`するようにするか，イメージのサイズが気にならなければ，無視することも挙げています．（理解ミスで間違っていたらご指摘お願いします．．．）

# 終わりに
今考えると，そりゃそうだと思う内容ばっかりなのですが，自分が通ってきた道ということで記事にしました．そりゃそうだと思えることが，成長した証というものでしょうか．今後も何かあれば，追記します．
また，何か気になる点や直すべき点があれば，コメントお願いします．

# 参考サイト
- [Enterprise Application Container Platform | Docker](https://www.docker.com)
- [Docker Documentation](https://docs.docker.com/engine/reference/builder)
- [Dockerfileでビルド時にbashを使う - Qiita](https://qiita.com/anagura0000/items/bef08bf129f1bd8529ce)
- [現在使っているシェルの名前を知る方法 - 揮発性のメモ２](https://iww.hateblo.jp/entry/20131108/dash)
- [hadolint/hadolint: Dockerfile linter, validate inline bash, written in Haskell](https://github.com/hadolint/hadolint)
- [DockerfileをLintする - Qiita](https://qiita.com/ktsujichan/items/ac7cd220b97fc0656a26)
- [DL3009 Warning when clean done as a later statement? · Issue #311 · hadolint/hadolint](https://github.com/hadolint/hadolint/issues/311)