Edited at

.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で無視するファイルを指定することができるファイルです。


.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に追加しておくことで、


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のプロジェクトに


.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.WalkRegexp.MatchString です。

つまり Dockerfile が存在するディレクトリをルートとして、ディレクトリをトラバースしながらファイル名が .dockerignore にマッチするかどうかを正規表現によって確認している処理によって実現しています。

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


  1. .dockerignore をパースする

  2. .dockerignore にかかれている文字から正規表現を作る

  3. ディレクトリをトラバーサルする


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

    • 正規表現がファイルにマッチした場合: ファイルを Ignore 対象とする



  4. 3. を繰り返す

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

また


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


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

つまり ! があると必ず .dockerignore のオーダーは O(kN) になります。

(k: .dockerignore のパターンの数、N: ルートからトラバーサルするファイルの数)


アンチパターン

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


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



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を一度見なおしてみてはいかがでしょうか?