Docker provides several options for configuring your builds and runtime containers. In this article, I'll try to help you understand the use cases and footguns for every available method.
First, let's look at some common mistakes that developers run into.
Common mistakes
- Docker build images cannot read environment variables. That's right, running
NEXT_PUBLIC_DOMAIN=foo.com docker build .
is not going to work even if you have anENV NEXT_PUBLIC_DOMAIN
instruction in your Dockerfile. - Do not store sensitive data in
ENV
instructions. To illustrate the problem, build a Dockerfile with anENV TOKEN=xyz
instruction, then rundocker inspect [image]
. You will be able to view your token in plain text. Note that I'm only referring to build environment variables, not runtime environment variables i.e.docker run -e TOKEN
. - Do not use Docker build arguments i.e.
ARG token
for passing secrets to a build. Build arguments are embedded in the image. An attacker can in some cases rundocker history
to see your secrets if you do this. - Do not
COPY
a.env
file or any other file with credentials into your build. No matter what you do, Docker layer caching will leak the contents of these files.COPY
instructions should never be used with sensitive data.
Now that we know what not to do, let's look at how to do things the right way.
The right way to configure with Docker
Build arguments ARG
Docker Build Arguments are useful for configuring builds when the configuration values you're passing to the build are not secrets. Examples for this include image versions, build and start commands, and public environment variables required by your build command (e.g. PUBLIC_DOMAIN
). FlexStack makes extensive use of build arguments in order to make our autogenerated Dockerfiles configurable.
ARG
instructions can be read by other instructions in the Dockerfile, but cannot be read from within containers themselves. Let's look at some examples.
Configuring images
In this example, we're setting two build arguments: one for the version and one for the builder image. We are using default values for each, but these are optional. You can omit default values if you want Docker to complain if they are not provided.
To override the default values during a build, you can add them to the build command with the --build-arg
option.
Adding environment variables to build commands
You can leverage build arguments to add configurations to your build commands (for example next build
) by forwarding their values to build-time environment variables.
To override the default argument values, run:
Build secrets --secret
Sometimes our build steps need to utilize secrets in order to perform tasks like installing private packages during CI/CD. For these use cases, Docker has provided us with Build Secrets.
Build secrets rely on SSH mounts in order to prevent secrets from being leaked through image layers or the final image.
Mount a secret from the host environment
Let's assume we are building an image in a CI/CD workflow, such as a GitHub Action. GitHub Actions allow users to inject secrets into run commands via environment variables. Getting these secrets into our Dockerfile securely is pretty straightforward.
Our workflow could look like:
And in our Dockerfile:
Mount a .env
file with credentials
Next, let's consider a case where you're building an image locally. You've got a .env
file that contains some credentials and you'd like to get the file into your container build securely, so that some build command can read the contents of the file. With Docker, we can configure source files for secrets and change where Docker mounts them within the container.
In the Dockerfile below we can see that the secret is being mounted to the target /build/.env
. We are making an assumption that the build command knows this and will read the contents of the file from that mount target.
And in our build command we can set a source for the file:
Runtime environment variables -e
As engineers, we want to be able to securely pass database credentials, service endpoints, ports, and other configuration to our applications at runtime. That is, when we start our application and start using it. In this case, we can finally rely on environment variables to do the work for us, as Docker allows us to set environment variables in its docker run
command.