Skip to content

Instantly share code, notes, and snippets.

@hardboiled
Last active July 12, 2022 13:27
Show Gist options
  • Save hardboiled/386926bea3f5364994e9a259da8f948b to your computer and use it in GitHub Desktop.
Save hardboiled/386926bea3f5364994e9a259da8f948b to your computer and use it in GitHub Desktop.
Description of what occurs with multi-stage docker builds

Multi-stage docker builds

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 /.

Building and Running

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.

History in image layers

application

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.

builder

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 args vs Env vars

--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

Other notes

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment