Module 2: Local & Container Deployment
7. Secrets and Local Security
Overview
In this capsule you'll learn to handle API keys, credentials, and sensitive data in your Docker Compose safely. By the end, your secrets will never be in your code, never in Git, and you'll have a clear flow to manage them across different environments.
Context: AI apps handle expensive API keys (OpenAI, Anthropic) and potentially sensitive user data. A leak of your OpenAI API key can cost you thousands of dollars in minutes. This capsule protects you from that.
The Problem: API Keys Everywhere
What you must NOT do
# ❌ NEVER hardcode API keys in code
client = OpenAI(api_key="sk-proj-abc123def456...")
# ❌ NEVER put keys in Dockerfiles
ENV OPENAI_API_KEY=sk-proj-abc123def456...
# ❌ NEVER commit .env with real keys
# If you do, the key is in the Git history FOREVER
What you SHOULD do
# ✅ Read from environment variables
import os
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
# ✅ Fail fast if the key doesn't exist
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise RuntimeError("OPENAI_API_KEY environment variable is required")
Level 1: .env Files (Basic)
# .env — Local file, NEVER in Git
OPENAI_API_KEY=sk-proj-your-real-key
ANTHROPIC_API_KEY=sk-ant-your-real-key
REDIS_PASSWORD=your-redis-password
# .env.example — Template, DO put in Git
OPENAI_API_KEY=sk-proj-replace-me
ANTHROPIC_API_KEY=sk-ant-replace-me
REDIS_PASSWORD=change-this
# .gitignore
.env
.env.production
.env.staging
!.env.example
# docker-compose.yml
services:
api:
env_file:
- .env # Loads all the variables from the file
Level 2: Docker Secrets (Compose)
For production, Docker Compose supports secrets as mounted files:
services:
api:
secrets:
- openai_key
environment:
- OPENAI_API_KEY_FILE=/run/secrets/openai_key
secrets:
openai_key:
file: ./secrets/openai_key.txt
# api/config.py — Read a secret from a file
import os
def read_secret(name: str) -> str:
"""Reads a secret from a Docker file or an environment variable."""
file_path = os.environ.get(f"{name}_FILE")
if file_path and os.path.exists(file_path):
with open(file_path) as f:
return f.read().strip()
return os.environ.get(name, "")
OPENAI_API_KEY = read_secret("OPENAI_API_KEY")
File structure for Docker Secrets
project/
├── secrets/ # NEVER in Git
│ ├── openai_key.txt # Contains only: sk-proj-...
│ ├── anthropic_key.txt
│ └── redis_password.txt
├── .gitignore # Includes secrets/
└── docker-compose.yml
# .gitignore
secrets/
!secrets/.gitkeep
Advantage over .env: Secrets are mounted as files in /run/secrets/, which is an in-memory filesystem (tmpfs). They never touch the container's disk.
Comparison: .env vs Docker Secrets vs Secret Managers
| Aspect | .env files | Docker Secrets | External Secret Manager |
|---|---|---|---|
| Complexity | Low | Medium | High |
| Security | Basic (file on disk) | Good (tmpfs, not on disk) | Excellent (encrypted, auditable) |
| Rotation | Manual (edit file, restart) | Manual (recreate secret, redeploy) | Automatic (API) |
| Auditing | None | Basic (who accesses the file) | Complete (who, when, what) |
| Multi-environment | .env.dev, .env.prod, etc. | Different secret per environment | Namespaces/paths per environment |
| Cost | $0 | $0 | $10-100+/month |
| When to use it | Local development | Staging, simple production | Production with compliance |
| Examples | — | Docker Swarm Secrets | AWS Secrets Manager, HashiCorp Vault |
Recommendation by stage
Local development → .env files (simple, fast)
Staging → Docker Secrets + .env fallback
Simple production → Docker Secrets
Enterprise production → External Secret Manager (AWS Secrets Manager, Vault)
Level 3: Validating Secrets at Startup
# api/config.py — Validate that secrets exist before accepting traffic
from pydantic_settings import BaseSettings
from pydantic import field_validator
class Settings(BaseSettings):
openai_api_key: str
redis_url: str = "redis://cache:6379"
@field_validator("openai_api_key")
@classmethod
def validate_api_key(cls, v):
if not v:
raise ValueError("OPENAI_API_KEY is required")
if v.startswith("sk-proj-replace") or v == "sk-your-key-here":
raise ValueError("OPENAI_API_KEY contains placeholder value")
if not v.startswith("sk-"):
raise ValueError("OPENAI_API_KEY format invalid (should start with sk-)")
return v
settings = Settings() # Fails on import if the key is invalid
Advanced validators with Pydantic
import re
from pydantic_settings import BaseSettings
from pydantic import field_validator, model_validator
class Settings(BaseSettings):
openai_api_key: str = ""
anthropic_api_key: str = ""
redis_url: str = "redis://cache:6379"
redis_password: str = ""
environment: str = "development"
log_level: str = "debug"
cache_ttl: int = 3600
model_name: str = "gpt-4o-mini"
max_tokens: int = 500
@field_validator("openai_api_key")
@classmethod
def validate_openai_key(cls, v):
if not v:
raise ValueError("OPENAI_API_KEY is required")
placeholders = ["replace", "your-key", "xxx", "change-this", "TODO"]
if any(p in v.lower() for p in placeholders):
raise ValueError("OPENAI_API_KEY contains a placeholder value — set a real key")
if not re.match(r"^sk-(proj-)?[a-zA-Z0-9_-]{20,}$", v):
raise ValueError(
"OPENAI_API_KEY format looks wrong. "
"Expected: sk-proj-... or sk-... with 20+ characters"
)
return v
@field_validator("anthropic_api_key")
@classmethod
def validate_anthropic_key(cls, v):
if not v:
return v
if not v.startswith("sk-ant-"):
raise ValueError("ANTHROPIC_API_KEY should start with 'sk-ant-'")
if len(v) < 30:
raise ValueError("ANTHROPIC_API_KEY looks too short")
return v
@field_validator("redis_url")
@classmethod
def validate_redis_url(cls, v):
if not v.startswith(("redis://", "rediss://")):
raise ValueError("REDIS_URL must start with redis:// or rediss://")
return v
@field_validator("cache_ttl")
@classmethod
def validate_cache_ttl(cls, v):
if v < 0:
raise ValueError("CACHE_TTL cannot be negative")
if v > 86400:
raise ValueError("CACHE_TTL too high (max 24 hours = 86400)")
return v
@field_validator("max_tokens")
@classmethod
def validate_max_tokens(cls, v):
if v < 1 or v > 128000:
raise ValueError("MAX_TOKENS must be between 1 and 128000")
return v
@field_validator("log_level")
@classmethod
def validate_log_level(cls, v):
valid = {"debug", "info", "warning", "error", "critical"}
if v.lower() not in valid:
raise ValueError(f"LOG_LEVEL must be one of: {valid}")
return v.lower()
@model_validator(mode="after")
def validate_production_settings(self):
if self.environment == "production":
if self.log_level == "debug":
raise ValueError("LOG_LEVEL should not be 'debug' in production")
if not self.redis_password:
raise ValueError("REDIS_PASSWORD is required in production")
return self
@property
def is_dev(self) -> bool:
return self.environment == "development"
@property
def detected_provider(self) -> str:
if self.openai_api_key:
return "openai"
if self.anthropic_api_key:
return "anthropic"
return "none"
class Config:
env_file = ".env"
# Usage in main.py
from config import Settings
try:
settings = Settings()
print(f"✅ Config valid | Provider: {settings.detected_provider} | Env: {settings.environment}")
except Exception as e:
print(f"❌ Config error: {e}")
exit(1)
Secret Rotation Workflow
Rotating API keys is something you'll do regularly: when a key is compromised, when an employee leaves the team, or simply as a good security practice.
Zero-downtime rotation flow
1. Generate a new key in the provider's dashboard
2. Add the new key to the environment (without removing the old one)
3. Verify that the new key works
4. Update containers with the new key
5. Revoke the old key
6. Verify that everything still works
Step-by-step implementation
# Step 1: Generate the new key
# Go to https://platform.openai.com/api-keys → Create new key
# New key: sk-proj-NEW_KEY_123...
# Step 2: Update .env with the new key
# (Save the old one as a temporary backup)
cp .env .env.backup
# Edit .env:
# OPENAI_API_KEY=sk-proj-NEW_KEY_123...
# Step 3: Verify the new key before deploying
curl https://api.openai.com/v1/models \
-H "Authorization: Bearer sk-proj-NEW_KEY_123..." \
-s | python -m json.tool | head -5
# Should return a list of models, not 401
# Step 4: Update the containers
docker compose up -d
# Compose detects the change in .env and recreates the containers
# Step 5: Verify that the app works with the new key
curl http://localhost:8000/health
# {"status":"healthy",...}
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt":"ping"}'
# Should respond normally
# Step 6: Revoke the old key in the dashboard
# OpenAI Dashboard → API Keys → Revoke old key
# Step 7: Clean up
rm .env.backup
Automated rotation script
#!/bin/bash
# rotate-key.sh — Rotates an API key with verification
KEY_NAME="${1:-OPENAI_API_KEY}"
NEW_VALUE="$2"
if [ -z "$NEW_VALUE" ]; then
echo "Usage: ./rotate-key.sh KEY_NAME NEW_VALUE"
echo "Example: ./rotate-key.sh OPENAI_API_KEY sk-proj-new-key..."
exit 1
fi
echo "=== Rotating $KEY_NAME ==="
# Backup
cp .env .env.backup-$(date +%Y%m%d-%H%M%S)
echo "✅ Backup created"
# Update .env
if grep -q "^${KEY_NAME}=" .env; then
sed -i.bak "s|^${KEY_NAME}=.*|${KEY_NAME}=${NEW_VALUE}|" .env
rm -f .env.bak
else
echo "${KEY_NAME}=${NEW_VALUE}" >> .env
fi
echo "✅ .env updated"
# Recreate containers
docker compose up -d
echo "✅ Containers recreated"
# Wait for the health checks to pass
echo "⏳ Waiting for health checks..."
sleep 10
# Verify
HEALTH=$(curl -s http://localhost:8000/health)
STATUS=$(echo "$HEALTH" | python -c "import sys,json; print(json.load(sys.stdin)['status'])" 2>/dev/null)
if [ "$STATUS" = "healthy" ]; then
echo "✅ Rotation successful — app healthy"
else
echo "❌ App not healthy after rotation"
echo "Health response: $HEALTH"
echo "Consider restoring the backup"
fi
Auditing Secrets in Docker Images
A common mistake: putting secrets in the Docker image during the build. Even if you delete them later, Docker's layers keep everything.
Verify that your image doesn't contain secrets
# See the image's layer history
docker history module-02-api --no-trunc
# Search for suspicious keywords
docker history module-02-api --no-trunc | grep -i "key\|secret\|password\|token"
# If something like this appears:
# ENV OPENAI_API_KEY=sk-proj-...
# → Your image has an embedded secret. DANGER.
Common mistakes that embed secrets
# ❌ BAD: ENV with a real key
ENV OPENAI_API_KEY=sk-proj-abc123
# ❌ BAD: COPY the .env into the image
COPY .env /app/.env
# ❌ BAD: ARG with a default that is a secret
ARG OPENAI_API_KEY=sk-proj-abc123
RUN echo "Key is $OPENAI_API_KEY" > /tmp/debug.log
# ✅ GOOD: Don't include secrets at build-time
# Secrets are passed as env vars at runtime (docker-compose.yml)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
.dockerignore to prevent leaks
# .dockerignore
.env
.env.*
!.env.example
secrets/
*.pem
*.key
.git/
__pycache__/
Without .dockerignore, the COPY . . in your Dockerfile copies EVERYTHING, including .env with your real keys. The .dockerignore is your first line of defense.
Multi-stage builds for extra security
# Build stage — may need secrets for tests, but they don't reach the final image
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Production stage — clean image, no build residue
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Automated secret scanning in images
# Use trivy to scan for secrets in images
# (install: brew install aquasecurity/trivy/trivy)
trivy image --scanners secret module-02-api
# Use dockle to audit the Dockerfile
# (install: brew install goodwithtech/r/dockle)
dockle module-02-api
Secrets in CI/CD
When your project lives on GitHub and you use GitHub Actions for CI/CD, you need a different flow for secrets.
GitHub Actions Secrets
# .github/workflows/deploy.yml
name: Deploy AI App
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and test
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
run: |
docker compose build
docker compose up -d
sleep 10
curl -f http://localhost:8000/health
docker compose down
Configure secrets in GitHub
1. Go to your repo → Settings → Secrets and variables → Actions
2. Click "New repository secret"
3. Name: OPENAI_API_KEY
4. Value: sk-proj-your-real-key
5. Click "Add secret"
Environment Secrets (per environment)
# .github/workflows/deploy.yml
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # Uses secrets from the "staging" environment
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: ./deploy.sh staging
deploy-production:
runs-on: ubuntu-latest
environment: production # Uses secrets from the "production" environment
needs: deploy-staging
steps:
- uses: actions/checkout@v4
- name: Deploy to production
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: ./deploy.sh production
Security rules in CI/CD
# ❌ NEVER print secrets in CI logs
echo $OPENAI_API_KEY # GitHub masks them, but don't trust that
# ❌ NEVER pass secrets as build arguments
docker build --build-arg OPENAI_API_KEY=$OPENAI_API_KEY .
# Build args stay in docker history
# ✅ Pass secrets only as env vars at runtime
docker run -e OPENAI_API_KEY=$OPENAI_API_KEY my-app
# ✅ Use Docker BuildKit for secrets at build time (if you need to)
DOCKER_BUILDKIT=1 docker build --secret id=openai_key,env=OPENAI_API_KEY .
# In the Dockerfile, with BuildKit secrets:
RUN --mount=type=secret,id=openai_key \
OPENAI_API_KEY=$(cat /run/secrets/openai_key) \
python -c "import openai; print('Key valid')"
# The secret does NOT stay in the layer — only available during the RUN
Security Checklist
Before committing or deploying:
- [ ] .env is not tracked by Git (verify with `git status`)
- [ ] .env.example exists and has placeholders (no real keys)
- [ ] .gitignore includes .env and secrets files
- [ ] .dockerignore includes .env and secrets/
- [ ] The code reads keys from environment variables (not hardcoded)
- [ ] The app fails fast if a key is missing (not on the first request)
- [ ] The API keys don't appear in logs (don't print/log keys)
- [ ] Docker images don't contain secrets (verify with docker history)
- [ ] CI/CD uses GitHub Secrets, not secrets in the repo
- [ ] Pydantic validates format and detects placeholders
What to Do If You Leaked a Secret
# 1. IMMEDIATELY: Rotate the key
# Go to OpenAI Dashboard → API Keys → Revoke → Create new key
# 2. Update the key in your environments
# .env, CI/CD secrets, managed platform env vars
# 3. Clean the Git history (if you committed it)
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch .env' HEAD
# 4. Force push (CAREFUL: destructive)
git push origin --force --all
# 5. Verify that the old key doesn't work
curl https://api.openai.com/v1/models \
-H "Authorization: Bearer sk-old-leaked-key"
# Should return 401 Unauthorized
Modern alternative to git filter-branch:
# BFG Repo-Cleaner (faster and safer)
# Install: brew install bfg
# Remove a file from the history
bfg --delete-files .env
# Remove specific text (the key) from the history
echo "sk-proj-abc123def456" > passwords.txt
bfg --replace-text passwords.txt
# Clean up
git reflog expire --expire=now --all
git gc --prune=now --aggressive
Hands-On Exercises
Exercise 1: Secure setup from scratch
Configure a project with .env, .env.example, .gitignore, and secret validation.
See solution
# 1. Create .env with your real keys
echo "OPENAI_API_KEY=sk-proj-your-real-key" > .env
# 2. Create .env.example with placeholders
echo "OPENAI_API_KEY=sk-proj-replace-me" > .env.example
# 3. Secure .gitignore
echo ".env" >> .gitignore
# 4. Verify that .env is NOT tracked
git status
# .env should NOT appear in "Changes to be committed" or "Untracked files"
# .env.example SHOULD appear
# 5. Add validation in config.py (see Level 3 above)
Exercise 2: Verify that your Docker image doesn't contain secrets
docker history module-02-api --no-trunc | grep -i key
# No API key should appear
See solution
If a key appears, your Dockerfile has an ENV or ARG with the key. Solution: use environment in Compose (runtime) instead of ENV in the Dockerfile (build time).
# Complete verification:
# 1. Check the image history
docker history module-02-api --no-trunc | grep -i "key\|secret\|password\|token"
# 2. Verify that .dockerignore exists and excludes .env
cat .dockerignore
# Should include: .env, .env.*, secrets/
# 3. Inspect the image's filesystem
docker run --rm module-02-api ls -la /app/
# .env or secrets/ should NOT appear
# 4. Inspect the image's env vars (not the container's)
docker inspect module-02-api --format='{{range .Config.Env}}{{println .}}{{end}}'
# Should only show system vars (PATH, PYTHON_VERSION, etc.)
# Should NOT show OPENAI_API_KEY
Exercise 3: Implement the read_secret function
Implement a function that reads secrets from Docker secret files OR environment variables.
See solution
def read_secret(name: str, default: str = "") -> str:
file_var = f"{name}_FILE"
file_path = os.environ.get(file_var)
if file_path:
try:
with open(file_path) as f:
return f.read().strip()
except FileNotFoundError:
pass
return os.environ.get(name, default)
# Usage:
OPENAI_API_KEY = read_secret("OPENAI_API_KEY")
Exercise 4: Implement key rotation with verification
Write a script that rotates the OPENAI_API_KEY: updates .env, recreates the containers, and verifies that the app stays healthy.
See solution
#!/bin/bash
# rotate-openai-key.sh
set -e
NEW_KEY="$1"
if [ -z "$NEW_KEY" ]; then
echo "Usage: ./rotate-openai-key.sh sk-proj-new-key"
exit 1
fi
echo "1. Verifying the new key with the OpenAI API..."
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://api.openai.com/v1/models \
-H "Authorization: Bearer $NEW_KEY")
if [ "$STATUS" != "200" ]; then
echo "❌ The new key is not valid (HTTP $STATUS)"
exit 1
fi
echo "✅ Valid key"
echo "2. Backing up .env..."
cp .env ".env.backup-$(date +%s)"
echo "3. Updating .env..."
sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$NEW_KEY|" .env
rm -f .env.bak
echo "4. Recreating containers..."
docker compose up -d
echo "5. Waiting for health checks..."
for i in $(seq 1 12); do
sleep 5
HEALTH=$(curl -s http://localhost:8000/health 2>/dev/null || echo '{"status":"waiting"}')
STATUS=$(echo "$HEALTH" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status','unknown'))" 2>/dev/null)
if [ "$STATUS" = "healthy" ]; then
echo "✅ App healthy with the new key"
echo "6. Rotation complete — revoke the old key in the OpenAI dashboard"
exit 0
fi
echo " Attempt $i/12: status=$STATUS"
done
echo "❌ App did not reach a healthy state after 60s"
echo "Restore .env from the backup if necessary"
exit 1
Exercise 5: Audit your complete Docker image
Create a script that audits a Docker image to verify it doesn't contain embedded secrets.
See solution
#!/bin/bash
# audit-image.sh
IMAGE="${1:-module-02-api}"
echo "=== Security audit: $IMAGE ==="
ISSUES=0
echo -e "\n--- 1. Searching for secrets in the history layers ---"
FOUND=$(docker history "$IMAGE" --no-trunc 2>/dev/null | grep -ic "key\|secret\|password\|token\|sk-proj\|sk-ant")
if [ "$FOUND" -gt 0 ]; then
echo "⚠️ Found $FOUND suspicious references in the history"
docker history "$IMAGE" --no-trunc | grep -i "key\|secret\|password\|token"
ISSUES=$((ISSUES + 1))
else
echo "✅ No secrets in the history"
fi
echo -e "\n--- 2. Checking for sensitive files in the image ---"
for FILE in .env .env.production secrets; do
EXISTS=$(docker run --rm "$IMAGE" ls -la "/app/$FILE" 2>/dev/null)
if [ -n "$EXISTS" ]; then
echo "⚠️ Sensitive file found: /app/$FILE"
ISSUES=$((ISSUES + 1))
fi
done
if [ "$ISSUES" -eq 0 ]; then
echo "✅ No sensitive files"
fi
echo -e "\n--- 3. Checking for embedded env vars ---"
ENV_SECRETS=$(docker inspect "$IMAGE" --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep -ic "key\|secret\|password")
if [ "$ENV_SECRETS" -gt 0 ]; then
echo "⚠️ Suspicious ENV vars in the image"
docker inspect "$IMAGE" --format='{{range .Config.Env}}{{println .}}{{end}}' | grep -i "key\|secret\|password"
ISSUES=$((ISSUES + 1))
else
echo "✅ No secrets in the image's ENV vars"
fi
echo -e "\n=== Result: $ISSUES issues found ==="
[ "$ISSUES" -eq 0 ] && echo "✅ Clean image" || echo "❌ Review the issues above"
Troubleshooting
"ValidationError: OPENAI_API_KEY is required" at startup
Your .env doesn't exist or doesn't have the variable. Verify:
# Does the file exist?
ls -la .env
# Does it have the variable?
grep OPENAI_API_KEY .env
# Does Docker Compose see it?
docker compose config | grep OPENAI
"The key works with curl but not in the container"
The container may be using an old .env or a cached variable:
# See what value the container has
docker compose exec api env | grep OPENAI
# If it's different from your current .env, recreate:
docker compose down
docker compose up -d
"docker history shows my API key"
Your Dockerfile has ENV OPENAI_API_KEY=... or an ARG with the key. Remove it, use runtime env vars via Compose, and rebuild with --no-cache:
docker compose build --no-cache api
# Verify that the key no longer appears:
docker history module-02-api --no-trunc | grep -i key
IMPORTANT: If the image with the secret was pushed to a registry, delete it from the registry too.
"Secrets don't update after changing .env"
Docker Compose doesn't automatically detect changes in .env for already-running containers:
# Force recreation
docker compose up -d --force-recreate
# or
docker compose down && docker compose up -d
"GitHub Actions doesn't find the secret"
Verify that the secret name in the workflow matches exactly the name in Settings:
# In the workflow:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# The name after "secrets." must match EXACTLY
# the name in Settings → Secrets
GitHub secrets are case-sensitive and don't allow spaces.
Summary
- Never hardcode API keys in code or Dockerfiles.
- Use .env files for development, Docker secrets for production.
- Validate secrets at startup — fail fast if they're missing.
- .gitignore must include .env; .env.example must exist with placeholders.
- Pydantic Settings validates format, detects placeholders, and checks rules per environment.
- If you leak a secret: rotate immediately, then clean the history.
- Don't log secrets — careful with
print(os.environ)when debugging. - Audit your images with
docker history— build-time secrets stay in the layers. - In CI/CD, use GitHub Secrets per environment — never secrets in the repo's code.
Additional Resources
- Docker Compose Secrets — Official reference
- OpenAI API Key Safety — OpenAI best practices
- git-secrets — Tool to prevent committing secrets
- truffleHog — Secret scanner for Git repos
- 12 Factor App — Config — Principle of config in env vars
- GitHub Actions Encrypted Secrets — Secrets in CI/CD
- Docker BuildKit Secrets — Secure build-time secrets
- BFG Repo-Cleaner — Clean secrets from Git history