背景
Dockerを利用する際、Multi-stage Buildが使われることが一般的になってきました。
しかし、Multi-stage Buildには、Mavenなどのパッケージマネージャのキャッシュを利用できないという欠点がありました。
そんな中、Dockerに搭載されたBuildkitで --mount=type=cache
を使えば、Multi-stage Buildでもパッケージマネージャのキャッシュが利用できるようになったとのことです。
そこで、MavenプロジェクトのSpring BootをMulti-stage Build + Buildkitでdocker buildしてみました。
環境
- Docker for Mac
- Docker Engine 18.09.0
手順
Spring Bootプロジェクト作成
Spring Initializr でMavenプロジェクトのSpring Bootを初期化しました。
Dockerfileを記述
Multi-stage Buildを使ったDockerfileを作成しました。
# syntax = docker/dockerfile:1.0-experimental
FROM openjdk:8u181-jdk-alpine3.8 AS builder
ADD . /work
WORKDIR /work
RUN --mount=type=cache,target=/root/.m2 \
./mvnw clean install
FROM openjdk:8u181-jre-alpine3.8 AS runner
ENV JAR=demo-0.0.1-SNAPSHOT.jar
RUN addgroup -S -g 1000 app \
&& adduser -D -S -G app -u 1000 app \
&& apk add --no-cache su-exec
WORKDIR /home/app
COPY --from=builder --chown=app:app /work/target/$JAR /home/app/
CMD ["sh", "-c", "su-exec app java -jar $JAR"]
# syntax = docker/dockerfile:1.0-experimental
の部分で --mount=type=cache
という文法を有効化しています。
RUN --mount=type=cache,target=/root/.m2
で.m2のキャッシュが効くようにしています。
Buildkit有効化
$ export DOCKER_BUILDKIT=1
ビルド1回目 1
$ docker build --no-cache -t demo .
[+] Building 151.6s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 557B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.0-experimental 2.0s
=> CACHED docker-image://docker.io/docker/dockerfile:1.0-experimental@sha256:d2d 0.0s
=> [internal] load metadata for docker.io/library/openjdk:8u181-jdk-alpine3.8 0.6s
=> [internal] load metadata for docker.io/library/openjdk:8u181-jre-alpine3.8 0.8s
=> CACHED [runner 1/3] FROM docker.io/library/openjdk:8u181-jre-alpine3.8@sha256 0.0s
=> [builder 1/3] FROM docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e4 9.7s
=> => resolve docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e45570b6f5 0.0s
=> => sha256:ef87ded15917facc562d84e802d02ded848ae802cc0419440 70.61MB / 70.61MB 7.5s
=> => sha256:b18e45570b6f59bf80c15c78d7f0daff1e18e9c19069c323613 2.03kB / 2.03kB 0.0s
=> => sha256:3ef5b11bf99dc3829740e59e17dfaa491aa745816828286c6e0bf53 947B / 947B 0.0s
=> => sha256:97bc1352afdef16cbb71b5fe50c605e3174ba40c721ce9e0da3 3.40kB / 3.40kB 0.0s
=> => extracting sha256:ef87ded15917facc562d84e802d02ded848ae802cc04194400ba4cb8 1.9s
=> [internal] load build context 0.1s
=> => transferring context: 67.98kB 0.0s
=> [runner 2/3] RUN addgroup -S -g 1000 app && adduser -D -S -G app -u 1000 3.3s
=> CACHED [internal] helper image for file operations 0.0s
=> [builder 2/3] ADD . /work 0.5s
=> [builder 3/3] RUN --mount=type=cache,target=/root/.m2 ./mvnw clean inst 134.3s
=> [runner 3/3] COPY --from=builder --chown=app:app /work/target/demo-0.0.1-SNAP 1.9s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:d1628e99aa58995cf80c0f0e655dc136ea48953a3c75a2028088e 0.0s
=> => naming to docker.io/library/demo 0.0s
ビルドには151秒かかりました。
ビルド2回目
$ docker build --no-cache -t demo .
[+] Building 24.4s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 37B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.0-experimental 1.9s
=> CACHED docker-image://docker.io/docker/dockerfile:1.0-experimental@sha256:d2d 0.0s
=> [internal] load metadata for docker.io/library/openjdk:8u181-jdk-alpine3.8 0.7s
=> [internal] load metadata for docker.io/library/openjdk:8u181-jre-alpine3.8 0.8s
=> [internal] load build context 0.0s
=> => transferring context: 1.67kB 0.0s
=> CACHED [internal] helper image for file operations 0.0s
=> CACHED [runner 1/3] FROM docker.io/library/openjdk:8u181-jre-alpine3.8@sha256 0.0s
=> CACHED [builder 1/3] FROM docker.io/library/openjdk:8u181-jdk-alpine3.8@sha25 0.0s
=> [runner 2/3] RUN addgroup -S -g 1000 app && adduser -D -S -G app -u 1000 2.6s
=> [builder 2/3] ADD . /work 0.5s
=> [builder 3/3] RUN --mount=type=cache,target=/root/.m2 ./mvnw clean insta 17.6s
=> [runner 3/3] COPY --from=builder --chown=app:app /work/target/demo-0.0.1-SNAP 1.6s
=> exporting to image 0.1s
=> => exporting layers 0.1s
=> => writing image sha256:56669cdad5c5798018d9faa7598b9a63d93927b61f8b97c679c03 0.0s
=> => naming to docker.io/library/demo 0.0s
.m2のキャッシュが効いているようで、24秒でビルドできました。
おまけ
キャッシュ削除
キャッシュの削除方法は INTRODUCING DOCKER ENGINE 18.09 に書かれていました。
Build cache pruning and configurable garbage collection: Build cache can be managed separately from images and cleaned up with a new command
docker builder prune
. You can also set policies around when to clear build caches.
docker builder prune
という新しいコマンドで削除できるとのことです。2
実際に削除してみました。
$ docker builder prune
WARNING! This will remove all dangling build cache. Are you sure you want to continue? [y/N] y
Deleted build cache objects:
uz11u06qximcevxlom5o0oqjr
81lx4qivrfshyovit8f0tjcfk
mgin0p02pfn3o43rvkjo4ivz1
mh2e2399h35v8nzp2u3ifzmc3
ltd1751lktom5ns0zqkgrog8n
bhvsubwkwkz11gysean5ylce1
i3fpig7npayhyh5t7gkfvtooe
abp0xlrlny8prxyl9bs90durc
sha256:0a3e38ed053dfc9a81a4f7cc87c7b1588c436663f726f013937491ab42fc2931
Total reclaimed space: 179.2MB
ビルド3回目
$ docker build --no-cache -t demo .
[+] Building 143.7s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 557B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.0-experimental 0.8s
=> CACHED docker-image://docker.io/docker/dockerfile:1.0-experimental@sha256:d2d 0.0s
=> [internal] load metadata for docker.io/library/openjdk:8u181-jre-alpine3.8 1.8s
=> [internal] load metadata for docker.io/library/openjdk:8u181-jdk-alpine3.8 1.2s
=> CACHED [runner 1/3] FROM docker.io/library/openjdk:8u181-jre-alpine3.8@sha256 0.0s
=> CACHED [internal] helper image for file operations 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 67.98kB 0.0s
=> [builder 1/3] FROM docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e 10.1s
=> => resolve docker.io/library/openjdk:8u181-jdk-alpine3.8@sha256:b18e45570b6f5 0.0s
=> => sha256:ef87ded15917facc562d84e802d02ded848ae802cc0419440 70.61MB / 70.61MB 7.6s
=> => sha256:b18e45570b6f59bf80c15c78d7f0daff1e18e9c19069c323613 2.03kB / 2.03kB 0.0s
=> => sha256:3ef5b11bf99dc3829740e59e17dfaa491aa745816828286c6e0bf53 947B / 947B 0.0s
=> => sha256:97bc1352afdef16cbb71b5fe50c605e3174ba40c721ce9e0da3 3.40kB / 3.40kB 0.0s
=> => extracting sha256:ef87ded15917facc562d84e802d02ded848ae802cc04194400ba4cb8 2.2s
=> [runner 2/3] RUN addgroup -S -g 1000 app && adduser -D -S -G app -u 1000 2.8s
=> [builder 2/3] ADD . /work 0.5s
=> [builder 3/3] RUN --mount=type=cache,target=/root/.m2 ./mvnw clean inst 126.6s
=> [runner 3/3] COPY --from=builder --chown=app:app /work/target/demo-0.0.1-SNAP 1.7s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:d832ff1987e91375bf080b69504233eafe717d2c46043a7179e9e 0.0s
=> => naming to docker.io/library/demo 0.0s
キャッシュを削除すると、143秒という1回目と同程度の時間がかかりました。
まとめ
Buildkitで --mount=type=cache
を使うことで、Multi-stage buildでパッケージマネージャのキャッシュを利用できました。
同様にして、Maven以外のキャッシュも使えるはずです。
補足
CircleCIなどCIのSaaSを利用する場合、CI側のキャッシュ設定も必要なので注意してください。
参考
- Dockerfile frontend experimental syntaxes
- Build Enhancements for Docker
- Docker 18.09 新機能
- JavaでもDockerでマルチステージビルド