Docker Secrets Management: Stop Hardcoding Credentials in Production
Docker secrets management explained with production patterns.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Docker basics (images, containers, services)
- ✓Docker Swarm or Kubernetes fundamentals
- ✓Familiarity with environment variables in Docker
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.
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/ 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.
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.
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.
envFrom with a Secret. It creates env vars that leak via /proc and kubectl exec. Always mount as a volume.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.
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.
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.
.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.
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
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.
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.
The 4GB Container That Kept Dying
- Never let your application re-read secrets from environment variables on every request — it's a performance and reliability trap.
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.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.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 secret lsdocker service ps <service> --no-trunc | grep secretecho 'password' | docker secret create <name> -| File | Command / Code | Purpose |
|---|---|---|
| env_var_leak.sh | docker run -d --name leaky -e DB_PASSWORD=supersecret alpine sleep 3600 | Why Environment Variables Are a Security Nightmare |
| swarm_secret_setup.sh | echo "my-db-password" | docker secret create db_password - | Docker Swarm Secrets |
| k8s_secret.yaml | apiVersion: v1 | Kubernetes Secrets |
| secret_watcher.py | SECRET_PATH = '/run/secrets/db_password' | Secret Rotation Without Downtime |
| docker-compose.vault.yml | version: '3.8' | External Vault Integration |
| local_secret_mount.sh | mkdir -p ./secrets | When Not to Use Docker Secrets |
| swarm_external_secrets_setup.sh | docker plugin install grafana/swarm-external-secrets:latest --alias secrets | swarm-external-secrets Plugin Deep Dive |
| zero_downtime_rotation.yml | version: '3.8' | Zero-Downtime Secret Rotation Patterns |
| buildkit_secret.Dockerfile | FROM node:18-alpine | BuildKit Build-Time Secrets for Non-Swarm Docker |
| github_actions_secrets.yml | name: Build with Secrets | Secrets in CI/CD |
Key takeaways
Interview Questions on This Topic
How does Docker Swarm handle secret encryption at rest and in transit?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Docker. Mark it forged?
4 min read · try the examples if you haven't