😊

debファイルのキャッシュを用いてdocker buildを高速化する

2023/08/21に公開

mount=type=cacheを利用してビルドを高速化する

Dockerfileにてdebianをベースイメージとして利用する場合、aptコマンドで必要なパッケージをインストールする。例えば、git をインストールしたければ apt-get install git する。

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

この実行結果はdocker layerキャッシュとして保存されるので、再度ビルドするときに毎回gitパッケージをダウンロード・インストールしなくて済む。
ただし、この方法ではRUNコマンドを再実行するときにはキャッシュが利用できないので同じパッケージをダウンロードする必要がある。このRUNコマンドより前のlayerに変更を加えた場合や、インクリメンタルに開発しており何度もapt-get installを繰り返す必要がある場合などではキャッシュが効かない。

このようなケースでは mount=type=cache を指定してキャッシュを利用する方法がdockerドキュメントなどでも推奨されている。

https://docs.docker.com/build/cache/#use-the-dedicated-run-cache

RUN \
    --mount=type=cache,target=/var/cache/apt \
    apt-get update && apt-get install -y git

この方法を採用することで、aptのキャッシュファイルはdockerイメージ外のストレージに保存される。このため、dockerイメージサイズを小さく保ちつつ、キャッシュファイルを流用してビルドを高速化できる。

debファイルの自動削除を無効化する

これで上手くいくと思いきや、実際にビルドを実行してみると毎回debファイルをダウンロードしておりキャッシュを活用できていない。キャッシュディレクトリを確認してみるとダウンロードしたはずのdebファイルが保存されていない。

$ cat Dockerfile
FROM debian

RUN echo "build"

RUN --mount=type=cache,target=/var/cache/apt \
    du -sh /var/cache/apt \
 && apt-get update \
 && apt-get install -y git

$ docker build . --progress=plain
...
#6 0.166 4.0K	/var/cache/apt
...
#6 3.014 Need to get 25.4 MB of archives.
...

# 先頭のRUNコマンドを変更して2回目のビルド
$ sed -i s/build/build2/ Dockerfile
$ docker build . --progress=plain
...
#6 0.149 12K	/var/cache/apt
...
#6 2.757 Need to get 25.4 MB of archives.
...

原因はaptの設定でdebファイルを削除していること。
自動削除の設定が /etc/apt/apt.conf.d/docker-clean にあり、apt-get install後にapt-get clean相当のコマンドが自動実行される。このため、mount=type=cache でdebファイルのキャッシュを利用しようとしても勝手に削除されてしまう。

$ docker run -it --rm debian cat /etc/apt/apt.conf.d/docker-clean
# Since for most Docker users, package installs happen in "docker build" steps,
# they essentially become individual layers due to the way Docker handles
# layering, especially using CoW filesystems.  What this means for us is that
# the caches that APT keeps end up just wasting space in those layers, making
# our layers unnecessarily large (especially since we'll normally never use
# these caches again and will instead just "docker build" again and make a brand
# new image).

# Ideally, these would just be invoking "apt-get clean", but in our testing,
# that ended up being cyclic and we got stuck on APT's lock, so we get this fun
# creation that's essentially just "apt-get clean".
DPkg::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };
APT::Update::Post-Invoke { "rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true"; };

Dir::Cache::pkgcache "";
Dir::Cache::srcpkgcache "";

# Note that we do realize this isn't the ideal way to do this, and are always
# open to better suggestions (https://github.com/debuerreotype/debuerreotype/issues).

これを回避するためには、この設定ファイルを削除すればよい。
また、 /var/cache/apt 以外にも /var/lib/apt/lists もネットワーク通信およびファイル保存が行われるので、合わせてキャッシュ対象に指定するとよさそう。
念のため、事前にdocker builder pruneコマンドでビルドキャッシュを削除しておく。

$ cat Dockerfile
FROM debian

RUN echo "build"

RUN --mount=type=cache,target=/var/cache/apt \
    du -sh /var/cache/apt \
 && rm /etc/apt/apt.conf.d/docker-clean \
 && apt-get update \
 && apt-get install -y git

$ docker build . --progress=plain
...
#6 0.194 4.0K	/var/cache/apt
...
#6 2.772 Need to get 25.4 MB of archives.
...

$ sed -i s/build/build2/ Dockerfile
$ docker build . --progress=plain
...
#6 0.142 94M	/var/cache/apt
...
#6 2.576 Need to get 0 B/25.4 MB of archives.
...

以上により、apt-get でダウンロードしたファイルをキャッシュとして保存できる。
dockerイメージは肥大化しないし、docker layerキャッシュが利用できない場合でもdockerビルドはキャッシュを用いて高速に実行できるようになる。

キャッシュの排他制御

Dockerfile referenceにはマウントオプションに sharing=locked を追加して排他制御を行うことを推奨している。
これにより並列で複数のビルドを実行しているときにキャッシュファイルへの排他制御を実現できる。

# syntax=docker/dockerfile:1
FROM ubuntu
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  apt update && apt-get --no-install-recommends install -y gcc

参考

Discussion