Baeldung Pro – Ops – NPI EA (cat = Baeldung on Ops)
announcement - icon

It's finally here:

>> The Road to Membership and Baeldung Pro.

Going into ads, no-ads reading, and bit about how Baeldung works if you're curious :)

Partner – Orkes – NPI EA (cat=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Overview

Handling secrets securely during the building of Docker images is crucial. It’s not advisable to hardcode secrets, such as database credentials, tokens, and API keys into Dockerfiles, or embed them in the resulting image.

One common approach to introducing secrets during the build process involves using files. In our case, we’ll explore a less common approach, which involves the introduction of secrets directly from environment variables.

In this tutorial, we’ll explore how to securely and effectively add a secret to a Docker build via environment variable.

2. The Challenge of Managing Secrets in Docker Builds

During the build process, the docker build command doesn’t provide a straightforward way to pass sensitive information without risking exposure. Therefore, it can become a challenge to manage secrets in Docker builds.

Firstly, embedding secrets in the image makes them accessible to anyone with access to the image, which violates best practices.

Secondly, using build arguments (–build-arg) is convenient, but not secure, since they’re stored in the image metadata.

Additionally, using temporary files can be a complicated approach. For instance, if these files are not properly removed after they’re used, they may remain on the system and expose sensitive information to unauthorized access.

To manage secrets more securely, we can leverage the –secret flag (introduced in Docker 18.09) with BuildKit.

3. Passing Secrets From Environment Variables

Before we proceed, we need to verify the Docker version, because BuildKit and the –secret flag are only available in Docker versions 18.09 or later. Additionally, we need to enable BuildKit, since it’s not enabled by default in some Docker installations. We also need to ensure the Dockerfile utilizes a syntax that’s compatible with BuildKit.

Now, let’s delve into the steps for passing a secret from an environment variable.

3.1. Define the Secret as an Environment Variable

First, let’s store the secret in an environment variable:

$ export MY_SECRET="super-secret-value"

Here, we avoid hardcoding the secret directly into a Dockerfile or a script. Instead, we depend on an environment variable to store the secret, isolating it from the build configuration.

3.2. Create the Dockerfile

To demonstrate, let’s use a simple directory structure:

$ tree sample_project
sample_project
└── Dockerfile

0 directories, 1 file

Our working directory holds the name sample_project. Let’s edit the Dockerfile to contain:

# syntax=docker/dockerfile:1.4

FROM ubuntu:20.04

# Use the secret during the build process
RUN --mount=type=secret,id=my_secret cat /run/secrets/my_secret > /tmp/secret_output

CMD ["bash"]

Let’s analyze the content of the Dockerfile:

  • # syntax=docker/dockerfile:1.4 – specifies that the Dockerfile uses BuildKit syntax
  • RUN –mount=type=secret,id=my_secret – temporarily mounts the secret at /run/secrets/my_secret during the build process
  • /run/secrets/my_secret > /tmp/secret_output – reads the secret and saves it into a temporary file for demonstration

At this point, the Dockerfile setup is complete, and we can proceed to build the image.

3.3. Build the Image

In this step, we need to build the image using BuildKit and pass the secret securely using the –secret flag:

$ DOCKER_BUILDKIT=1 docker build \
  --secret id=my_secret,env=MY_SECRET \
  -t my_image .

Let’s break down the command:

  • DOCKER_BUILDKIT=1 – ensure that Docker uses BuildKit for the build process
  • –secret id=my_secret,env=MY_SECRET – instructs Docker to pass the secret stored in the environment variable MY_SECRET and avail it under the id my_secret
  • -t my_image – tags the image built as my_image

Additionally, the dot (.) sets our current directory (sample_project/) as the build context. Therefore, Docker uses this directory to find the Dockerfile.

3.4. Verify the Secret Was Used

In this final step, let’s verify if the secret was correctly used during the build:

$ docker run --rm my_image cat /tmp/secret_output && echo
super-secret-value

The above output proves everything worked correctly, since we can see the secret value printed in the console. To clarify, the && echo command introduces a newline after the output.

Let’s note that the secret isn’t stored in the final image; it’s only available during the build process.

4. Troubleshooting Common Issues

Although the process usually goes smoothly, we may occasionally encounter problems. Let’s look at a few of the most common issues and how to tackle them.

4.1. Secret Not Available During Build

If the secret is unavailable during the build, then Docker can’t access it while building the image.

Now, this issue may arise if BuildKit isn’t enabled. The –secret flag only works with BuildKit, a new and advanced Docker build system. As a consequence, if BuildKit isn’t enabled, Docker fails to recognize the –secret flag, and the secret isn’t passed in. So, we can ensure that we use DOCKER_BUILDKIT=1 before the build command to enable BuildKit.

We may also experience this issue when we store the secret in an environment variable after running the docker build command. So, we need to set the environment variable first and then confirm that it’s set:

$ echo $MY_SECRET
super-secret-value

If the output is empty, the build doesn’t receive the secret.

Also, an incorrect id value can cause this issue. In the build command defined in subsection 3.3, Docker takes the value from the environment variable MY_SECRET and assigns it the label my_secret. Meanwhile, in the Dockerfile defined in sub-section 3.2, we instruct Docker to use the secret with the label my_secret. Therefore, we need to ensure that the id in the Dockerfile matches the id in the build command.

4.2. Secret Accidentally Stored in Image

This issue occurs if the secret value is present in the final image.

For this, we can ensure that secrets are only utilized in intermediate build steps. To explain, this means using the secret only in temporary commands that don’t create a new image layer:

RUN echo "$MY_SECRET" > /app/config

So, we need to avoid a command like the one above, because it causes the secret to persist in the image.

Alternatively, we can ensure that secrets are only temporarily accessible and can’t be copied to later layers using –mount=type=secret.

Finally, we can verify that no build steps include commands that explicitly copy a secret file into the image:

COPY secret_file /app/config

Above, if secret_file contains sensitive information, it’s embedded in the image layers, making it retrievable.

5. Conclusion

In this article, we discussed adding a secret to docker build from an environment variable.

First, we defined the secret as an environment variable. Next, we created a Dockerfile and leveraged Docker BuildKit’s –secret flag to build the Docker image. From this, we managed to introduce our secret into the build process without exposing it in the final image. Additionally, we managed to troubleshoot a few common mistakes, ensuring that secrets are not mistakenly stored in the final image.

​​​​​It’s essential to pass a secret securely to docker build to maintain an application’s security. Hence, adding this concept to our Docker workflows can be useful.