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 Compose files 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 secrets in 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 files and 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 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 -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_PASS and API_KEY above appear as files (/run/secrets/DB_PASS, /run/secrets/API_KEY); the mounted with-secrets.sh wrapper script exports them as DB_PASS and API_KEY environment variables for your-original-command --with-args.
Decision points and alternatives
- Prefer native
*_FILEsupport if 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 secrets are 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: Never print 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.


