[Docker] 初心者が知っておくと便利かもしれない18の知識

初心者の状態から、既存サービスをDocker化するまでに学んだ知識のメモです。
※ 随時更新していきます

| 検証環境                | Version     |
|:-----------------------|:------------:
| Docker                 | 18.09.1     |
| Docker Compose         | 1.23.2      |
| Compose file format    | 3.0         |

以下の内容は、他の記事で分かりやすく説明されているのでここでは省きます。

- Docker / Docker Compose のインストール方法
- 基本的な使い方（といっても、私が知らなかっただけで一般的な使い方を挙げているかもしれませんがそこはご了承ください

また、dockerコマンドオプションとdocker-compose.ymlのオプション指定は適宜読み替えてください。

では早速行きましょう！


## Docker コマンド

### 1. build 時にキャッシュを使わない
Dockerfileを編集しても内容が反映されないときは試してみてください。

```bash
$ docker build -t hoge:v1 . --no-cache
```
### 2. \<none>：\<none> の image のみ抽出する
これまで `grep` 使ってましたが、ちゃんとドキュメントにありました。

```shell
$ docker images --filter "dangling=true"
```
↑を使って消す時は、

```bash
$ docker rmi $(docker images --filter "dangling=true" --format {{.ID}})
# もっと簡単に
$ docker rmi $(docker images --filter "dangling=true" -q)
```

### 3. コンテナ内のタイムゾーンを設定する

環境変数の`TZ`に`Asia/Tokyo`を設定すればOKです。

```shell
$ docker run -it -e TZ=Asia/Tokyo ruby:2.4 bash
```
Dockerfileに書いてももちろん問題ありません。

### 4. `-it`オプションってなに？
次のようなコマンドをよく見ます。私も大抵はこう使います。

```bash
$ docker exec -it [コンテナID] bash
```
この `-it`オプションは、`-i`と`-t`をまとめたもので、それぞれのざっくりな意味合いは

- `-i`：コンテナ内の標準出力とホスト側の**出力**をつなげる
- `-t`：ホスト側の**入力**をコンテナの標準出力につなげる

だそうです。試してみましょう。

```shell
$ docker run -i ruby:2.4 bash

（なにも操作ができない


$ docker run -t ruby:2.4 bash
root@05cda9646908:/#

（コンテナには入れるが、入力しても表示されない
```
コンテナ内でゴニョゴニョしたい場合は、２つのオプションをセットで使う必要があります。

### 5 `--no-trunc` をつけると、情報を省略せずに表示される

#### `--no-trunc` なし

```bash
 $ docker ps --format "{{.Command}}"
"irb"
"docker-entrypoint.s…"
"drakov -f api.md -p…"
"/usr/local/bin/run …"
"/usr/local/bin/run …"
"/usr/local/bin/run …"
"docker-entrypoint.s…"
"docker-entrypoint.s…"
```
#### `--no-trunc` あり

```bash
$ docker ps --format "{{.Command}}" --no-trunc
"irb"
"docker-entrypoint.sh mysqld"
"drakov -f api.md -p 3031 --autoOptions --public"
"/usr/local/bin/run npm run start"
"/usr/local/bin/run bundle exec sidekiq -q default -q mailers -q pull -q push"
"/usr/local/bin/run bundle exec rails s -p 3000 -b 0.0.0.0"
"docker-entrypoint.sh postgres"
"docker-entrypoint.sh redis-server"
```


## Dockerfile / docker-compose.yml

### 6. RUNを多用する時は

一行ごとにRUNを書くのではなく、次のようにつなげて書きましょう。
できるだけRUNを呼ぶ回数を減らすことで、可読性・メンテナンス性が向上します。また、buildのStep数削減で多少の時間短縮ができます。

```Dockerfile
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       build-essential \
       curl \
       git \
       zlib1g-dev \
       libssl-dev \
       libreadline-dev \
       libyaml-dev \
       libxml2-dev \
       libxslt-dev \
       libmysqlclient-dev
```

### 7. 不要な命令は減らす
Dockerfileで一通り動作する環境が出来たら、次は最適化していきましょう。
[Quickstart: Compose and Rails](https://docs.docker.com/compose/rails/) を見ると、こんな記述があります。

```Dockerfile
RUN mkdir /myapp
WORKDIR /myapp
```
myappディレクトリを作成して、作業用ディレクトリとするため`cd myapp`をした感じです。

この`WORKDIR`ですが、指定するディレクトリがもし存在しない場合は作成してくれますので、

```Dockerfile
WORKDIR /myapp
```

だけでよいことになります。


### 8. ADDとCOPYってどう使い分けるの？

基本的にはローカルのファイルをコンテナにコピーする役割で使用します。
ただし、

- ADD： コピーするソースが `gzip、bzip2、xz` の場合、指定ディレクトリに展開される
- COPY：ただコピーするだけ

という違いがあります。
使い分けとして、以下のように`何をする命令なのか明確にする`という点で使用するといいと思います。

```Dockerfile
ADD db_data.tar.gz /tmp/dump/
COPY Gemfile /app/Gemfile
```

### 9. EXPOSEって必要？
弊社で開発環境をDocker化しているリポジトリを覗いてみると、7割のDockerfileで、EXPOSEが記述されていました。

公式でEXPOSEを見てみると、
>EXPOSE 命令は、特定のネットワーク・ポートをコンテナが実行時にリッスンすることを Docker に伝えます。 EXPOSE があっても、これだけではホストからコンテナにアクセスできるようにしません。

とあります。
つまり、この命令で何かが実行されるわけではありませんが、`どのポートに接続するのかを明示しておく`、という点で書いておいたほうが親切かもしれません。


### 10. RUNとCMDの違い

先ほどもでてきた`RUN`ですが、これはシェル形式でコマンドを実行するものであり、Dockerfile内に複数回出てきても問題のないものです。

それに比べて`CMD`は、Dockerfileには原則１つのみ記述します。
もし複数の記述がある場合は、最後の`CMD`が実行されます。
また、`CMD`は、コンテナ実行時のデフォルトを提供するそうです。

例として、Rubyの公式イメージを使用してみます。

```shell
$ docker run -it ruby:2.4
irb(main):001:0>
```

`irb`の待受状態になりました。
これは、Rubyの[公式イメージ](https://github.com/docker-library/ruby/blob/beb05c8c3c694e4527effe3e3623120486d0e5ed/2.4/stretch/Dockerfile)を見るとわかりますが、Dockerfileの最後に

```Dockerfile
CMD [ "irb" ]
```
と書いてあるためです。

RUNとCMDの使い分けは、Railsのアプリケーションで例えると、

```Dockerfile
RUN bundle install \
    && rails db:create \
    && rails db:migrate

CMD ["rails", "s", "-b", "0.0.0.0"]
```
という感じでしょうか。
これでコンテナが実行されるとき、同時にRails Serverが起動します。

### 11. DBコンテナであらかじめユーザを作成しておきたい

MySQLやPostgreSQLを使用する時、事前にDBやユーザを作成しておきたい、なんてことがあると思います。
その時は、あらかじめ拡張子が`.sh`または`.sql`のファイルを用意し、`/docker-entrypoint-initdb.d/`ディレクトリへ設置しておくだけで、コンテナ実行時に自動で行ってくれます。
具体的にはこんな感じです。

```init.sh
psql -v ON_ERROR_STOP=1 -U postgres <<-EOSQL
    CREATE USER hoge_user WITH CREATEDB;
    CREATE DATABASE hoge_development OWNER hoge_user;
EOSQL
```

```Dockerfile.postgres
FROM postgres:9.3.16
COPY init.sh /docker-entrypoint-initdb.d/
```
ファイルの実行順を指定したい場合は、`1_init.sh`、`2_restore.sql`とすればOKです。

### 12. cachedを使う
マウント遅い問題ですが、Docker `17.04.0-ce`から`cached`が使えるようになり、多少は改善されたようです。


```docker-compose.yml
app:
  build: .
  volumes:
  - .:/myapp:cached
```
ただし、読み込みは速くなったものの、ホストからの書き込みがコンテナ内で反映されるまで時間がかかる場合もあるそうです。

ちなみに、cachedの他にも`consistent `と`delegated`が追加されていました。

- consistent：ホストとコンテナの整合性を保つ（デフォルト）
- delegated：コンテナ内でのマウントは速いものの、コンテナで行われた書き込みがホストからの読み込みに遅延が生じることがある

私は基本的に`cached`指定ですが、適宜変更していく形がいいかと思います。

### 13. 複数のDockerfileをディレクトリに含める
複数のDockerfileを扱おうとすると、リポジトリ直下が汚くなったり、Dockerfileの名前を`Dockerfile.db`として扱うのはちょっとかっこ悪いので、例えば`.docker`というディレクトリにまとめて管理したいとします。

```docker-compose.yml
version: '3'
services:
  web:
    build:
      context: .
      dockerfile: .docker/containers/app/Dockerfile
...
```
buildの`context`と`dockerfile`を指定します。
contextを変えれば、`docker-compose.yml`も`.docker`ディレクトリに含めることも可能ですが、その場合は`docker-compose`コマンドでファイルオプションをつける必要があるため、ymlだけはディレクトリ直下に置いています。

### 14. EC2でDockerコンテナ内のcronが動かない
ローカルでは問題なく動いていたのに…
詳しくは http://tanksuzuki.com/post/docker-pam-error/ をご覧ください。

解決方法として、`docker-compose.yml`を以下のように修正します

```docker-compose.yml
app:
    build: .
    cap_add:
      - audit_control
...
```


### 15. `RUN` 命令のキャッシュ

`RUN、COPY、ADD`コマンドではレイヤを作成し、`RUN` は文字列が変わらない限りレイヤーを cache します。
`COPY` `ADD` したファイルはチェックサムを計算し、以前と異なる場合は、それ以降の `RUN` の cache が破棄されるそうです。

よって、Lockfile などを先にコピーしておくことにより、効率的にレイヤーキャッシュを活用できるそうです。

```dockerfile
...

# Gemfile, Gemfile.lock が更新されない限り、bundle install はキャッシュされる
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
```

### 16. 意図しない改行を防ぐ

コンテナ内のコンソールを操作中に意図しないところで改行してしまうことがあります。
そのときは、以下の環境変数を設定しましょう。

```bash
$ docker-compose exec -e COLUMNS=$COLUMNS -e LINES=$LINES -e TERM=$TERM app bash
```

## その他

### 17. no space left on device

`docker-compose build`をすると、さっきまで何もなかったのに、急に以下のメッセージで通らなくなってしまうことがあります。

```
... no space left on device
```
これは、VMの容量が足りないときに発生するようです。
そんなときは、使用していないイメージとコンテナを削除してみましょう。

```bash
$ docker rm CONTAINER [CONTAINER…]
$ docker rmi IMAGE [IMAGE...]
```

### 18. Docker関連をまっさらに

イメージ、コンテナ、ボリュームをキレイに消せる便利なコマンドがあります。
https://github.com/ZZROTDesign/docker-clean

ただ、Dockerのステータスを理解しておかないと望んでいないコンテナを削除してしまう恐れもある（経験あり）ので、使用には気をつけてください。

[追記] @AkihiroSuda さんよりコメントいただきました

```bash
$ docker system prune
```
というものがあるそうで、実際に試してみると

```bash
$ docker system prune --all
WARNING! This will remove:
        - all stopped containers
        - all networks not used by at least one container
        - all dangling images
        - all build cache
Are you sure you want to continue? [y/N]
.
.
.

Total reclaimed space: 14.8GB
```
と親切に警告も出してくれます。スッキリしました。

## おわりに
まだまだ勉強中のため、「お、そこ違うかもよ？」という点があればいつでもコメントお待ちしております！
誰かのお役に立てば幸いです。