Skip to main content

Docker Secrets

Sensitive data (passwords, tokens, certificates) that must be available to containers at runtime without being stored in image layers, environment variables, or compose files in plaintext. Docker provides BuildKit secret mounts for build-time and file-based secrets for runtime.

Docker Secrets — Handling Credentials Without Leaking Them

What Are Docker Secrets in Simple Terms?

The most common Docker security mistake is putting a database password in a Dockerfile ENV instruction or in a docker-compose.yml file. Both store the secret in plaintext — one bakes it permanently into the image, the other commits it to version control. Docker Secrets provides the correct alternative.

Bash
WRONG — secret baked into image layer:
ENV DB_PASSWORD=supersecret
docker history my-image
# ENV DB_PASSWORD=supersecret <- visible to anyone
WRONG — secret in compose file:
environment:
DB_PASSWORD: supersecret <- committed to git
CORRECT — build-time secret:
RUN --mount=type=secret,id=db_pass ...
# not in any layer
CORRECT — runtime secret:
Pass via environment at docker run time from secret manager
# not in image, not in git

BuildKit Secret Mounts (Build-Time)

Dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
# Secret available only during this RUN, never in any layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm install --registry https://registry.npmjs.org
COPY . .
CMD ["node", "server.js"]
Bash
# Pass secret at build time
docker buildx build \
--secret id=npm_token,src=$HOME/.secrets/npm-token \
-t myapp:latest .
# Verify secret not in image
docker history myapp:latest
# No NPM_TOKEN visible anywhere

Runtime Secrets — Correct Patterns

Bash
# PATTERN 1: Environment variable from a file
# Store secret in a file, not hardcoded
echo "supersecret" > /run/secrets/db_password
docker run -d \
--env-file /run/secrets/env-file \
payment-api:latest
# PATTERN 2: Docker Compose secrets
# docker-compose.yml
services:
api:
secrets:
* db_password
# secret available at /run/secrets/db_password inside container
secrets:
db_password:
file: ./secrets/db_password.txt # dev only — file on host
# App reads: fs.readFileSync('/run/secrets/db_password').toString().trim()
# PATTERN 3: AWS Secrets Manager at runtime
# Application fetches secret on startup:
# const sm = new SecretsManagerClient()
# const secret = await sm.send(new GetSecretValueCommand({SecretId: 'prod/db'}))
# process.env.DB_PASSWORD = JSON.parse(secret.SecretString).password

What Never to Do

Dockerfile
# NEVER: secrets in ENV
ENV DB_PASSWORD=supersecret123
# NEVER: secrets in ARG
ARG GITHUB_TOKEN=ghp_secret
RUN git clone https://${GITHUB_TOKEN}@github.com/...
# NEVER: copying .env file into image
COPY .env . # <- ALL secrets now in image layer forever
Security

If you have accidentally baked a secret into a pushed image, rotate the secret immediately — before removing the image. Removing the image from the registry does not help users who already pulled it. The secret is still in their local Docker cache. Rotation first, cleanup second.

Frequently Asked Questions

Why is putting a secret in an ENV instruction in a Dockerfile considered unsafe even if the container itself is private?

Every instruction in a Dockerfile becomes a permanent, immutable layer in the image, and `ENV`-baked secrets remain visible in that layer forever — anyone who can pull the image (including from a registry) can extract it with `docker history` or by inspecting the layer filesystem directly, regardless of what the running container's environment looks like later. This is true even if you later `unset` the variable in a subsequent layer; the earlier layer with the secret still exists in the image.

What's the right way to handle secrets at build time versus runtime?

At build time, use BuildKit's `RUN --mount=type=secret` which mounts the secret into the build step's filesystem without ever writing it to a layer. At runtime, avoid plain environment variables where possible (they're visible via `docker inspect` and process listings) and prefer file-based secrets mounted from a secrets manager, Docker Swarm secrets, or Kubernetes Secrets — none of which are foolproof against a compromised host, but all of which avoid the much more common mistake of a secret ending up committed inside an image layer.