I thought this would help dispell confusion with multi-stage docker builds and how environment variables and build-arguments function with them.
This example file is similar to many multi-stage builds.
FROM alpine:3.16.0 as builder
ARG BUILDTIME_VAR
ENV RUNTIME_VAR "default_runtime_value"
RUN echo "builder-stage RUNTIME_VAR=$RUNTIME_VAR BUILDTIME_VAR=$BUILDTIME_VAR" > /builder_results.txt
FROM alpine:3.16.0 as application
RUN echo "application-stage RUNTIME_VAR=$RUNTIME_VAR BUILDTIME_VAR=$BUILDTIME_VAR" > application_results.txt
COPY --from=builder /builder_results.txt /.
CMD ["cat", "builder_results.txt", "application_results.txt"]
The idea is that you have a builder stage and a application stage that uses the builder stage. None of the context between these stages is shared except for what you copy between them... i.e.
COPY --from=builder /builder_results.txt /.
If you build the Dockerfile and then run it like so:
docker build --build-arg BUILDTIME_VAR=buildtime_value -t "build-arg-env-test:1.0.1" --target=application .
#... prints stuff
docker run --rm build-arg-env-test:1.0.1
The output is this:
builder-stage RUNTIME_VAR=default_runtime_value BUILDTIME_VAR=buildtime_value
application-stage RUNTIME_VAR= BUILDTIME_VAR=
This was the result of the cat command:
CMD ["cat", "builder_results.txt", "application_results.txt"]
This demonstrates that the environment variables and build args set in the builder stage do not propagate to the application stage.
If you check the history in the image layers for the application stage, you get this:
$ docker history build-arg-env-test:1.0.1
IMAGE CREATED CREATED BY SIZE COMMENT
ce05655008d4 4 minutes ago CMD ["cat" "builder_results.txt" "applicatio… 0B buildkit.dockerfile.v0
<missing> 4 minutes ago COPY /builder_results.txt /. # buildkit 78B buildkit.dockerfile.v0
<missing> 4 minutes ago RUN /bin/sh -c echo "application-stage RUNTI… 46B buildkit.dockerfile.v0
<missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 7 weeks ago /bin/sh -c #(nop) ADD file:3ae36c6c4a1fc4315… 5.27MB
The "7 weeks ago" layers are from the alpine base image. You can see from the output that none of the builder stage layers exist in the final application image.
If you want to look at just the builder image, you can run this:
docker build --build-arg BUILDTIME_VAR=buildtime_value -t "build-arg-env-buildertest:1.0.1" --target=builder .
Inspecting it shows the dangers of potentially providing secrets to an image and demonstrates why copying the output of one stage to another stage can help circumvent leaking secrets.
$ docker history build-arg-env-buildertest:1.0.1
IMAGE CREATED CREATED BY SIZE COMMENT
bb9a3fd4d72e 8 minutes ago RUN |1 BUILDTIME_VAR=buildtime_value /bin/sh… 78B buildkit.dockerfile.v0
<missing> 8 minutes ago ENV RUNTIME_VAR=default_runtime_value 0B buildkit.dockerfile.v0
<missing> 8 minutes ago ARG BUILDTIME_VAR 0B buildkit.dockerfile.v0
<missing> 7 weeks ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 7 weeks ago /bin/sh -c #(nop) ADD file:3ae36c6c4a1fc4315… 5.27MB
Specifically, any build-args provided to the image are stored inside the image layer, see this line:
bb9a3fd4d72e 8 minutes ago RUN |1 BUILDTIME_VAR=buildtime_value /bin/sh… 78B buildkit.dockerfile.v0
Everything is saved into the builder-stage image. This is why if we wanted to provide secret values that we didn't want to store in the final application image, we would create a builder stage and copy it into a new application stage.
--build-arg / ARG
are only available at build-time. Docker allows you to use ENV
in docker files to specify default values for
environment variables in your image, but these are not necessary if you don't use these values at build time as is done in the example build stage.
FROM alpine:3.16.0 as builder
ARG BUILDTIME_VAR
ENV RUNTIME_VAR "default_runtime_value" # <--- default value
RUN echo "builder-stage RUNTIME_VAR=$RUNTIME_VAR BUILDTIME_VAR=$BUILDTIME_VAR" > /builder_results.txt
If you don't need a default value for the build stage, you can just specify the value you want when running the container if preferred.
$ docker run --rm -it --env RUNTIME_VAR=my_non_default_value alpine:3.16.0
/ # echo $RUNTIME_VAR
my_non_default_value # <-- printed value from custom env variable passed to base alpine image
If you need to use secrets, but don't want to use multi-stage builds, you can also use BUILDKIT and docker secrets to accomplish similar results.
Read more here.