Skip to main content

Docker Secrets Management — Avoiding Credentials in Images and Compose Files

Keep credentials out of Docker images and Compose files using Docker Secrets, BuildKit secret mounts, and environment variable best practices.

44 Terms

Overview and What You Will Learn

Secrets leak through Docker in three common ways: baked into image layers via COPY or RUN commands, hardcoded in docker-compose.yml and committed to git, or passed as plain --build-arg values that get recorded in image history. Any of these means your database password, API key, or TLS certificate ends up in a place it should never be.

This lab covers the correct patterns for each scenario.

By the end of this lab you will:

  • Use BuildKit secret mounts to pass credentials during docker build without baking them into image layers
  • Verify no secrets exist in image history
  • Use Docker Compose secrets to mount credentials as in-memory files at runtime
  • Use .env files correctly — what they protect against and what they do not
  • Apply the minimum-exposure principle: secrets available only to the containers that need them

Why This Matters in Production

A Swiggy backend engineer once ran docker history swiggy-api:prod to debug a build issue and found the prod database password sitting in plain text in a RUN layer from six months ago. The image had been pushed to a private ECR registry, but anyone with ECR pull access — including every engineer on the backend team — had had access to that credential for months without knowing it.

Docker image layers are permanent and cumulative. Even if you delete the secret in a later RUN layer, the original layer containing it is still part of the image and fully readable. The only safe options are to never put secrets in a RUN or COPY command, or to use BuildKit secret mounts which are specifically designed to be zero-trace.

Core Principles

Where secrets can leak in a Docker workflow:

Bash
+--------------------------------------------+
| docker build |
| |
| UNSAFE: RUN npm install with .npmrc COPY | <- secret baked into layer
| UNSAFE: ARG NPM_TOKEN passed at build time | <- visible in docker history
| SAFE: BuildKit --mount=type=secret | <- zero-trace, not in layers
+--------------------------------------------+
|
v
+--------------------------------------------+
| docker run / compose up |
| |
| UNSAFE: environment: DATABASE_URL=... | <- visible in inspect output
| UNSAFE: hardcoded in docker-compose.yml | <- committed to git
| SAFE: secrets: mounted as tmpfs file | <- in-memory, not in env vars
| SAFE: .env file (gitignored) | <- keeps secrets out of git
+--------------------------------------------+

Detailed Step-by-Step Practical Lab

Milestone 1 — Use BuildKit secret mounts during docker build

Dockerfile
## Dockerfile
## Bad pattern — .npmrc is COPY'd and stays in the image layer
## RUN commands after this can still read it, and so can docker history
## COPY .npmrc /root/.npmrc <-- DO NOT DO THIS
## RUN npm install
## RUN rm /root/.npmrc <-- deletion does NOT remove from the layer
## Correct pattern — BuildKit secret mount
## The secret is available only during this single RUN command
## It is NOT written to the image filesystem or any layer
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm install --production
## Secret is completely gone after this RUN completes
COPY . .
CMD ["node", "server.js"]
Bash
## Pass the secret file at build time — it never touches the image
## DOCKER_BUILDKIT=1 is the env var to enable BuildKit on older Docker versions
DOCKER_BUILDKIT=1 docker build \
--secret id=npmrc,src=.npmrc \
-t swiggy-api:latest .
## Verify the secret did not leak into image history
docker history swiggy-api:latest
## You should see only the COPY and CMD layers — no .npmrc content anywhere

Milestone 2 — Verify no secrets exist in image layers

Bash
## Check build args and environment in full image metadata
docker inspect swiggy-api:latest | grep -i -A5 "env\|arg\|cmd"
## Scan all image layers for accidental secret patterns
## (requires dive tool — useful for security audits)
dive swiggy-api:latest
## Quick layer-by-layer content check without extra tools
docker history --no-trunc swiggy-api:latest
## Look for any RUN commands that reference credential files or tokens

Milestone 3 — Use Docker Compose secrets for runtime credentials

Bash
## Create secret files on the host (chmod 600, never commit these)
echo "postgres://api_user:xK9mP2@10.0.1.50:5432/swiggy_prod" > /run/secrets/db_url
chmod 600 /run/secrets/db_url
echo "sk_live_razorpay_key_here" > /run/secrets/payment_key
chmod 600 /run/secrets/payment_key
YAML
## docker-compose.yml
services:
api:
image: swiggy-api:latest
secrets:
- db_url
- payment_key
## Read the secret from the mounted file in application code
## (not from an environment variable)
## The file is available at /run/secrets/db_url inside the container
worker:
image: swiggy-worker:latest
secrets:
## Worker only gets the db secret — not the payment key
## Principle of least privilege
- db_url
secrets:
db_url:
## File-based secret — reads from this host path
file: /run/secrets/db_url
payment_key:
file: /run/secrets/payment_key
JAVASCRIPT
// Reading a Compose secret in a Node.js application
// Secrets are mounted as files at /run/secrets/<name>
const fs = require('fs');
const dbUrl = fs.readFileSync('/run/secrets/db_url', 'utf8').trim();
const paymentKey = fs.readFileSync('/run/secrets/payment_key', 'utf8').trim();
Remember

Compose secrets are mounted as a tmpfs (in-memory filesystem) inside the container at /run/secrets/<secret_name>. They do not appear in docker inspect environment output, and they disappear when the container stops.

Milestone 4 — Use .env files correctly

Bash
## .env (gitignored — for local dev only)
DATABASE_URL=postgres://dev_user:devpass@localhost:5432/swiggy_dev
REDIS_URL=redis://localhost:6379
NODE_ENV=development
Bash
## .gitignore — ensure this is always present
.env
.env.*
!.env.example
Bash
## .env.example (committed to git — safe dummy values only)
DATABASE_URL=postgres://user:password@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
NODE_ENV=development
Common Mistake

Thinking that a .env file is "secure". It is not encrypted. It protects against accidentally committing secrets to git, but anyone with access to the server filesystem can read it. For secrets at rest, use Docker Secrets or a secrets manager like AWS Secrets Manager.

Milestone 5 — Avoid build-arg secrets entirely

Bash
## UNSAFE — build args are visible in docker history
## docker build --build-arg NPM_TOKEN=npm_abc123 . <-- DO NOT DO THIS
## Verify a build arg leaked
docker history --no-trunc some-image:latest | grep NPM_TOKEN
## If you see the token value in output, the image is compromised
## Rotate the credential immediately and rebuild with BuildKit secret mounts
DOCKER_BUILDKIT=1 docker build --secret id=npm_token,env=NPM_TOKEN -t fixed-image:latest .

Production Best Practices and Common Pitfalls

Scenario Wrong Correct
Build-time credentials --build-arg TOKEN=... BuildKit --mount=type=secret
Runtime credentials Hardcoded in compose environment: Compose secrets: block with host files
Git safety .env in the repo .env gitignored, .env.example committed
Least privilege All containers share all secrets Each service declares only its own secrets
Leaked image Secret in old RUN layer Rotate credential, rebuild with BuildKit, re-push

Quick Reference and Troubleshooting Commands

Task Command
Build with secret mount DOCKER_BUILDKIT=1 docker build --secret id=mykey,src=keyfile .
Check image for leaked args docker history --no-trunc <image>
Inspect container env vars docker inspect <container> --format '{{.Config.Env}}'
List secrets on a container docker inspect <container> --format '{{.HostConfig.Secrets}}'
Verify secret file in container docker exec <container> cat /run/secrets/<name>
Tip

After any security incident involving a leaked Docker build arg or secret, treat the credential as permanently compromised — rotate it immediately. Deleting the image or tag from the registry does not help if anyone pulled the image before the deletion.

Security

Environment variables passed via docker run -e or Compose environment: are visible to any process inside the container, appear in docker inspect output, and may be logged by crash reporters. For credentials, always prefer file-based secrets over environment variables.

Resources

Explore More in Docker Security and Production Practices

All 4 Topics

Frequently Asked Questions

Is Docker Secrets Management — Avoiding Credentials in Images and Compose Files free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Secrets Management — Avoiding Credentials in Images and Compose Files topic cover?

Keep credentials out of Docker images and Compose files using Docker Secrets, BuildKit secret mounts, and environment variable best practices.