Avatar
Mostly just a place where I dump interesting things I've learned that I'm pretty sure I'll forget.. It's my own personal knowledge base.

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

all tags