Skip to main content
RunBook Academy

Docker & ContainersXIV Β· SecretsExternal managers

External secret managers β€” Vault, AWS SM, Doppler, Infisical

Intermediate⏱ ~24 min

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

Not yet marked complete on this device.

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 SDKSidecar agentInit container
Who fetchesThe applicationA long-running companionA container that runs and exits
Secret lands inApplication memoryA shared tmpfs fileA shared tmpfs file
Handles rotationYes, if you wrote it toYes, re-renders on changeNo, only at start
Application changes neededSubstantialNoneNone
Failure blocks startYesYes, with service_healthyYes, 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"
Read-only / Safeverify it is really tmpfs
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'
Read-only / Safethe two possible answers
$ 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,relatime

Illustrative 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

NeedPick
Dynamic database credentials, transit, PKIVault
AWS-native, want the instance role to be the identityAWS Secrets Manager
GCP-nativeGoogle Secret Manager
One shared store with history and RBAC, minimal opsDoppler or Infisical
Air-gapped or on-prem, no SaaSVault or Infisical, self-hosted
  1. 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.
  2. Deliver as a file, never as an environment variable. Including the bootstrap credential itself.
  3. Render into a tmpfs. Verify with findmnt, not by reading the Compose file.
  4. Set a small tmpfs size and a restrictive mode. size=1m,mode=0700, with uid= set to the UID the application runs as.
  5. Make the failure explicit. depends_on with service_healthy or service_completed_successfully, so a missing secret is a container that will not start rather than one that starts wrong.
  6. Plan the mass restart. Token TTLs, backoff, caching, staggered rollout.
  7. 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

  1. Q1. What is the "bootstrap problem" in a secret-manager deployment?

  2. Q2. A Vault Agent sidecar renders a database password into a named volume shared with the application. What is wrong?

  3. Q3. Which are acceptable ways for a container to authenticate to a secret manager? Select all that apply.

  4. Q4. An init container that fetches a secret at start-up is sufficient for credentials with a one-hour dynamic lease.

  5. 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.