Home DevOps Docker Secrets Management: Stop Hardcoding Credentials in Production
Advanced 4 min · July 11, 2026

Docker Secrets Management: Stop Hardcoding Credentials in Production

Docker secrets management explained with production patterns.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 35 min
  • Docker basics (images, containers, services)
  • Docker Swarm or Kubernetes fundamentals
  • Familiarity with environment variables in Docker
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Use Docker's built-in docker secret create for Swarm services, or mount secrets as files via bind mounts with restricted permissions. Never pass secrets as environment variables in production — they're visible in process lists and logs.

✦ Definition~90s read
What is Docker Secrets Management?

Docker Secrets Management is the practice of securely storing and injecting sensitive data (passwords, API keys, TLS certificates) into containers at runtime, avoiding hardcoded values in images or environment variables that leak via docker inspect.

Think of Docker secrets like a hotel safe deposit box.
Plain-English First

Think of Docker secrets like a hotel safe deposit box. You (the container) get a key to open the box when you check in, but the box's contents are never written on the key itself. If someone steals the key, they still can't read the contents without physically opening the box. Similarly, Docker secrets are stored encrypted at rest and only decrypted in memory inside the container that needs them.

I've seen a startup's entire database credentials leaked because someone ran docker inspect on a running container. The password was in an environment variable. That's not a rookie mistake — it's a design flaw. Docker Secrets Management exists precisely to prevent this. It's not about convenience; it's about not getting pwned at 3 AM when an attacker enumerates your containers. After this article, you'll be able to design a secret delivery pipeline that survives audits, rotates keys without downtime, and doesn't make you cringe when ops asks for the secret inventory.

Why Environment Variables Are a Security Nightmare

Environment variables are the default way to pass configuration to containers. They're easy, they're documented everywhere, and they're wrong for secrets. Here's why: any process with access to /proc//environ can read them. That includes docker inspect, kubectl exec, and any compromised container on the same host. Worse, env vars leak into logs, error reports, and core dumps. I've personally debugged a case where a Python traceback printed the entire DB password because it was in the environment. The fix? Never put secrets in env vars in production. Use Docker secrets or a vault.

env_var_leak.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — DevOps tutorial

# Run a container with a secret as env var
docker run -d --name leaky -e DB_PASSWORD=supersecret alpine sleep 3600

# Inspect the container — password visible
docker inspect leaky | grep DB_PASSWORD
# Output: "DB_PASSWORD=supersecret"

# Or read from /proc
docker exec leaky cat /proc/1/environ | tr '\0' '\n' | grep DB_PASSWORD
# Output: DB_PASSWORD=supersecret
Output
DB_PASSWORD=supersecret
DB_PASSWORD=supersecret
⚠ Production Trap:
Even if you delete the env var after reading it, the process's initial environment is captured in /proc. There's no safe way to use env vars for secrets.
docker-secrets-management THECODEFORGE.IO External Vault Integration Architecture HashiCorp Vault with Docker for centralized secret management Application Layer Docker Containers | Sidecar Proxies Secret Injection Layer Vault Agent | Secret CSI Driver Policy Layer ACL Policies | Audit Logs Storage Backend Vault Server | Consul/Etcd THECODEFORGE.IO
thecodeforge.io
Docker Secrets Management

Docker Swarm Secrets: The Built-In Solution

Docker Swarm has native secret management. Secrets are encrypted at rest in the Swarm store and only decrypted when a container that needs them is scheduled. They're mounted as files in /run/secrets/ by default, with in-memory tmpfs. No disk writes. No env var leaks. The flow: docker secret create stores the secret, then you grant access to services via --secret. The container reads the file at startup. Rotation requires updating the secret and redeploying the service. For zero-downtime rotation, you can mount multiple versions and have the app watch for changes.

swarm_secret_setup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — DevOps tutorial

# Create a secret from stdin
echo "my-db-password" | docker secret create db_password -

# Verify
docker secret ls

# Deploy a service with the secret
docker service create --name web \
  --secret db_password \
  --publish 80:80 \
  nginx:alpine

# Inside the container, the secret is at /run/secrets/db_password
docker exec $(docker ps -q) cat /run/secrets/db_password
# Output: my-db-password
Output
my-db-password
🔥Senior Shortcut:
Use docker secret create with --label to track metadata like rotation date or owner. It's a lifesaver when auditors ask for an inventory.

Kubernetes Secrets: More Power, More Pitfalls

Kubernetes Secrets are similar but more flexible — and more dangerous. By default, Secrets are stored unencrypted in etcd. You must enable encryption at rest. They can be mounted as volumes or injected as env vars (don't do the latter). The real power is in integration with external vaults like HashiCorp Vault via CSI drivers or sidecars. For production, never use plain Kubernetes Secrets for anything sensitive. Always enable encryption and use RBAC to restrict access. The classic mistake: storing a cloud API key in a Secret and then accidentally committing the YAML to git.

k8s_secret.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — DevOps tutorial

apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  password: bXktZGItcGFzc3dvcmQ=  # base64 of 'my-db-password'
---
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
  - name: web
    image: nginx:alpine
    volumeMounts:
    - name: secrets
      mountPath: /etc/secrets
      readOnly: true
  volumes:
  - name: secrets
    secret:
      secretName: db-credentials
⚠ Never Do This:
Setting envFrom with a Secret. It creates env vars that leak via /proc and kubectl exec. Always mount as a volume.
Docker Swarm Secrets vs Kubernetes Secrets Built-in secret management trade-offs for container orchestration Docker Swarm Secrets Kubernetes Secrets Encryption at Rest Encrypted by default Requires encryption config Secret Rotation Manual redeploy needed Automated via controllers Access Control Service-level only RBAC with namespaces External Vault Integration Plugin-based (swarm-external-secrets) Native CSI driver support Secret Size Limit 500 KB per secret 1 MB per secret THECODEFORGE.IO
thecodeforge.io
Docker Secrets Management

Secret Rotation Without Downtime

Rotating secrets is where most teams fail. The naive approach: update the secret, restart all containers. That causes downtime. Better: mount the secret as a file and have the application watch for file changes (e.g., inotify). When the file changes, reload the connection. For databases, use a connection pool that supports credential refresh (like HikariCP with a DataSource that reads from a file). In Swarm, you can update a secret and redeploy the service with --update-delay for rolling updates. In Kubernetes, use a controller that watches the Secret and triggers a rolling restart via annotation change.

secret_watcher.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// io.thecodeforge — DevOps tutorial

import os
import time
import signal

SECRET_PATH = '/run/secrets/db_password'

def load_secret():
    with open(SECRET_PATH) as f:
        return f.read().strip()

def reload_handler(signum, frame):
    global db_password
    db_password = load_secret()
    print(f"Secret reloaded at {time.ctime()}")

# Register SIGHUP handler for manual reload
signal.signal(signal.SIGHUP, reload_handler)

# Initial load
db_password = load_secret()
print(f"Initial password: {db_password}")

# Simulate main loop
while True:
    time.sleep(10)
    # In production, use inotify or watchdog library for file change detection
Output
Initial password: my-db-password
Secret reloaded at Mon Jan 15 12:00:00 2024
💡Interview Gold:
How do you rotate a database password without dropping connections? Answer: Use a connection pool that supports credential refresh, or implement a two-phase rotation where both old and new passwords are accepted during a transition window.

External Vault Integration: HashiCorp Vault with Docker

For serious production environments, built-in secrets managers are too limited. You need audit logs, dynamic secrets (short-lived credentials), and fine-grained access control. HashiCorp Vault is the gold standard. Integrate with Docker via the Vault agent sidecar pattern: a container that authenticates to Vault, retrieves secrets, and writes them to a shared volume. The application reads from that volume. Vault handles rotation automatically. The downside: complexity. You now manage Vault itself. But for compliance-heavy environments (PCI-DSS, HIPAA), it's non-negotiable.

docker-compose.vault.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — DevOps tutorial

version: '3.8'
services:
  vault-agent:
    image: vault:latest
    command: agent -config=/etc/vault/config.hcl
    volumes:
      - shared-secrets:/secrets
      - ./vault-config:/etc/vault
    environment:
      VAULT_ADDR: https://vault.example.com
  app:
    image: myapp:latest
    volumes:
      - shared-secrets:/secrets:ro
    depends_on:
      - vault-agent
volumes:
  shared-secrets:
🔥Production Trap:
Vault agent can become a single point of failure. Always run at least two replicas and use a load balancer. Also, ensure the shared volume is tmpfs, not a network filesystem — latency kills.

When Not to Use Docker Secrets

Docker secrets are overkill for local development. Use .env files or docker-compose's env_file — just don't commit them to git. For single-node deployments, bind-mounting a secrets directory with restricted permissions is simpler and equally secure. Also, if your orchestrator doesn't support secrets (e.g., plain Docker Compose without Swarm), you're stuck with workarounds. In those cases, use a vault sidecar or encrypt the env var with a key that the app decrypts at startup. The key itself must be stored securely — a chicken-and-egg problem that vault solves.

local_secret_mount.shBASH
1
2
3
4
5
6
7
8
9
10
// io.thecodeforge — DevOps tutorial

# For local dev, just mount a file with restricted permissions
mkdir -p ./secrets
echo "dev-password" > ./secrets/db_password
chmod 0400 ./secrets/db_password
chown 1000:1000 ./secrets/db_password

docker run -v $(pwd)/secrets:/run/secrets:ro alpine cat /run/secrets/db_password
# Output: dev-password
Output
dev-password
💡Senior Shortcut:
For local dev, use a .secrets directory with a .gitignore entry. Add a secrets.example file with placeholder values for onboarding.

swarm-external-secrets Plugin Deep Dive

The swarm-external-secrets Docker plugin brings multi-provider secret management to Docker Swarm. Instead of storing secrets in the Swarm Raft store, it fetches them on-demand from external providers: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, and OpenBao. Authentication is handled via AppRole for Vault/OpenBao, IAM roles for AWS, and managed identities for Azure. A key feature is SHA256 hash-based rotation detection — the plugin periodically compares the hash of the stored secret against the current value in the external store; when they differ, it rotates without service redeployment. The plugin also performs cleanup of old plugin versions to prevent version drift across the Swarm. However, a limitation to be aware of: worker nodes cannot serve as plugin managers; only manager nodes coordinate secret retrieval, which adds latency in large clusters and creates a potential bottleneck if the manager pool is undersized.

swarm_external_secrets_setup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — DevOps tutorial

# Install the plugin on all Swarm nodes
docker plugin install grafana/swarm-external-secrets:latest --alias secrets

# Configure Vault provider in /etc/docker/secrets-plugin.yaml
providers:
  vault:
    addr: https://vault.example.com:8200
    approle:
      role_id: "your-role-id"
      secret_id: "your-secret-id"
    tls_skip_verify: false

# Create a secret backed by external provider
docker secret create --driver secrets \
  --driver-opts provider=vault,path=secret/data/db/password \
  db_password -

# Deploy service — secret retrieved from Vault at container start
docker service create --name web \
  --secret db_password \
  nginx:alpine
Output
Plugin installed. Secret created with Vault backing. Service starts with secret retrieved from external store.
⚠ Worker Node Limitation:
Only Swarm manager nodes can act as plugin managers. If all managers are down, no new secrets can be retrieved. Always run at least 3 managers and monitor their health.

Zero-Downtime Secret Rotation Patterns

Rotating secrets without dropping connections is the holy grail of secrets management. There are two main patterns: application-level reload and container restart. Application-level reload involves the app watching the secret file via inotify or polling, then updating in-memory credentials without restarting. This requires the app to support credential refresh — not all do (e.g., legacy Java apps). The restart approach uses Docker's update_config settings with rolling updates: --update-delay 10s --update-parallelism 1 --update-order start-first. This gradually replaces containers, ensuring capacity at all times. Rotation interval tuning is critical — too fast and you overwhelm the system; 5 minutes is the standard production interval for most setups. For monitoring rotation events, pipe Docker logs into Loki: docker service logs --since 5m | promtail. This gives you a searchable audit trail of when secrets were rotated and which containers restarted as a result.

zero_downtime_rotation.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — DevOps tutorial

version: '3.8'
services:
  api:
    image: myapp:latest
    secrets:
      - db_password_v2
    deploy:
      replicas: 5
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
        failure_action: rollback
      restart_policy:
        condition: any

secrets:
  db_password_v2:
    external: true
Output
Rolling update replaces one container at a time. New containers get the rotated secret. Old containers continue serving until replaced.
💡Interview Gold: Two-Phase Rotation
For databases, implement a two-phase rotation: first add the new password to the database (both old and new accepted), rotate the app, then remove the old password. This eliminates any window where old and new containers disagree on credentials.

BuildKit Build-Time Secrets for Non-Swarm Docker

Docker BuildKit supports build-time secrets that never end up in the final image. Using --mount=type=secret during a docker build, you can pass sensitive values (npm tokens, SSH keys, signing certs) to the build process without embedding them. The syntax is --mount=type=secret,id=mysecret in the Dockerfile RUN command, then --secret id=mysecret,src=./local-secret on the docker build command line. For docker-compose builds, use the secrets key under the build context with a secrets section in your compose file (v3.9+). The critical difference from Swarm secrets: BuildKit secrets exist only during the build phase and are never present in the running container. Swarm secrets are for runtime. They serve different purposes — build vs run. A common trap: developers put deployment credentials in build args (--build-arg) which DO end up in image history. BuildKit secrets do not. Always choose --mount=type=secret over build args for anything sensitive.

buildkit_secret.DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — DevOps tutorial

# syntax=docker/dockerfile:1
FROM node:18-alpine

WORKDIR /app
COPY package*.json ./

# Mount npm token as build-time secret — not stored in image
RUN --mount=type=secret,id=npmrc \
    cat /run/secrets/npmrc > .npmrc && \
    npm ci --only=production

COPY . .
CMD ["node", "server.js"]

# Build command:
# docker build --secret id=npmrc,src=./.npmrc -t myapp .
Output
Build succeeds using npm token. Token is NOT present in any layer or image history.
⚠ Build Args Are Not Secrets:
Build args (--build-arg) are stored in image history and visible via docker history. Never use them for tokens or passwords. Only --mount=type=secret keeps values out of the final image.

Secrets in CI/CD

CI/CD pipelines have unique secret challenges: secrets must be available during build but must never leak into logs, artifacts, or image layers. GitHub Actions offers encrypted secrets per repo/environment accessible via ${{ secrets.MY_SECRET }}. GitLab CI has CI/CD Variables with masking and protection (masked variables are hidden in job logs). The perennial debate: mount secrets as files or inject as env vars? Files win — env vars are visible in /proc, printed by debugging tools, and logged by CI runners. Use GitHub Actions to write secrets to a temp file with restricted permissions. For GitLab, use the file type variable (variable type: file) which mounts as a file. To prevent secret leak in build logs: never echo secrets, disable verbose commands, use set +x before secret access, and scan logs for accidental exposure with tools like truffleHog or git-secrets. Most importantly, never cache layers that contain secret data — use --no-cache or target specific stages that exclude secrets.

github_actions_secrets.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// io.thecodeforge — DevOps tutorial

name: Build with Secrets
on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Write secret to file — never to env var
      - name: Write npmrc
        run: |
          mkdir -p ~/.npm
          echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
          chmod 0600 ~/.npmrc
        shell: bash

      - name: Build with BuildKit
        run: |
          docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

      - name: Scan for leaks
        run: |
          docker history --no-trunc myapp | grep -i token && exit 1 || echo "No tokens in history"
Output
NPM token written to file, passed to BuildKit build, not stored in image. Final step verifies no token leaked.
🔥Production Trap: Log Leakage
I once saw a CI pipeline print the entire .env file to stdout during debugging. The password was in build logs for 6 months. Always mask secrets in your CI provider and use log scanners.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
A microservice handling payment processing crashed every 6 hours with OOMKilled. Logs showed 'Error: Connection pool exhausted' before crash.
Assumption
The team assumed a memory leak in the application code and spent days profiling heap dumps.
Root cause
The database password was stored as an environment variable. The connection pool library was reading the env var on every connection attempt, but the secret was rotated externally, causing the pool to hold stale connections that failed authentication. The retry logic spawned new connections without closing old ones, eventually exhausting memory.
Fix
Switched to Docker secrets mounted as a file. The application read the secret once at startup and cached it. Rotation now requires a restart, but the pool is stable and predictable.
Key lesson
  • Never let your application re-read secrets from environment variables on every request — it's a performance and reliability trap.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Container fails to start with 'secret not found'
Fix
1. Run docker secret ls to verify secret exists. 2. Check service definition for correct secret name. 3. Ensure service is in the same Swarm as the secret.
Symptom · 02
Application reads empty secret file
Fix
1. Verify secret file size: docker exec <container> ls -la /run/secrets/. 2. Check if secret was created with trailing newline — use printf instead of echo. 3. Ensure file permissions allow read by the runtime user.
Symptom · 03
Secret rotation not taking effect
Fix
1. Confirm secret was updated: docker secret inspect <secret>. 2. Redeploy the service: docker service update --secret-rm <old> --secret-add <new> <service>. 3. For zero-downtime, use rolling update with --update-delay.
★ Docker Secrets Management Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
`secret not found` error
Immediate action
Check if secret exists in Swarm
Commands
docker secret ls
docker service ps <service> --no-trunc | grep secret
Fix now
Create the secret: echo 'password' | docker secret create <name> -
Empty secret file inside container+
Immediate action
Check file size and content
Commands
docker exec <container> ls -la /run/secrets/<name>
docker exec <container> cat /run/secrets/<name> | wc -c
Fix now
Recreate secret without trailing newline: printf 'password' | docker secret create <name> -
Permission denied reading secret+
Immediate action
Check file permissions and ownership
Commands
docker exec <container> ls -la /run/secrets/
docker exec <container> id
Fix now
Set secret with specific uid/gid: docker service create --secret source=<name>,uid=1000,gid=1000,mode=0400 ...
Secret not updated after rotation+
Immediate action
Verify secret version and redeploy
Commands
docker secret inspect <name> --format '{{.UpdatedAt}}'
docker service update --secret-rm <old> --secret-add <new> <service>
Fix now
Use docker service update --force to trigger a rolling restart
Feature / AspectEnvironment VariablesDocker Swarm SecretsKubernetes SecretsHashiCorp Vault
Encryption at restNoYes (in Swarm store)Optional (encryption config)Yes (always)
Audit trailNoNoNo (native)Yes (detailed logs)
Rotation without restartNoNo (requires redeploy)No (requires restart)Yes (dynamic secrets)
ComplexityLowMediumMediumHigh
Leak via /procYesNo (file mount)No if mounted as volumeNo (file mount)
⚙ Quick Reference
10 commands from this guide
FileCommand / CodePurpose
env_var_leak.shdocker run -d --name leaky -e DB_PASSWORD=supersecret alpine sleep 3600Why Environment Variables Are a Security Nightmare
swarm_secret_setup.shecho "my-db-password" | docker secret create db_password -Docker Swarm Secrets
k8s_secret.yamlapiVersion: v1Kubernetes Secrets
secret_watcher.pySECRET_PATH = '/run/secrets/db_password'Secret Rotation Without Downtime
docker-compose.vault.ymlversion: '3.8'External Vault Integration
local_secret_mount.shmkdir -p ./secretsWhen Not to Use Docker Secrets
swarm_external_secrets_setup.shdocker plugin install grafana/swarm-external-secrets:latest --alias secretsswarm-external-secrets Plugin Deep Dive
zero_downtime_rotation.ymlversion: '3.8'Zero-Downtime Secret Rotation Patterns
buildkit_secret.DockerfileFROM node:18-alpineBuildKit Build-Time Secrets for Non-Swarm Docker
github_actions_secrets.ymlname: Build with SecretsSecrets in CI/CD

Key takeaways

1
Never use environment variables for secrets in production
they leak via /proc and docker inspect.
2
Docker Swarm secrets and Kubernetes Secrets are better, but still require careful handling for rotation and access control.
3
For zero-downtime rotation, mount secrets as files and have the app watch for changes or use a vault sidecar.
4
External vaults like HashiCorp Vault are the only way to get dynamic secrets and audit trails at scale.
5
For secret rotation at scale, implement two-phase rotation (old + new credentials accepted during transition) combined with rolling updates. Never rotate all containers simultaneously.
6
BuildKit --mount=type=secret is the only safe way to pass secrets during Docker builds. Build args leak secrets into image history and should never be used for sensitive data.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Docker Swarm handle secret encryption at rest and in transit?
Q02SENIOR
When would you choose Kubernetes Secrets over HashiCorp Vault in a produ...
Q03SENIOR
What happens when you update a Docker secret while a service is running ...
Q04JUNIOR
What is the difference between `docker secret create` and storing a secr...
Q05SENIOR
A developer stored a database password in a Kubernetes Secret and commit...
Q06SENIOR
How would you design a secret delivery system for a 500-container micros...
Q07SENIOR
Design a zero-downtime secret rotation system for a 50-microservice plat...
Q08SENIOR
You need to deliver secrets to 200 containers in a non-Swarm Docker envi...
Q01 of 08SENIOR

How does Docker Swarm handle secret encryption at rest and in transit?

ANSWER
Secrets are encrypted using the Swarm manager's Raft log encryption (AES-GCM) at rest. In transit, they're sent over TLS between managers and workers. The decryption key is stored in the manager's memory and never written to disk.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How do I pass secrets to a Docker container without using environment variables?
02
What's the difference between Docker secrets and Kubernetes secrets?
03
How do I rotate a Docker secret without restarting the container?
04
Can I use Docker secrets with docker-compose?
05
What is the swarm-external-secrets plugin and when should I use it?
06
How do BuildKit build-time secrets differ from Docker Swarm secrets?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Docker. Mark it forged?

4 min read · try the examples if you haven't

Previous
Docker with GitLab CI/CD
32 / 43 · Docker
Next
Falco Runtime Security for Docker