Docker Secrets: What Should Never Be in Your Image
AI generated
Docker · Security · Secrets Management · DevOps
Docker Secrets: What Should Never Be in Your Image
BuildKit, Swarm Secrets, and Secure Credential Flows

Passwords in Dockerfiles, API keys as ENV variables, certificates baked into image layers: these are real security holes in thousands of container setups. Docker Secrets, BuildKit mount secrets, and well-designed credentials management eliminate these risks without slowing down the development workflow.

12 min read BuildKit · Swarm Secrets · Vault · ENV Security Docker 24+ · BuildKit 0.11+

1. The Problem: Secrets in Images Are a Recurring Issue

It sounds obvious: passwords, API keys, and private certificates do not belong in Docker images. And yet Docker Secrets violations turn up regularly in security audits, because everyday development offers tempting shortcuts. A quick ENV DB_PASSWORD=supersecret in the Dockerfile, a COPY .env . for the build process, a hardcoded API key in a startup script: every one of these decisions leaves traces in the image layers, and those traces can be extracted with simple tools.

The risk isn't limited to deliberate misuse. Public registry pushes, Docker Hub leaks, compromised CI systems, or simply an image digest shared with the wrong person can all lead to credentials being extracted from images. The consequences range from database access to fully compromised cloud accounts. Using Docker Secrets correctly closes this entire class of attack structurally, not through policies that people forget to follow.

This article covers the full secrets lifecycle in Docker: how secrets end up in layers, how BuildKit and Swarm secrets prevent that structurally, how Compose works with Docker Secrets for local development, and when external secret stores like HashiCorp Vault are the right addition.

2. How Secrets End Up in Image Layers, and Why That's Dangerous

Docker images consist of read-only layers stacked on top of each other. Every instruction in the Dockerfile, whether RUN, COPY, or ENV, creates a new layer that freezes the state of the filesystem at that point in time. The problem: a later RUN rm /secrets/api.key instruction deletes the file from the topmost layer, but the underlying layer that contains the file is still there, and it is fully extractable with docker history, docker save, or tools like dive.

Even ENV variables passed via ARG SECRET_KEY in the Dockerfile and then --build-arg SECRET_KEY=$KEY remain visible in the layer history: docker history --no-trunc image:tag shows the complete build command, including every argument. Another path that's often overlooked: COPY --from=builder /root/.ssh /tmp/ssh in multi-stage builds, where a developer forgets that the final stage still contains the copied files. Docker Secrets as a concept address exactly this class of layer leak, because secrets are never persisted as a filesystem snapshot in the first place.

3. BuildKit Mount Secrets: Secrets in the Build Without a Layer Trace

BuildKit, enabled by default since Docker 23, introduces the --mount=type=secret directive in the Dockerfile. A secret is mounted as a temporary file in /run/secrets/ for the duration of a single RUN instruction, and only for that instruction, without ever appearing in the resulting layer. The file exists exclusively in the process namespace during that build step and leaves no trace in the image whatsoever. This is the cleanest way to use, for example, private Composer or npm registry tokens during a build.

The secret is passed at build time either as a file (--secret id=composer_auth,src=$HOME/.composer/auth.json) or as an environment variable (--secret id=npm_token,env=NPM_TOKEN). The Dockerfile then references the secret by ID. This approach works reliably for build-time secrets such as package registry credentials, SSH keys for private Git repositories, or internal CA certificates. Docker Secrets via BuildKit require no refactoring of the rest of the infrastructure, just the Dockerfile and the build command.


# syntax=docker/dockerfile:1.5
# Dockerfile: BuildKit secret mount for private Composer registry
FROM php:8.4-cli AS builder

WORKDIR /app

# Copy composer files first (layer caching)
COPY composer.json composer.lock ./

# Mount secret only for this RUN step, leaves NO trace in the image layer
RUN --mount=type=secret,id=composer_auth,target=/root/.composer/auth.json \
    composer install --no-dev --optimize-autoloader

COPY . .

FROM php:8.4-fpm AS runtime
WORKDIR /app
COPY --from=builder /app/vendor ./vendor
COPY --from=builder /app .

# Build command: secret is passed from a local file, never stored in the image
docker build \
  --secret id=composer_auth,src=$HOME/.composer/auth.json \
  --tag myapp:latest .

# Verify: secret does NOT appear in image history
docker history --no-trunc myapp:latest | grep -i auth
# (no output, secret left no trace)

# SSH agent forwarding for private Git repos during build
docker build \
  --ssh default=$SSH_AUTH_SOCK \
  --tag myapp:latest .

4. ENV Variables: Limits and Safe Usage

Environment variables are the common way to pass configuration into a container, and for many non-sensitive values they're the right choice. The problem starts when secrets are set via ENV in the Dockerfile, or passed via --env SECRET=value in a way that's visible through docker inspect. docker inspect container_id returns the full env array, including every value that was set. Anyone with access to the Docker socket can read those values.

Using environment variables safely for Docker Secrets requires that the value never appears in the Dockerfile, and is instead injected exclusively at runtime, either via --env-file (with a .env file that's listed in .gitignore and .dockerignore) or through a dedicated secrets management solution. For genuinely sensitive values like database master passwords or API tokens with write access, the rule is: ENV variables are not Docker Secrets. They're convenient, but they offer no real isolation.

5. Docker Swarm Secrets: Secrets for Production Services

Docker Swarm's built-in Docker Secrets mechanism provides an infrastructure-native solution for production environments. Secrets are stored encrypted in the Swarm manager's raft log and are only passed to the container process on the worker node where the service actually runs. There they appear as in-memory files under /run/secrets/: they're read like normal files, but they're never written to disk and are only accessible to the container process they belong to.

Swarm Docker Secrets are managed via the CLI: secrets are created (docker secret create), assigned to services (--secret in docker service create), and can be rotated without redeploying the service. This rotation workflow is a decisive advantage over ENV-based approaches: a new secret is created, added to the service, and the old one removed, with the service update rolling out without downtime. For Kubernetes users: Swarm secrets are conceptually similar to Kubernetes Secrets, just without any external components.


# Create a Docker Swarm secret from a file (not echoed to shell history)
printf "supersecret_db_password" | docker secret create db_password -

# List existing secrets (values are NEVER shown)
docker secret ls

# Deploy service with secret mounted at /run/secrets/db_password
docker service create \
  --name myapp \
  --secret db_password \
  --secret source=api_token_v2,target=api_token \
  myapp:latest

# Inside container: read secret like a regular file
cat /run/secrets/db_password

# Rotate secret without downtime
printf "new_supersecret_password" | docker secret create db_password_v2 -
docker service update \
  --secret-rm db_password \
  --secret-add source=db_password_v2,target=db_password \
  myapp
docker secret rm db_password

6. Docker Compose Secrets for Local Development

For local development with Docker Compose, Docker Secrets also offer a cleaner alternative to plain ENV values in compose.yaml. Compose supports a top-level secrets directive that reads secrets from files and mounts them into the container as entries under /run/secrets/. The secret file lives locally and is excluded from the repository via .gitignore, so the repository itself only ever contains the Compose configuration, never the value.

The practical benefit: new developers on the team clone the repository, create the secret files locally (from a password manager or an internal wiki), and can start up without any further configuration. The Compose file is fully checked into Git, with no credentials in it. For CI pipelines, the secret files are generated from pipeline variables before Docker Compose starts. This workflow makes Docker Secrets in Compose good practice even outside Swarm setups.

7. HashiCorp Vault and External Secret Stores

For more advanced requirements, secret rotation, audit logs, dynamically generated credentials, fine-grained access policies, the built-in Docker Secrets mechanisms aren't enough on their own. HashiCorp Vault complements Docker by acting as a central secret store. Vault generates short-lived credentials on demand, rotates secrets on configurable intervals, and logs every access. The container authenticates to Vault (via AppRole, a Kubernetes service account, or AWS IAM) and receives a short-lived token that it uses to fetch the secrets it needs from Vault.

The Vault agent sidecar is an established pattern for Kubernetes and Docker Swarm: a separate Vault agent container runs alongside the application container, authenticates to Vault, and writes the retrieved secrets to a shared in-memory volume. The application container reads the secrets from that volume, similar to native Docker Secrets, but with the full power of Vault's dynamic secrets, lease management, and audit backend. For environments that need to meet regulatory requirements (SOC 2, PCI DSS), this approach is often mandatory.

8. Secret Scanning: Catching Leaks Before They Happen

Even with correct Docker Secrets management, mistakes happen: a forgotten debug commit, a temporary hardcoded value that never got reverted, a COPY . . that accidentally sweeps in a local .env file. Secret scanners catch these leaks before the image is pushed to a registry. Tools like trivy (with --scanners secret), gitleaks, and truffleHog scan images, Git history, and Dockerfiles for known secret patterns: AWS keys, GitHub tokens, Stripe secrets, private keys.

Integrating this into CI pipelines is straightforward: trivy image --scanners secret myapp:latest fails the build if known secret patterns are found. In parallel, gitleaks detect should run as a pre-commit hook, so commits containing secrets never make it into the repository in the first place. A .dockerignore file that excludes .env, *.key, *.pem, *.p12, .aws/, and similar files is the last line of defense for the Docker Secrets concept.

9. Comparison: Secrets Methods at a Glance

Choosing the right Docker Secrets method depends on context: build time vs. runtime, local development vs. production, a simple setup vs. enterprise requirements. The table below summarizes the main methods and their security posture.

Method Context Security Level Recommendation
ENV in Dockerfile Build + runtime Critically unsafe Never for secrets
--build-arg Build time Unsafe (history) Do not use for secrets
BuildKit --mount=type=secret Build time Secure Recommended for build secrets
Docker Swarm Secrets Runtime (Swarm) Secure, encrypted Recommended for production
HashiCorp Vault Runtime (all) Enterprise grade For complex requirements

The choice between Swarm secrets and Vault has less to do with security, both are sufficiently secure for normal operation, and more to do with operational requirements. Vault offers secret rotation, dynamic credentials, audit logs, and fine-grained policies; Swarm secrets are simpler to set up and don't require an external dependency. For teams that already run Kubernetes, Kubernetes Secrets combined with an external secret store operator (for example, External Secrets Operator with AWS Secrets Manager) is the natural evolution of the Docker Secrets concept.

Mironsoft

Docker security, secrets management, and secure container infrastructure

Need to manage credentials securely across your Docker infrastructure?

We audit existing Docker setups for secrets leaks, migrate ENV-based credentials to secure Docker Secrets, and integrate secret scanners into your CI pipelines.

Security Audit

Systematic scan of all Dockerfiles and images for secrets leaks and unsafe patterns

BuildKit Migration

Switching to BuildKit mount secrets for build-time credentials with no layer trace

Vault Integration

HashiCorp Vault or AWS Secrets Manager for dynamic, rotating credentials

10. Summary

The core principle of Docker Secrets management is simple: credentials belong in none of the following: Dockerfiles, image layers, or version control. They're provided temporarily at build time via BuildKit mount secrets, injected at runtime via Docker Swarm secrets or an external secret store, and never persisted in the container filesystem. This isn't a theoretical security concept, it's a practical workflow that can be implemented with Docker's own built-in tools.

The pragmatic starting point: extend .dockerignore to cover all sensitive files, replace --build-arg for secrets with BuildKit mount secrets, remove ENV values from Dockerfiles, and integrate trivy --scanners secret into the CI pipeline. Swarm secrets or a Vault integration can follow step by step once production operations demand more robust guarantees. Docker Secrets aren't a one-time effort, they're an ongoing concept that grows along with the stack.

Docker Secrets: The Essentials at a Glance

BuildKit Secrets

RUN --mount=type=secret mounts secrets only for a single build step, no trace in layer history or the image filesystem.

Swarm Secrets

Stored encrypted, passed only to the assigned service, provided as an in-memory file under /run/secrets/, never written to disk.

Know the Limits of ENV

ENV in a Dockerfile ends up in layer history. docker inspect shows all env vars. For real secrets, always use a proper secrets mechanism.

Secret Scanning in CI

trivy --scanners secret and gitleaks as a pre-commit hook. .dockerignore for *.env, *.key, *.pem. The last safety net against accidental leaks.

11. FAQ: Docker Secrets

1What exactly are Docker Secrets?
A mechanism for sensitive data with no layer trace. Stored encrypted in Swarm, provided as an in-memory file under /run/secrets/, never on disk or in image history.
2Why is ENV in a Dockerfile unsafe?
ENV values end up in the layer history, readable with docker history --no-trunc. At runtime, docker inspect shows every env variable. No protection against insider threats or a compromised registry.
3--build-arg vs. BuildKit mount secrets?
build-arg appears in docker history. Mount secrets exist only during a single RUN step, no trace in the layer or manifest. The only safe option for build-time credentials.
4Compose secrets without Swarm?
Yes. The secrets directive in compose.yaml reads from a local file and mounts it under /run/secrets/. The file is listed in .gitignore, only the Compose config goes into the repository.
5Rotate Swarm secrets without downtime?
Create a new secret, then update the service with --secret-rm plus --secret-add (same target). Docker performs a rolling update without downtime. Delete the old secret afterwards.
6When do I need Vault?
For dynamically generated credentials, per-access audit logs, short-lived database secrets, or fine-grained policies across different services and environments.
7Keep .env files out of images?
Populate .dockerignore with .env, *.key, *.pem, .aws/. Add trivy --scanners secret as a CI gate. A last safety net against an accidental COPY . .
8Are Swarm secrets really encrypted?
Yes. Encrypted in the manager's raft store. On worker nodes, only in the RAM of authorized containers. docker secret inspect shows metadata, never the value itself.
9Pass SSH keys safely during a build?
docker build --ssh default=$SSH_AUTH_SOCK. In the Dockerfile: RUN --mount=type=ssh. The SSH agent socket is forwarded temporarily, the private key never leaves the host.
10What does trivy check during a secret scan?
Over 150 secret types: AWS keys, GitHub tokens, Stripe keys, private RSA keys, JWT tokens. Scans every image layer, not just the topmost one.