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 buildwithout 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
.envfiles 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:
+--------------------------------------------+| 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## 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 layerFROM node:20-alpineWORKDIR /appCOPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm install --production ## Secret is completely gone after this RUN completesCOPY . .CMD ["node", "server.js"]## 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 versionsDOCKER_BUILDKIT=1 docker build \ --secret id=npmrc,src=.npmrc \ -t swiggy-api:latest . ## Verify the secret did not leak into image historydocker history swiggy-api:latest## You should see only the COPY and CMD layers — no .npmrc content anywhereMilestone 2 — Verify no secrets exist in image layers
## Check build args and environment in full image metadatadocker 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 toolsdocker history --no-trunc swiggy-api:latest## Look for any RUN commands that reference credential files or tokensMilestone 3 — Use Docker Compose secrets for runtime credentials
## 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_urlchmod 600 /run/secrets/db_url echo "sk_live_razorpay_key_here" > /run/secrets/payment_keychmod 600 /run/secrets/payment_key## docker-compose.ymlservices: 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// 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 THIS**Remember:** Compose secrets are mounted as a `tmpfs` (in-memory filesystem) inside the container at `/run/secrets/`. They do not appear in `docker inspect` environment output, and they disappear when the container stops.
Milestone 4 — Use .env files correctly
## .env (gitignored — for local dev only)DATABASE_URL=postgres://dev_user:devpass@localhost:5432/swiggy_devREDIS_URL=redis://localhost:6379NODE_ENV=development## .gitignore — ensure this is always present.env.env.*!.env.example## .env.example (committed to git — safe dummy values only)DATABASE_URL=postgres://user:password@localhost:5432/dbnameREDIS_URL=redis://localhost:6379NODE_ENV=developmentCOMMON MISTAKE / WARNING**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
## UNSAFE — build args are visible in docker history## docker build --build-arg NPM_TOKEN=npm_abc123 . <-- DO NOT DO THIS ## Verify a build arg leakeddocker 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 mountsDOCKER_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> |
PLACEMENT PRO TIP**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.
COMMON MISTAKE / WARNING**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.