Sometimes it can be useful to split the build process of a Docker image into multiple stages to keep the final image small and secure. This is called a multi-stage build.
If you have a multi-stage Dockerfile and you're working with build arguments in multiple stages, you have to be aware that they are only available in the stage where they are defined. This means that you can't use an argument defined in the first stage in the second stage. This can be tedious if you're working with default values for your build arguments. But there is a way to work around this limitation.
The Problem
Let's say you have a multi-stage Dockerfile for a static site that's built with Node.js and served by Nginx. You want to pass the version of the site to the build process and use it in the runner stage to set the version in the Nginx configuration.
# Build stage
FROM node:20 as build
RUN npm ci
ARG VERSION=latest
RUN npm run build
# Runner stage
FROM nginx:alpine as runner
COPY ./configure-nginx.sh /configure-nginx.sh
ARG VERSION=latest
RUN /configure-nginx.sh $VERSION
COPY /app/dist /usr/share/nginx/html
You're building the Docker image with the following command:
docker build --build-arg VERSION=1.0 -t my-static-site .
This way you have to maintain the version in two places in the Dockerfile. This can be error-prone and tedious if you have more build arguments to pass around and have more stages in your Dockerfile.
The Solution
To work around this limitation, you can declare the argument before the first stage and use it in the following stages by just redeclaring them. Docker then uses the value passed as a build argument or the default value defined in the outer scope. This way you can maintain all build arguments and their default values in one place in the Dockerfile.
ARG VERSION=latest
# Build stage
FROM node:20 as build
RUN npm ci
ARG VERSION
RUN npm run build
# Runner stage
FROM nginx:alpine as runner
COPY ./configure-nginx.sh /configure-nginx.sh
ARG VERSION
RUN /configure-nginx.sh $VERSION
COPY /app/dist /usr/share/nginx/html
You build the Docker image in the same way as before:
docker build --build-arg VERSION=1.0 -t my-static-site .
Summary
- Build arguments are only available in the stage where they are defined.
- Declare ARGs in the outer scope of the Dockerfile by defining them before the first stage and redeclaring them in the stages where you want to use them. This way you can maintain all build arguments and their default values in one place in the Dockerfile.
- If you have secrets to pass to your Docker image, you should use secrets instead of ARGs to avoid exposing sensitive data in the final image.
Notes on using ARGs for secrets
If you use secrets for building your Docker image, you should consider using build secrets instead of build arguments. The values of build arguments are visible in the history of the Docker image. So, if you pass sensitive data or environment variables as build arguments, they can be exposed in the final image. Build secrets solve this problem by ensuring the values are not exposed in the history of the Docker image and are the best practice for passing sensitive data to your Docker image. You ca read more about them in the Docker documentation.
I hope this short guide helps you with keeping your Dockerfile cleaner and more maintainable when using the same arguments across multiple stages.
If you found this post helpful, please share it with others. It's the best thanks I can ask for and it gives me the motivation to write more posts like this.
If you have any questions or feedback, please reach out to me on any channel. I'm always happy to help you with your questions and learn from your feedback.