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 gitBuildKit Secret Mounts (Build-Time)
Dockerfile
# syntax=docker/dockerfile:1 FROM node:20-alpineWORKDIR /appCOPY package.json ./ # Secret available only during this RUN, never in any layerRUN --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 timedocker buildx build \ --secret id=npm_token,src=$HOME/.secrets/npm-token \ -t myapp:latest . # Verify secret not in imagedocker history myapp:latest# No NPM_TOKEN visible anywhereRuntime Secrets — Correct Patterns
Bash
# PATTERN 1: Environment variable from a file# Store secret in a file, not hardcodedecho "supersecret" > /run/secrets/db_password docker run -d \ --env-file /run/secrets/env-file \ payment-api:latest # PATTERN 2: Docker Compose secrets# docker-compose.ymlservices: 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).passwordWhat Never to Do
Dockerfile
# NEVER: secrets in ENVENV DB_PASSWORD=supersecret123 # NEVER: secrets in ARGARG GITHUB_TOKEN=ghp_secretRUN git clone https://${GITHUB_TOKEN}@github.com/... # NEVER: copying .env file into imageCOPY .env . # <- ALL secrets now in image layer foreverCOMMON MISTAKE / WARNING**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.