# ❌ WRONG
COPY .env .env
RUN echo "API_KEY=$API_KEY" >> config.env
CMD ["python", "app.py"]
Or even simpler:
# ❌ WRONG
ARG API_KEY
RUN curl https://api.example.com/validate?key=$API_KEY
You think the secret is deleted after the build? It’s not.
Docker layer caching. Each RUN, COPY, and ADD creates a layer. Layers are immutable and can be inspected:
docker history your-image
docker save your-image | tar -x
# Extract any layer and grep for secrets
The secret persists in that layer’s filesystem, even if a later layer deletes the file.
Git history. Commit .env once by mistake:
git log -p -- .env
# Secret visible forever (even after deletion)
Registry access. Push the image anywhere — public registry, private registry, DockerHub, GitHub Container Registry:
docker pull ghcr.io/attacker/your-image
docker save your-image | tar -x
# Extract the layers, find the secret
Anyone with image access has the secret.
CI/CD logs. Docker build output appears in GitHub Actions, GitLab CI, Jenkins:
docker build --build-arg API_KEY=$API_KEY .
# Build log captures the secret
Build context leaks. Even if you don’t COPY .env, the build context includes it:
docker build . # includes all files in current directory
Unless .dockerignore explicitly excludes it.
Scenario 1: Shared repository
Scenario 2: Registry compromise
Scenario 3: Shared CI/CD logs
Scenario 4: Accidental push
Build time: No secrets.
# ✅ CORRECT
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
No ARG, no .env, no secrets in the image. The image is generic and shareable.
Runtime: Inject secrets.
# Local dev
export API_KEY="dev-key-here"
docker run -e API_KEY=$API_KEY your-image
# Docker Compose
# .env file (gitignored) sits next to compose.yml
# compose.yml reads it
docker compose up -d
# Kubernetes
kubectl create secret generic api-key --from-literal=key=$API_KEY
# Pod mounts it as env var or volume
# Cloud (AWS, GCP, Azure)
# Secrets Manager, Parameter Store, Key Vault
# Platform injects at runtime
Why this works:
Baking secrets into Docker images trades away:
Injecting at runtime costs nothing:
Do it once, do it right.