docker cache invalidation and the ARG command
About
We ran into a caching issue on a docker build that was triggerd by a really easy to make mistake in your Dockerfile… this seemed like as good a place as any to start with a new blog.
Setup
Given a simple Dockerfile like this:
FROM alpine:latest
ARG CACHE_BUSTER
WORKDIR /app
RUN echo "Slow step..." && sleep 5
RUN echo "Fast step"
ENV CACHE_BUSTER=${CACHE_BUSTER}
RUN echo "Updated Cache Buster ${CACHE_BUSTER}"
It might be natural to expect that steps 1-3 would not be invalidated if
CACHE_BUSTER is updated. However, here’s the reality … the ARG
CACHE_BUSTER line invalidates every step after it any time the CACHE_BUSTER
argument is changed.
In the first build we prep the cache:
$ BUILDKIT_PROGRESS=plain \
DOCKER_BUILDKIT=1 \
docker buildx build \
-f Dockerfile.simple \
--build-arg CACHE_BUSTER=0 .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile.simple
#1 transferring dockerfile: 233B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/alpine:latest
#2 DONE 0.3s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/5] FROM docker.io/library/alpine:latest@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
#4 DONE 0.0s
#5 [2/5] WORKDIR /app
#5 CACHED
#6 [3/5] RUN echo "Slow step..." && sleep 5
#6 0.335 Slow step...
#6 DONE 5.4s
#7 [4/5] RUN echo "Fast step"
#7 0.624 Fast step
#7 DONE 0.7s
#8 [5/5] RUN echo "Updated Cache Buster 0"
#8 0.476 Updated Cache Buster 0
#8 DONE 0.5s
#9 exporting to image
#9 exporting layers 0.1s done
#9 writing image sha256:8728d27baadfc85c7dd0858b9cf1b22f7803709d43d142b562d7bfeef94cc19e done
#9 DONE 0.1s
Now we re-build but we update the CACHE_BUSTER key…
$ BUILDKIT_PROGRESS=plain \
DOCKER_BUILDKIT=1 \
docker buildx build \
-f Dockerfile.simple \
--build-arg CACHE_BUSTER=100 .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile.simple
#1 transferring dockerfile: 233B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/alpine:latest
#2 DONE 0.5s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/5] FROM docker.io/library/alpine:latest@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
#4 DONE 0.0s
#5 [2/5] WORKDIR /app
#5 CACHED
#6 [3/5] RUN echo "Slow step..." && sleep 5
#6 0.243 Slow step...
#6 DONE 5.3s
#7 [4/5] RUN echo "Fast step"
#7 0.546 Fast step
#7 DONE 0.6s
#8 [5/5] RUN echo "Updated Cache Buster 100"
#8 0.649 Updated Cache Buster 100
#8 DONE 0.7s
#9 exporting to image
#9 exporting layers 0.1s done
#9 writing image sha256:fe4d3903b58dc81b6668a6275884ad5ec96348556a4c3b695490595e815c5774 done
#9 DONE 0.1s
We can see that the two RUN echo ... steps were re-run even though we didn’t expect them to be. Why?
The Fix - Move the ARG
The ARG command is changing its value, which causes the rest of the steps in the build to be re-calculated. The fix is to move the ARG statement to right before the ENV statement where it’s used:
FROM alpine:latest
WORKDIR /app
RUN echo "Slow step..." && sleep 5
RUN echo "Fast step"
ARG CACHE_BUSTER
ENV CACHE_BUSTER=${CACHE_BUSTER}
RUN echo "Updated Cache Buster ${CACHE_BUSTER}"
Now the first cache build works like this:
$ BUILDKIT_PROGRESS=plain DOCKER_BUILDKIT=1 docker buildx build -f Dockerfile.simple --build-arg CACHE_BUSTER=0 .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile.simple
#1 transferring dockerfile: 235B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/alpine:latest
#2 DONE 0.3s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/5] FROM docker.io/library/alpine:latest@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
#4 DONE 0.0s
#5 [2/5] WORKDIR /app
#5 CACHED
#6 [3/5] RUN echo "Slow step..." && sleep 5
#6 0.293 Slow step...
#6 DONE 5.3s
#7 [4/5] RUN echo "Fast step"
#7 0.621 Fast step
#7 DONE 0.6s
#8 [5/5] RUN echo "Updated Cache Buster 0"
#8 0.602 Updated Cache Buster 0
#8 DONE 0.6s
#9 exporting to image
#9 exporting layers 0.1s done
#9 writing image sha256:d4b922a88bf6d9a3e7beb097d4c00c8c8173c2af22c3a8ce44c937adf773b28f done
#9 DONE 0.1s
and a followup build with a new CACHE_BUSTER key only recalculates the remaining steps after the ARG command:
$ BUILDKIT_PROGRESS=plain DOCKER_BUILDKIT=1 docker buildx build -f Dockerfile.simple --build-arg CACHE_BUSTER=100 .
#0 building with "default" instance using docker driver
#1 [internal] load build definition from Dockerfile.simple
#1 transferring dockerfile: 235B done
#1 DONE 0.0s
#2 [internal] load metadata for docker.io/library/alpine:latest
#2 DONE 0.3s
#3 [internal] load .dockerignore
#3 transferring context: 2B done
#3 DONE 0.0s
#4 [1/5] FROM docker.io/library/alpine:latest@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715
#4 DONE 0.0s
#5 [2/5] WORKDIR /app
#5 CACHED
#6 [3/5] RUN echo "Slow step..." && sleep 5
#6 CACHED
#7 [4/5] RUN echo "Fast step"
#7 CACHED
#8 [5/5] RUN echo "Updated Cache Buster 100"
#8 0.450 Updated Cache Buster 100
#8 DONE 0.5s
#9 exporting to image
#9 exporting layers 0.0s done
#9 writing image sha256:3f42832a297ad4d4bbd9fa1586c3e1c81b0055fd7fd207a71a49f5c9a3fe8db2 done
#9 DONE 0.0s