.dockerignore アンチパターン

## .dockerignoreとは
Dockerfileからイメージをビルドする場合、Dockerfileの存在するディレクトリの中身はtarで固められdaemonへと送られます。

```
$ ls -la
total 2097168
drwxr-xr-x  5 muni  staff         170  3  7 12:40 .
drwxr-xr-x  8 muni  staff         272  3 10 15:19 ..
-rw-r--r--  1 muni  staff          76  2 16 17:04 Dockerfile
-rw-r--r--  1 muni  staff          18  2 16 10:10 docker-compose.yml
-rw-r--r--  1 muni  staff  1073741824  2 16 10:04 dummy.file
$ cat Dockerfile
FROM debian:jessie
```
このようなimageに含まないファイルも送信するため、

```
$ time docker build .
Sending build context to Docker daemon 1.074 GB
Step 1 : FROM debian:jessie
 ---> 9a02f494bef8
Successfully built 9a02f494bef8
docker build .  12.28s user 2.98s system 49% cpu 30.589 total #<= slow...
```
ビルドに余計な時間がかかってしまいます。
そこで **.dockerignore** の出番です。

.dockerignoreは.gitignoreのようにdockerで無視するファイルを指定することができるファイルです。

```text:.dockerignore
dummy.file
```

上記のようなファイルをDockerfileと同じディレクトリに置くだけで

```
$ time docker build .
Sending build context to Docker daemon 4.096 kB
Step 1 : FROM debian:jessie
 ---> 9a02f494bef8
Successfully built 9a02f494bef8
docker build .  0.01s user 0.01s system 37% cpu 0.055 total
```

1GBの無駄なファイルに時間をとられることなくイメージをビルドできるようになります。

またnode_modulesやvendor/assetsのようなイメージのビルド時に追加されるディレクトリも.dockerignoreに追加しておくことで、

```text:Dockerfile
FROM node

ADD . /src #<= node_modulesを除く全ファイルを/srcに配置する
WORKDIR /src

RUN npm install

CMD ["/usr/local/bin/node", "index.js"]
```
このようにDockerfileを書けるようになります。

## 本題

### .dockerignoreによってビルドが遅くなる
先ほど.dockerignoreを配置することでイメージのビルド時間の短縮が見込めると言いましたが、そうでない場合もあります。

```
$ ls
Capfile      Guardfile    bin          import       package.json spec
Dockerfile   Rakefile     config       lib          public       vendor
Gemfile      Vagrantfile  config.ru    log          readme.md
Gemfile.lock app          db           node_modules recipes
```

このようなRailsのプロジェクトに

```text:.dockerignore
vendor/assets
```

上記のようなファイルを配置してビルド時間を比較してみましょう。

**.dockerignore配置前**

```
$ time docker build .
Sending build context to Docker daemon 1.885 MB
Step 1 : FROM debian:jessie
 ---> 9a02f494bef8
Successfully built 9a02f494bef8
docker build .  0.05s user 0.02s system 42% cpu 0.169 total
```

**.dockerignore配置後**

```
$ echo vendor/assets > .dockerignore
$ time docker build .
Sending build context to Docker daemon 1.883 MB #<= -0.002MB
Step 1 : FROM debian:jessie
 ---> 9a02f494bef8
Successfully built 9a02f494bef8
docker build .  0.10s user 0.04s system 71% cpu 0.188 total #<= +0.019s???
```

何故かビルド時間が増加してしまいました...

## 原因
.dockerignore の実装は Go の [filepath.Walk](https://golang.org/pkg/path/filepath/#Walk) と [Regexp.MatchString](https://golang.org/pkg/regexp/#Regexp.MatchString) です。
つまり Dockerfile が存在するディレクトリをルートとして、ディレクトリをトラバースしながらファイル名が .dockerignore にマッチするかどうかを正規表現によって確認している処理によって実現しています。

実際にフローを文字に書き起こすと以下のようになります。

1. .dockerignore をパースする
2. .dockerignore にかかれている文字から正規表現を作る
3. ディレクトリをトラバーサルする
    * 正規表現がディレクトリにマッチした場合: ディレクトリを Ignore 対象とし、そのディレクトリをスキップする
    * 正規表現がファイルにマッチした場合: ファイルを Ignore 対象とする
4. 3. を繰り返す

これは**プロジェクトのファイル数（ベンダー含む）が多くなるに連れパフォーマンスが低下する**ということを表しています。

また
> 正規表現がディレクトリにマッチした場合: ディレクトリを Exclude 対象とし、そのディレクトリをスキップする

は Ignore 対象のディレクトリの中だけど Docker image に含めたいケースで利用する `!` が .dockerignore に存在する場合は行なわれません。

つまり `!` があると必ず .dockerignore のオーダーは `O(kN)` になります。
（k: .dockerignore のパターンの数、N: ルートからトラバーサルするファイルの数）

## アンチパターン
.gitignoreのように汎用的なものを列挙するのではなく、必要な物だけ記載するようにしましょう。

```text:Dockerfile_bad
# 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
```

```text:Dockerfile_good
.rspec
log
tmp
db/*.sqlite3
db/*.sqlite3-journal
vendor/assets
node_modules
```

また `!` をつかわなくていいケースであれば追加しないようにしましょう。

## まとめ

大きなプロジェクトでは.dockerignoreによるビルドの速度低下が発生する可能性があります。

||.dockeringnoreあり|.dockerignore無し|
|:-----:|:-----:|:-----:|
|--no-cache=true|14m18.878s|13m15.641s|
|--no-cache=false|1m7.326s|7.931s|

(464M ファイル数10207のRailsアプリケーションのdocker buildの時間比較)

.dockerignoreを一度見なおしてみてはいかがでしょうか？