Docker & ContainersXIV Β· SecretsExternal managers
External secret managers β Vault, AWS SM, Doppler, Infisical
What you'll learn
- Choose an external secret manager for a Docker deployment
- Solve the bootstrap problem: how the container authenticates to the manager
- Deliver fetched secrets to a tmpfs, not a disk-backed volume
- Plan for the manager being unreachable at start-up
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
For serious production secrets β database credentials, cloud API keys, signing keys β an external secret manager is the right answer. The manager is the source of truth; the container fetches on demand.
That sentence hides the question this lesson is really about. If the container fetches a secret from the manager, it has to prove it is entitled to. What does it prove that with, and where did that credential come from?
The bootstrap problem
The three delivery topologies
How the fetched value reaches the application matters as much as where it came from.
| App-native SDK | Sidecar agent | Init container | |
|---|---|---|---|
| Who fetches | The application | A long-running companion | A container that runs and exits |
| Secret lands in | Application memory | A shared tmpfs file | A shared tmpfs file |
| Handles rotation | Yes, if you wrote it to | Yes, re-renders on change | No, only at start |
| Application changes needed | Substantial | None | None |
| Failure blocks start | Yes | Yes, with service_healthy | Yes, with service_completed_successfully |
App-native is the cleanest and the most work. The application holds a client, refreshes on a timer, and never writes the value anywhere. Nothing is on disk at any point. It requires touching the application, which rules it out for anything you did not write.
Sidecar is the workhorse. Vault Agent is the canonical implementation: it authenticates once, keeps its token renewed, renders secrets into files from templates, and re-renders when the value changes. The application reads a file and knows nothing about Vault.
Init container is the simplest and covers most cases. A
container fetches the secret, writes the file, exits. Compose waits
for it with service_completed_successfully. There is no rotation β
the value is fixed until the next deploy β which is fine for
credentials with a 90-day life and unacceptable for a one-hour
dynamic lease.
A sidecar that is correct
Here is the shape, with the details that matter marked.
services:
vault-agent:
image: hashicorp/vault:1.18
command: ["vault", "agent", "-config=/etc/vault/agent.hcl"]
environment:
VAULT_ADDR: https://vault.example.com:8200
configs:
- source: agent_config
target: /etc/vault/agent.hcl
secrets:
- source: approle_secret_id
target: secret-id
mode: 0400
volumes:
- rendered:/rendered
healthcheck:
test: ["CMD", "test", "-s", "/rendered/db_password"]
interval: 5s
timeout: 2s
retries: 30
api:
image: myorg/api:1.4.0
user: "1001:1001"
environment:
DB_PASSWORD_FILE: /rendered/db_password
volumes:
- rendered:/rendered:ro
depends_on:
vault-agent:
condition: service_healthy
configs:
agent_config:
file: ./vault/agent.hcl
secrets:
approle_secret_id:
file: ./vault/secret-id
volumes:
rendered:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
o: "size=1m,mode=0700,uid=1001"
VOLUME=$(docker compose ps -q vault-agent | xargs -r docker inspect --format '{{range .Mounts}}{{.Name}} {{end}}')
# Where the daemon thinks it is
docker volume inspect rendered --format '{{.Options}} {{.Mountpoint}}'
# What the kernel actually mounted there. Expect FSTYPE tmpfs.
findmnt -no FSTYPE,SIZE,OPTIONS "$(docker volume inspect rendered --format '{{.Mountpoint}}')"
# And the definitive check: is the plaintext on the host disk?
grep -rl --binary-files=without-match "$(cat ./vault/known-test-value)" /var/lib/docker/volumes 2>/dev/null || echo 'not on disk'$ findmnt -no FSTYPE,SIZE,OPTIONS $(docker volume inspect rendered --format '{{.Mountpoint}}')# correct β the volume is RAM-backed
tmpfs 1M rw,relatime,size=1024k,mode=700,uid=1001,inode64
# wrong β the volume is a directory on the host filesystem
ext4 879G rw,relatimeIllustrative output
FSTYPE tmpfs passes. FSTYPE ext4 means every rendered secret is
in your host backups β and the giveaway is the SIZE column, which
reports the whole host filesystem rather than the small cap you
thought you had set.
The managers
HashiCorp Vault
The most capable option and the most operational work. What you buy:
- Dynamic secrets. Vault creates a Postgres role on demand with a one-hour lease and drops it when the lease expires. Nobody, at any point, knows a long-lived database password β there isnβt one. This is the single strongest reason to run Vault.
- Transit. Encryption as a service. The application sends plaintext and receives ciphertext; key material never leaves Vault.
- PKI. Issue and rotate certificates with short lives.
- A real audit device. Every read is logged with who, what and when.
# Store and read a static secret
vault kv put secret/database/prod password=REPLACE_ME
vault kv get -field=password secret/database/prod
The cost is that Vault is a stateful, quorum-based service you now have to run, unseal, back up and upgrade. Vault being down is an outage in everything that starts.
AWS Secrets Manager and Parameter Store
For AWS-native deployments, and the bootstrap problem largely disappears: the instance or task role is the identity.
aws secretsmanager create-secret \
--name prod/database/password \
--secret-string REPLACE_ME
aws secretsmanager get-secret-value \
--secret-id prod/database/password \
--query SecretString --output text
Secrets Manager supports scheduled rotation with a Lambda function, including managed rotation for RDS. Parameter Store is cheaper and simpler for values that do not need rotation. Both are billed per secret and per API call, which matters if a fleet re-fetches on every container start.
Google Secret Manager
printf '%s' REPLACE_ME | gcloud secrets create prod-database-password --data-file=-
gcloud secrets versions access latest --secret=prod-database-password
Note printf '%s' rather than echo. echo appends a newline and
the newline becomes part of the secret β the failure from the
previous lesson, arriving here at creation time instead of read
time.
Authentication is via Application Default Credentials, which on GCE means the instance service account and no stored credential.
Doppler and Infisical
Simpler operationally, less feature-rich. Both do versioned key-value secrets with environments, access control and audit logs, and both integrate with CI through a service token. Infisical is open source and can be self-hosted; Doppler is SaaS with a free tier.
Neither does dynamic database credentials, which is the honest dividing line: if what you need is βone place for our secrets, with history and access controlβ, they are a much better fit than Vault. If you need credentials that expire on their own, you need Vault or a cloud-native equivalent.
Choosing
| Need | Pick |
|---|---|
| Dynamic database credentials, transit, PKI | Vault |
| AWS-native, want the instance role to be the identity | AWS Secrets Manager |
| GCP-native | Google Secret Manager |
| One shared store with history and RBAC, minimal ops | Doppler or Infisical |
| Air-gapped or on-prem, no SaaS | Vault or Infisical, self-hosted |
- Choose by the bootstrap story, not the feature list. If the platform can give the container an identity with no stored credential, that beats every feature comparison.
- Deliver as a file, never as an environment variable. Including the bootstrap credential itself.
- Render into a tmpfs. Verify with
findmnt, not by reading the Compose file. - Set a small tmpfs size and a restrictive mode.
size=1m,mode=0700, withuid=set to the UID the application runs as. - Make the failure explicit.
depends_onwithservice_healthyorservice_completed_successfully, so a missing secret is a container that will not start rather than one that starts wrong. - Plan the mass restart. Token TTLs, backoff, caching, staggered rollout.
- Read the audit log. A secret manager whose audit log nobody looks at is a filesystem with extra steps. Alert on reads from unexpected identities and on rotation failures.
Knowledge check
Knowledge check Β· 5 questions
Q1. What is the "bootstrap problem" in a secret-manager deployment?
Q2. A Vault Agent sidecar renders a database password into a named volume shared with the application. What is wrong?
Q3. Which are acceptable ways for a container to authenticate to a secret manager? Select all that apply.
Q4. An init container that fetches a secret at start-up is sufficient for credentials with a one-hour dynamic lease.
Q5. A whole fleet reboots and every container authenticates to Vault at once. What is the primary risk?
Passing score: 75%. Answers are checked in this browser.