Docker Secrets
Docker secrets are a safer way to give containers sensitive values such as passwords, tokens, certificates, and private keys. Instead of baking a secret into an image or passing it as an environment variable, Docker mounts it as a file that the application can read at runtime.
Secrets matter because container images are copied, cached, pushed to registries, inspected, and reused. A secret that enters an image layer or command history can be difficult to remove completely.
Overview: How Docker Secrets Work
A Docker image is a read-only template made from stacked filesystem layers plus metadata. A container is an instance of that image with a thin writable layer, mounts, networking, runtime configuration, and a running process. Secrets should not live in the image layers, because layers are content-addressed and may be cached locally, pushed to a registry, or visible through image history.
Docker has three commonly encountered secret mechanisms. First, Docker Compose can define secrets from local files and mount them into service containers. This is useful for local development and single-host Compose projects. Second, Docker Swarm has built-in secrets managed by the Docker daemon and made available only to services that request them. Third, BuildKit build secrets let a Dockerfile read a secret during a single RUN instruction without copying it into the final image.
In a running container, secrets are normally exposed as files under /run/secrets/. For example, a secret named db_password is usually available at /run/secrets/db_password. Applications should read the file content, trim a trailing newline if their configuration format requires it, and avoid printing the value.
Docker secrets are not the same as environment variables. Environment variables are convenient, but they can be exposed through process inspection, crash reports, application diagnostics, or docker inspect access depending on the platform and permissions. A mounted secret file is still readable by processes with access to that file inside the container, so it is not magic encryption inside your app. It is a narrower and cleaner delivery path.
On Docker Desktop, containers run inside a Linux virtual machine. On Linux Docker Engine, they run directly on the Linux host using namespaces and cgroups. In both cases, the same practical rule applies: only users who can control the Docker daemon should be trusted like administrators, because Docker access can usually lead to broad host access.
Syntax
docker secret create SECRET_NAME FILE
docker service create --name SERVICE_NAME --secret SECRET_NAME IMAGE[:TAG] COMMAND
docker build --secret id=SECRET_ID,src=FILE -t IMAGE:TAG .
services:
SERVICE_NAME:
image: IMAGE:TAG
secrets:
- SECRET_NAME
secrets:
SECRET_NAME:
file: ./secret-file.txt
| Form | Where it is used | Purpose |
|---|---|---|
docker secret create |
Swarm | Stores a secret in the Swarm secret store from a file or standard input. |
--secret SECRET_NAME |
Swarm service | Grants a service access to an existing secret, mounted under /run/secrets/. |
secrets: |
Compose | Declares which secrets a service receives. |
file: |
Compose top-level secret | Loads the secret value from a local file on the machine running Compose. |
docker build --secret |
BuildKit build | Passes a secret to selected Dockerfile RUN instructions. |
RUN --mount=type=secret |
Dockerfile with BuildKit | Temporarily mounts the secret for one build step without saving it in a layer. |
Use obvious placeholders in examples and documentation, such as <YOUR_API_TOKEN> or changeme. In real projects, keep secret files out of Git and load production secrets from the deployment platform, CI system, or orchestrator.
Examples
Example 1: Use a Secret with Docker Compose
mkdir -p secrets
printf 'changeme\n' > secrets/db_password.txt
Create this Compose file:
services:
app:
image: alpine:3.20
command: sh -c 'printf "password length: %s\n" "$(wc -c < /run/secrets/db_password)"'
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Run the service:
docker compose up --abort-on-container-exit
Output:
app-1 | password length: 9
app-1 exited with code 0
Compose reads the local file and mounts it into the container at /run/secrets/db_password. The example prints only the length, not the secret. The count is 9 because printf 'changeme\n' writes eight visible characters plus a newline.
Example 2: Use a Secret in a Swarm Service
printf 'changeme\n' | docker secret create api_token -
docker service create --name secret-reader --secret api_token alpine:3.20 sh -c 'test -s /run/secrets/api_token && echo secret-mounted'
docker service logs secret-reader
Output:
secret-mounted
This example assumes Swarm mode is already enabled with docker swarm init. The secret is stored in Docker’s Swarm secret store, and only services that request api_token receive it. A regular docker run container does not automatically get Swarm secrets; Swarm secrets attach to services.
Example 3: Use a Build Secret Without Baking It Into the Image
printf '<YOUR_NPM_TOKEN>\n' > npm_token.txt
Use this Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN --mount=type=secret,id=npm_token \
sh -c 'test -s /run/secrets/npm_token && echo "secret was available during this RUN" > /message.txt'
CMD ["sh", "-c", "cat /message.txt"]
Build and run it:
docker build --secret id=npm_token,src=npm_token.txt -t secret-build-demo:1.0 .
docker run --rm secret-build-demo:1.0
Output:
secret was available during this RUN
The secret file exists only during that specific build step. The resulting image contains /message.txt, but it does not contain npm_token.txt or the mounted secret file. This is the right pattern for private package registry tokens, license keys needed during compilation, or credentials used to download private dependencies.
How It Works Step by Step
- The Docker client sends a container, service, Compose, or build request to the Docker daemon or BuildKit builder.
- The image layers are pulled or reused from the local cache. Secret content is not part of those image layers.
- For a runtime secret, Docker creates a mount inside the container, normally under
/run/secrets/SECRET_NAME. The application reads it like a normal file. - For a Swarm service, the service specification lists which secrets the task may access. Docker schedules the task and makes only those selected secrets available to that task.
- For a BuildKit secret, the builder mounts the secret for one
RUNinstruction. When that instruction completes, the mount disappears before the layer is committed. - If the container stops, the secret mount goes away with the container. The image remains reusable and does not gain the secret.
This file-based model also affects application design. Many official images support _FILE environment variables such as MYSQL_PASSWORD_FILE or POSTGRES_PASSWORD_FILE, letting you point the application at a secret file instead of placing the secret value directly in the environment. For your own apps, adding support for config values loaded from files is a small change that makes deployments safer.
Common Mistakes
Baking Secrets into Image Layers
FROM alpine:3.20
ENV API_TOKEN=<YOUR_API_TOKEN>
RUN echo "configured" > /status.txt
This is wrong because ENV stores the value in image metadata. A later RUN unset API_TOKEN or deleting a file does not reliably erase data from an earlier layer or from build history. Pass secrets at runtime or with BuildKit secret mounts instead.
Passing Secrets on the Command Line
docker run --rm -e "API_TOKEN=<YOUR_API_TOKEN>" alpine:3.20 env
This is better than baking the token into an image, but it can still expose the value in shell history, process listings, logs, or inspection output. Use a mounted secret file when the value is sensitive and the application can read files.
Expecting docker run to Read Swarm Secrets
docker secret create db_password secrets/db_password.txt
docker run --rm --name app alpine:3.20 cat /run/secrets/db_password
This does not work as intended. Swarm secrets are attached to Swarm services, not arbitrary docker run containers. Use docker service create --secret in Swarm, or use Compose secrets for a Compose-managed local service.
Printing Secrets While Debugging
docker compose exec app cat /run/secrets/db_password
A quick debug command can leak a secret into terminal scrollback, CI logs, screen recordings, or shared support transcripts. Prefer checks that prove the secret exists without printing it, such as test -s /run/secrets/db_password.
Best Practices
- Keep secrets out of Dockerfiles, image layers, labels, build arguments, committed env files, and application logs.
- Use BuildKit
--secretfor build-time credentials instead ofARGorENV. - Read runtime secrets from files under
/run/secrets/, and add application support for file-based configuration where possible. - Grant each service only the secrets it needs. Do not mount one shared file full of every production credential.
- Use specific image tags such as
alpine:3.20;latestis a moving target and makes secret-related troubleshooting less reproducible. - Add local secret files, such as
secrets/*.txt, to.gitignore. - Rotate a secret if it was printed, committed, copied into an image, or exposed in CI output.
- Prefer your production orchestrator’s secret store for production deployments, such as Swarm secrets, Kubernetes Secrets with appropriate controls, or a cloud secret manager.
- Do not rely on Docker access as a low-privilege boundary. Anyone who can control the Docker daemon should be treated as highly privileged.
Practice Exercises
- Create a Compose service based on
alpine:3.20that receives a secret namedapp_key. Expected end state: the service printskey presentwhen/run/secrets/app_keyis non-empty, without printing the value. - Write a Dockerfile that uses a BuildKit secret named
license_keyduring oneRUNstep. Hint: the final image should prove the step ran, but should not contain the secret file. - In a Swarm-enabled test environment, create two services and one secret. Give the secret to only one service. Expected end state: one service can read
/run/secrets/SECRET_NAME, and the other cannot.
Summary
- Docker secrets deliver sensitive values as mounted files instead of image content or ordinary environment variables.
- Compose secrets are convenient for Compose-managed services; Swarm secrets are attached to Swarm services.
- BuildKit build secrets are temporary mounts for selected Dockerfile
RUNinstructions. - Secrets should be read, not printed. Debug with existence and length checks instead of dumping values.
- Never bake secrets into Dockerfile
ENV,ARG, copied files, or image layers. - Use narrow access, specific image tags, ignored local secret files, and rotation when exposure happens.
