Docker ARGs

Today I realised that there is more depth to the ARG docker command than I had anticipated. Especially its scope in the context of multi-stage builds is non-trivial. Take the following (highly convoluted) Dockerfile for example:

ARG VERSION=latest

FROM alpine:$VERSION as build
ARG VERSION=3.16
RUN echo $VERSION > image_version

FROM alpine:$VERSION
COPY --from=build /image_version /other_image_version
ARG VERSION
RUN echo $VERSION > image_version
CMD cat other_image_version && cat image_version

The first stage will use alpine:latest as base image. VERSION then gets overwritten to 3.16 and this value is saved to file image_version.

Which alpine tag will the second stage use? Because ARG commands within a stage are local to that stage, the ARG VERSION=3.16 line has no effect on this FROM statement. Instead, the previous value latest will be used.

This second stage retrieves the version file from the first stage and also creates its own image_version file. Which value will be written to this file? We defined an argument VERSION without value in this stage, but that does not mean that the argument will be empty. Rather, this line allows the ARG VERSION=latest statement on line 1 to enter the scope of the second stage. As a consequence, the value latest will be written to the image_version file. If we had skipped the ARG VERSION line, $VERSION would have been undefined and the image_version file would have been empty.

Let's confirm our theory:

$ docker build -t argtest .
[+] Building 1.0s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                        0.0s
 => => transferring dockerfile: 320B                                        0.0s
 => [internal] load .dockerignore                                           0.0s
 => => transferring context: 2B                                             0.0s
 => [internal] load metadata for docker.io/library/alpine:latest            0.0s
 => [build 1/2] FROM docker.io/library/alpine:latest                        0.0s
 => [build 2/2] RUN echo 3.16 > image_version                               0.3s
 => [stage-1 2/3] COPY --from=build /image_version /other_image_version     0.0s
 => [stage-1 3/3] RUN echo latest > image_version                           0.5s
 => exporting to image                                                      0.0s
 => => exporting layers                                                     0.0s
 => => writing image sha256:667654e8ba78edf07a9d64a3fb7576fdfb6a4be421b...  0.0s
 => => naming to docker.io/library/argtest                                  0.0s

$ docker run --rm argtest
3.16
latest

When we manually specify a value for the build argument, it is applied everywhere:

$ docker build -t argtest --build-arg VERSION=edge .
$ docker run --rm argtest
edge
edge

Fun fact

ARGs are not secret. The values passed during docker build can be retrieved from an image with the docker history command. Do not use ARGs to pass sensitive information such as passwords. Use RUN --mount=type=secret instead.

Summary

  • ARGs defined before the first stage
    • can only be used in FROM statements
    • can be imported in the scope of a stage by re-declaring them without value
  • ARGs defined within a stage
    • are scoped to the subsequent lines of that stage
    • shadow any outside ARGs with the same name
  • ARGs are unsuited for secrets

References