Suggest an editImprove this articleRefine the answer for “How to manage secrets (passwords, keys) in Docker properly?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Three layers of "don't put secrets in plain places":** BuildKit `--mount=type=secret` for build-time, Docker Swarm secrets (file-mounted) for runtime, external managers (Vault, AWS Secrets Manager) for production at scale. **Never use ENV vars or `--build-arg` for secrets.** ```dockerfile RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci ``` ```yaml services: api: secrets: [db_password] secrets: db_password: external: true ``` **Key:** secrets in env leak via `inspect`, image history, ps, logs. Mounted-as-files secrets only exist in tmpfs at runtime. For multi-service production, an external secret manager (Vault, cloud) is the only acceptable answer.Shown above the full answer for quick recall.Answer (EN)Image**Managing secrets in Docker** is a layered problem: build-time secrets, runtime secrets, and "how do I never type a password into a YAML file". Each layer has the right tool; using the wrong tool is how passwords end up on Pastebin. ## Theory ### TL;DR Three distinct problems, three answers: 1. **Build-time secrets** (npm tokens, private repo SSH keys, registry credentials): **BuildKit secret mounts** (`--mount=type=secret`). 2. **Runtime secrets** (DB passwords, API keys): **mount as files** via Swarm secrets, K8s secrets, or `docker run --secret`. 3. **Single source of truth at scale**: **external secret manager** (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault). Containers fetch at startup or via sidecar. **Forbidden patterns:** secrets in ENV, secrets in `--build-arg`, secrets baked into image layers. ### Why ENV is wrong ```bash docker run -e DB_PASSWORD=hunter2 myapp ``` Leaks via: - `docker inspect <container>` — full env in plain text. - `ps auxe` on the host — anyone with shell access. - Container's own `/proc/1/environ` — readable from inside. - App startup logs — many frameworks log full env. - Process listings inside the container. - Crash dumps — env may end up in core files. Go through any of these and the password is sitting there. ### Build-time secrets via BuildKit ```dockerfile # syntax=docker/dockerfile:1.7 FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci COPY . . ``` ```bash docker buildx build --secret id=npmrc,src=$HOME/.npmrc -t myapp . ``` The `.npmrc` is mounted into the RUN step but never lands in any layer. `docker history` shows no trace. **Compare with the bad pattern:** ```dockerfile # WRONG: token in image history forever ARG NPM_TOKEN RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc && npm ci ``` `docker history --no-trunc` shows the literal RUN line, including the token value. ### Runtime secrets via Swarm ```bash # Create the secret (one-time) echo "hunter2" | docker secret create db_password - # Reference it in a service docker service create \ --name api \ --secret db_password \ myapp ``` Inside the container: ``` /run/secrets/db_password # contents: hunter2 ``` The secret is mounted as a file at `/run/secrets/<name>` on a tmpfs (RAM-only, never on disk). The app reads the file at startup: ```python with open('/run/secrets/db_password') as f: password = f.read().strip() ``` No env var, no inspectable string. Inside the container, only the running process can read the tmpfs file. ### Compose with secrets ```yaml version: '3.9' services: api: image: myapp secrets: - db_password - api_key db: image: postgres:16 environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password # postgres image supports _FILE pattern secrets: - db_password secrets: db_password: file: ./secrets/db_password.txt api_key: external: true ``` Many official images (postgres, mysql, mariadb) support the `_FILE` env-var convention: `POSTGRES_PASSWORD_FILE=/run/secrets/db_password` instead of `POSTGRES_PASSWORD=...`. Use this pattern. ### Production: external secret managers At scale, none of "file in repo" or "docker secret create from CLI" survive. The answer is a real secret manager: **HashiCorp Vault** ```bash # App fetches at startup using a Vault sidecar or built-in SDK vault read secret/data/db/password # OR use the agent-injector pattern in K8s ``` **AWS Secrets Manager / Parameter Store** ```python import boto3 client = boto3.client('secretsmanager') response = client.get_secret_value(SecretId='prod/db/password') password = response['SecretString'] ``` **GCP Secret Manager / Azure Key Vault** — similar APIs. The app authenticates with workload identity (IAM role, service account) — no static credentials anywhere except the cloud's IAM. ### Sidecar / init pattern ```yaml services: vault-init: image: vault command: ["vault", "agent", "-config=/etc/vault/agent.hcl"] volumes: - vault-secrets:/secrets api: image: myapp volumes: - vault-secrets:/secrets:ro depends_on: vault-init: condition: service_healthy ``` Vault Agent fetches secrets, writes to a tmpfs volume. App reads from `/secrets/`. The app never authenticates to Vault directly. ### Common mistakes **Secrets in env** Covered. Most common, most leaked. **Build-args used as secrets** ```dockerfile ARG DB_PASSWORD RUN echo "$DB_PASSWORD" > /etc/myapp/db_password ``` `docker history` shows the build-arg value forever. Layer contents include the file. Two leaks for the price of one. **Secrets committed to git as `secrets.yaml`** Classic mistake. Even if you remove the file later, git history has it. **Use `git-secrets`, `gitleaks`, or pre-commit hooks** to prevent commits, not just clean up after. **Decrypting at startup with a hardcoded key** ```python encrypted_password = config['DB_PASSWORD'] key = 'this-is-the-key' # hardcoded → not actually a secret password = decrypt(encrypted_password, key) ``` If the key is in the image, the encryption is theatre. The right pattern: key comes from the runtime environment (KMS, IAM, sidecar), encrypted blob at rest is fine. **Logging the env at startup** ```python log.info(f"Starting with config: {os.environ}") # Secrets in env now also in logs ``` Whitelist log fields, never blanket-log env. Use a sanitizer that filters keys matching `*PASSWORD*`, `*TOKEN*`, `*KEY*`. ### Real-world architectures #### Small team / single host - Build secrets: BuildKit secret mounts. - Runtime: Compose `secrets:` with files on disk, gitignored. - Acceptable for staging; risky for prod (depends on threat model). #### Medium team / Swarm - Build secrets: BuildKit, fed from CI. - Runtime: Swarm secrets (`docker secret create`). - Audit: Docker secret CLI shows what exists; rotate by recreating. #### Production / Kubernetes / multi-cluster - Vault or cloud-native secret manager. - App fetches at startup via SDK with workload-identity auth. - Or Vault Agent sidecar that writes to tmpfs. - Auto-rotation: short-lived dynamic credentials (Vault DB engine). ### Follow-up questions **Q:** Are Docker Swarm secrets encrypted at rest? **A:** Yes, in the raft store on managers. They are decrypted at use time and mounted as tmpfs (in RAM, no disk write inside the container). **Q:** Can I rotate a Swarm secret? **A:** Not in-place. Create `db_password_v2`, update the service to use the new one, delete the old. Most apps need to be restarted to pick up new secrets — design for it. **Q:** What is the `_FILE` pattern? **A:** Convention popularized by official Docker images: instead of `MY_VAR=value`, accept `MY_VAR_FILE=/path/to/file` and read the value from the file. Postgres, MySQL, MariaDB, RabbitMQ all support it. The file can be a Swarm secret mount. **Q:** How do I handle secrets in CI? **A:** CI's secret store (GitHub Actions secrets, GitLab masked variables, AWS Secrets Manager pulled by an OIDC role). Pass to BuildKit via `--secret`. Never commit, never log. **Q:** (Senior) How would you design secret management for a multi-team, multi-cluster production environment? **A:** Vault as the source of truth. Each service's CI pipeline writes its secrets via Vault's API at deploy time. Each cluster has a Vault Agent sidecar pattern (or external-secrets-operator on K8s). Apps authenticate via workload identity (K8s service account, IAM role, AppRole). Secret rotation: dynamic engine (DB credentials regenerated each request, lifespan minutes). Audit: every secret read logged in Vault's audit log. Recovery: Vault unseal procedure documented, key shares distributed via Shamir's Secret Sharing. The point: developers never see a static password; CI/CD never sees one in plaintext outside Vault; nothing in any container's image or env is sensitive. ## Examples ### BuildKit secret for npmrc ```dockerfile # syntax=docker/dockerfile:1.7 FROM node:22-alpine WORKDIR /app COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ --mount=type=cache,target=/root/.npm \ npm ci COPY . . ``` ```yaml # GitHub Actions - uses: docker/build-push-action@v5 with: secrets: | npmrc=${{ secrets.NPMRC }} ``` No `.npmrc` in the image history; cache mount keeps installs fast. ### Compose with `_FILE` pattern ```yaml version: '3.9' services: postgres: image: postgres:16 environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: [db_password] api: image: myapp environment: DB_PASSWORD_FILE: /run/secrets/db_password secrets: [db_password] secrets: db_password: file: ./secrets/db_password.txt ``` App reads the file at startup; postgres image natively understands `_FILE`. Same secret distributed to two services. ### Vault Agent sidecar ```yaml # K8s pod with vault-agent sidecar apiVersion: v1 kind: Pod metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "myapp" vault.hashicorp.com/agent-inject-secret-db: "secret/data/myapp/db" spec: containers: - name: app image: myapp # /vault/secrets/db is created by the injected agent sidecar ``` App reads `/vault/secrets/db`. No Vault SDK in the app code. Vault rotates credentials; agent updates the file in place.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.