Docker build secrets are a pain. You need them to pull private repos, authenticate to registries, or run commands that require sensitive credentials. But the moment you COPY or ADD a secrets file into your image, they’re there forever, baked into every layer, visible to anyone who can inspect your image. This is a massive security hole.

The classic mistake is to COPY a secrets.txt file into the image and then RUN a command that uses it.

# BAD EXAMPLE
FROM ubuntu
COPY secrets.txt /app/
RUN /app/my_script_that_uses_secrets.sh

docker history <your-image> will show you the COPY command and, if you dig deep enough, you can often extract secrets.txt from the image’s filesystem history.

The modern, and correct, way to handle this is with Docker BuildKit’s secret mounting feature. It’s designed specifically to inject secrets into the build process without ever writing them to the image filesystem.

Here’s how it works: you create a secret, usually from a file, and then mount it into a specific build stage.

Let’s say you have a secrets.json file with your registry credentials:

{
  "username": "myuser",
  "password": "mypassword123"
}

In your Dockerfile, you’ll use the RUN --mount=type=secret syntax.

# GOOD EXAMPLE
FROM ubuntu as builder
RUN --mount=type=secret,id=mysecret \
    echo "Importing secrets..." && \
    cat /run/secrets/mysecret > /app/secrets.json && \
    echo "Secrets copied to image layer (temporarily)." && \
    echo "Using secrets to pull private repo..." && \
    # Example: docker login using secrets
    docker login -u myuser -p $(cat /run/secrets/mysecret | jq -r '.password') \
    https://myregistry.example.com --username $(cat /run/secrets/mysecret | jq -r '.username') && \
    # Now pull your private image
    docker pull myregistry.example.com/my/private-image:latest && \
    echo "Cleanup secrets from builder stage..." && \
    rm /app/secrets.json

When you build this, you’ll pass the secret like so:

DOCKER_BUILDKIT=1 docker build \
  --secret id=mysecret,src=secrets.json \
  -t myimage .

The DOCKER_BUILDKIT=1 environment variable is crucial. It tells Docker to use BuildKit, which supports this feature. The --secret id=mysecret,src=secrets.json flag tells Docker to take the file secrets.json from your local filesystem and make it available inside the build container under the ID mysecret.

Inside the RUN command, --mount=type=secret,id=mysecret mounts that secret. It appears in the build container at /run/secrets/mysecret. This is a special, ephemeral location. Once the RUN command finishes, the secret is gone from the build container’s filesystem. Crucially, it’s never written to a Docker image layer.

The cat /run/secrets/mysecret > /app/secrets.json line does write the secret to a file within the current build stage’s filesystem. This is a common point of confusion. However, this file is only present in the builder stage. If you were to COPY artifacts from this builder stage to a final stage, you would ensure that this temporary secrets.json file is not copied. The example shows this cleanup (rm /app/secrets.json).

The real magic is that even if you didn’t do the rm and COPY it to a later stage, the secret itself is only ever accessible in memory or as a temporary file within the ephemeral build container. It’s not stored in the image’s history or filesystem layers.

You can mount multiple secrets:

RUN --mount=type=secret,id=ssh-key,target=/root/.ssh/id_rsa \
    --mount=type=secret,id=api-token,target=/etc/api/token \
    apt-get update && apt-get install -y openssh-client && \
    ssh -i /root/.ssh/id_rsa git@github.com && \
    curl -H "Authorization: Bearer $(cat /etc/api/token)" https://api.example.com/data

And build it with:

DOCKER_BUILDKIT=1 docker build \
  --secret id=ssh-key,src=/path/to/your/ssh/private_key \
  --secret id=api-token,src=/path/to/your/api_token_file \
  -t myapp .

The target option in --mount=type=secret lets you specify where the secret should appear inside the build container. If target is omitted, it defaults to /run/secrets/<id>.

A common pitfall is forgetting DOCKER_BUILDKIT=1. Without it, your build will fail with an unknown flag error. Another is trying to access the secret from a different build stage that doesn’t have the secret mounted, or attempting to access it after the RUN command that mounted it has completed. BuildKit’s secret mounts are ephemeral to the RUN instruction.

You can also use secrets for SSH keys to clone private Git repositories directly within your build.

# Dockerfile for cloning private repo
FROM ubuntu as downloader
RUN apt-get update && apt-get install -y git openssh-client

RUN --mount=type=ssh \
    echo "Cloning private repository..." && \
    git clone git@github.com:myuser/my-private-repo.git /app/repo

FROM ubuntu
COPY --from=downloader /app/repo /app/

And build it with:

DOCKER_BUILDKIT=1 docker build \
  --ssh default \
  -t myapp .

This uses --mount=type=ssh which leverages your local SSH agent. The default value for --ssh tells BuildKit to use the SSH agent socket if available.

The most surprising thing is how these secrets are handled at a network level. When you use --mount=type=ssh, BuildKit actually forwards your SSH agent’s connection to the build container. It’s not just copying a key file; it’s enabling a secure, authenticated channel that the build container can use to talk to your SSH agent directly, without the agent’s private keys ever leaving your host machine.

The next hurdle you’ll face is understanding how to manage secrets that need to be available across multiple RUN commands within the same build stage.

Want structured learning?

Take the full Docker course →