10 Lines to Better Docker Compose Secrets

This is a prac­ti­cal pat­tern I use when con­tainer­ized apps expect envi­ron­ment vari­ables but I want the secu­ri­ty ben­e­fits of file-​mounted secrets. Drop the shell script below next to your Docker Compose files and you can do the same.

Quick overview

Secrets like pass­words and API keys belong out­side your repos­i­to­ry and appli­ca­tion image lay­ers. Docker Compose can mount such secrets in your con­tain­ers as files under /run/secrets, which keeps them out of images and ver­sion con­trol. But many apps still expect con­fig­u­ra­tion via envi­ron­ment vari­ables. Rather than chang­ing app code, I use a tiny wrap­per script that:

  • reads every file in /run/secrets
  • exports each file’s con­tents as an envi­ron­ment variable
  • then execs the orig­i­nal command

It’s small, pre­dictable, portable, and keeps secrets from mix­ing with your ver­sioned .env envi­ron­ment files and out of your Compose files.

How it works

  • Location: Docker Compose mounts secrets into the con­tain­er at /run/secrets/<NAME>.
  • Mapping rule: The wrap­per uses those file names as envi­ron­ment vari­able names; the file con­tents become the val­ues. Secret names in your Compose file must be valid shell iden­ti­fiers (they become both the file names in /run/secrets and the export­ed vari­able names).
  • Execution: After export­ing vari­ables, the script uses exec "$@" so that the wrapped process replaces the shell and inher­its the export­ed environment.
  • Security mod­el: Secrets remain files you can per­mis­sion appro­pri­ate­ly on the host; they’re not baked into images or stored in your Compose YAML as plain text.

The script

Let’s call it with-secrets.sh:

#!/bin/sh
set -eu

for secret_file in /run/secrets/*; do
  [ -e "$secret_file" ] || continue
  if [ -f "$secret_file" ]; then
    name=$(basename "$secret_file")
    export "$name=$(cat "$secret_file")"
  fi
done

exec "$@"

Notes about the script

  • set -eu fails fast on unset vari­ables or errors.
  • Since it exports each secret using the file name as the vari­able name, san­i­tize the file name if you need dif­fer­ent envi­ron­ment vari­able names.
  • The final exec hands con­trol to your app with­out leav­ing an extra shell process.

Example Compose snippet

services:
  app:
    image: your-app:latest
    secrets:
      - DB_PASS
      - API_KEY
    volumes:
      - ./with-secrets.sh:/with-secrets.sh:ro
    command: ["/with-secrets.sh", "your-original-command", "--with-args"]

secrets:
  DB_PASS:
    file: ./secrets/db_password.txt
  API_KEY:
    file: ./secrets/api_key.txt

Behavior: DB_PASS and API_KEY above appear as files (/run/secrets/DB_PASS, /run/secrets/API_KEY); the mount­ed with-secrets.sh wrap­per script exports them as DB_PASS and API_KEY envi­ron­ment vari­ables for your-original-command --with-args.

Decision points and alternatives

  • Prefer native *_FILE sup­port if your app sup­ports it (e.g., PostgreSQL’s PGPASSFILE). That avoids the wrap­per entirely.
  • For multi-​host or high-​compliance deploy­ments, use an exter­nal secrets man­ag­er (e.g., Hashicorp Vault, cloud KMS, SOPS) rather than Compose secrets.
  • Build-​time secrets are a sep­a­rate con­cern; use BuildKit or ded­i­cat­ed build secret mech­a­nisms to avoid leak­ing cre­den­tials into your image layers.

Risks and mitigations

  • Risk: Accidentally log­ging or dump­ing envi­ron­ment vari­ables
    Mitigation: Never print envi­ron­ment vari­ables in logs and restrict debug output
  • Risk: Secret file names that are not valid shell iden­ti­fiers
    Mitigation: Normalize or map file names to safe envi­ron­ment vari­able names before exporting
  • Risk: Secrets checked into git or oth­er ver­sion con­trol
    Mitigation: Keep secret files out of repos, add strict .gitignore rules, and inject secrets via CI/​CD or run­time provisioning

Final notes

This pat­tern is inten­tion­al­ly prag­mat­ic: it pre­serves the secu­ri­ty advan­tage of file-​mounted secrets while let­ting unmod­i­fied apps keep using envi­ron­ment vari­ables. It’s not a sil­ver bul­let for every environment–use it where Compose secrets are appro­pri­ate and pair it with stronger secret stores for production-​grade, multi-​host deployments.


Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.

Comments

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)