This is a practical pattern I use when containerized apps expect environment variables but I want the security benefits of file-mounted secrets. Drop the shell script below next to your Docker Composefiles and you can do the same.
Quick overview
Secrets like passwords and API keys belong outside your repository and application image layers. Docker Compose can mount such secretsin your containers as files under /run/secrets, which keeps them out of images and version control. But many apps still expect configuration via environment variables. Rather than changing app code, I use a tiny wrapper script that:
- reads every file in
/run/secrets - exports each file’s contents as an environment variable
- then
execs the original command
It’s small, predictable, portable, and keeps secrets from mixing with your versioned .env environment filesand out of your Compose files.
How it works
- Location:Docker Compose mounts secrets into the container at
/run/secrets/<NAME>. - Mapping rule:The wrapper uses those file names as environment variable names; the file contents become the values. Secret names in your Compose file must be valid shell identifiers(they become both the file names in
/run/secretsand the exported variable names). - Execution:After exporting variables, the script uses
exec "$@"so that the wrapped process replaces the shell and inherits the exported environment. - Security model:Secrets remain files you can permission appropriately on the host; they’re not baked into images or stored in your Compose YAMLas 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 -eufails fast on unset variables or errors.- Since it exports each secret using the file name as the variable name, sanitize the file name if you need different environment variable names.
- The final
exechands control to your app without leaving 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_PASSand API_KEYabove appear as files (/run/secrets/DB_PASS, /run/secrets/API_KEY); the mounted with-secrets.shwrapper script exports them as DB_PASSand API_KEYenvironment variables for your-original-command --with-args.
Decision points and alternatives
- Prefer native
*_FILEsupportif your app supports it (e.g., PostgreSQL’sPGPASSFILE). That avoids the wrapper entirely. - For multi-host or high-compliance deployments,use an external secrets manager (e.g., Hashicorp Vault, cloud KMS, SOPS) rather than Compose secrets.
- Build-time secretsare a separate concern; use BuildKit or dedicated build secret mechanisms to avoid leaking credentials into your image layers.
Risks and mitigations
- Risk:Accidentally logging or dumping environment variables
Mitigation: Neverprint environment variables in logs and restrict debug output - Risk:Secret file names that are not valid shell identifiers
Mitigation:Normalize or map file names to safe environment variable names before exporting - Risk:Secrets checked into
gitor other version control
Mitigation:Keep secret files out of repos, add strict.gitignorerules, and inject secrets via CI/CD or runtime provisioning
Final notes
This pattern is intentionally pragmatic: it preserves the security advantage of file-mounted secrets while letting unmodified apps keep using environment variables. It’s not a silver bullet for every environment–use it where Compose secrets are appropriate and pair it with stronger secret stores for production-grade, multi-host deployments.

