Secrets and environment variables in Docker containers require careful handling to avoid leaking credentials into image layers, container logs, or environment variable dumps. The most common mistakes are: hardcoding credentials in Dockerfiles (they persist in image layer history forever), passing secrets via --env or --env-file flags (visible in docker inspect output and the process environment), and storing secrets in Docker images via COPY or RUN commands (persisted in layers even after deletion). Docker provides two mechanisms for secure secret management: Docker Secrets (available in Docker Swarm, mounts secrets as tmpfs files) and BuildKit build secrets (available during docker build, not stored in image layers). This guide covers all patterns for secure secret handling in Docker containers on RHEL 9.
Prerequisites
- Docker Engine installed on RHEL 9
Step 1 — Avoid Secrets in Dockerfiles (Build Secrets)
# WRONG — API key stored permanently in image layer:
# RUN pip install --index-url https://user:[email protected]/simple/ mypackage
# CORRECT — BuildKit secrets (not stored in any layer):
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=secret,id=pip_token
pip install --index-url https://$(cat /run/secrets/pip_token)@pypi.example.com/simple/ -r requirements.txt
COPY . .
# Build with the secret (never stored in image)
DOCKER_BUILDKIT=1 docker build
--secret id=pip_token,src=/root/.pip-token
-t myapp:latest .
Step 2 — Environment Variables — Right and Wrong
# WRONG — visible in docker inspect and process list:
# docker run -e DB_PASSWORD=secret123 myapp
# BETTER — use .env file (not committed to Git):
echo 'DB_PASSWORD=SecurePass123!' > /run/secrets/app.env
chmod 600 /run/secrets/app.env
docker run --env-file /run/secrets/app.env myapp
# BEST — mount credentials as files, not environment variables:
docker run -v /run/secrets/db_password:/run/secrets/db_password:ro myapp
# Application reads: fs.readFileSync('/run/secrets/db_password', 'utf8').trim()
Step 3 — Docker Swarm Secrets
# Create a Docker Swarm secret (encrypted at rest and in transit)
echo 'SecurePass123!' | docker secret create db_password -
# Use the secret in a service (mounted as /run/secrets/db_password in the container)
docker service create
--name myapp
--secret db_password
--env DB_PASSWORD_FILE=/run/secrets/db_password
myapp:latest
# Application reads the password from file:
# const password = fs.readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim()
Step 4 — Docker Compose with Secrets
# compose.yaml — file-based secrets for Docker Compose
services:
app:
image: myapp:latest
secrets:
- db_password
environment:
# Point to the secret file path, not the value
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
file: /run/secrets/db_password # File on host — not committed to Git
Step 5 — Inspect and Verify No Secret Leakage
# Check that no secrets appear in environment variables
docker inspect myapp | python3 -c "import sys,json; e=json.load(sys.stdin)[0]['Config']['Env']; print('n'.join(e))"
# Check image history for leaked secrets
docker history --no-trunc myapp:latest | grep -i 'password|secret|token|key'
# Scan image layers for secrets (install truffleHog or similar)
docker save myapp:latest | trufflehog --json filesystem -
Conclusion
The core principle for Docker secrets on RHEL 9 is: credentials should only exist in memory or as temporary files, never in image layers, environment variables visible to all processes, or version control. The file-based pattern (writing secrets to a tmpfs-backed file and reading them via SECRET_FILE environment variables) works across Docker Compose, Docker Swarm, and Kubernetes (which mounts Secrets as files at /run/secrets/) — making applications portable between orchestration platforms without code changes.
Next steps: How to Build Multi-Stage Docker Images on RHEL 9, How to Set Up a Private Docker Registry on RHEL 9, and How to Configure Docker Daemon TLS Encryption on RHEL 9.