Skip to main content

Docker Python Secrets Management: Secure Your Credentials

Hardcoding database passwords, API keys, or AWS credentials in your Dockerfile or Compose file is a critical security vulnerability. Credentials in images are readable by anyone with image access; they're baked in permanently and visible in git history. This article teaches you three secure patterns for managing secrets in Docker Python applications: .env files for development, Docker secrets for production Swarm, and integration with cloud secret managers like AWS Secrets Manager for scaled deployments.

I once found an AWS production key hardcoded in a colleague's git history. By the time we discovered it, an attacker had racked up 40,000 in unexpected charges using the key. Proper secrets management prevents these disasters.

The Problem: Hardcoded Secrets Are Dangerous

Here's a Dockerfile you should never write:

FROM python:3.11-slim

ENV DATABASE_URL=postgresql://user:MyActualPassword123@postgres:5432/myapp
ENV AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
ENV AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

COPY . /app
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

Problems:

  1. Visible in image: Anyone with docker history myapp or image access sees the keys.
  2. Searchable in git: If this was committed, even a force-push doesn't remove it from history. Tools like GitGuardian scan for exposed keys automatically.
  3. Hard to rotate: Changing credentials requires rebuilding and redeploying the entire image.
  4. Non-environment-specific: The same credentials appear in dev, test, and production.

Secrets must be injected at runtime, not baked into images.

Pattern 1: .env Files for Local Development

For local development, create a .env file (gitignored) with your secrets:

# .env file (never commit this)
DATABASE_URL=postgresql://postgres:devpassword@localhost:5432/myapp
REDIS_URL=redis://localhost:6379/0
API_KEY=sk-1234567890abcdefghij
DEBUG=true

Add to .gitignore:

.env
.env.local
.env.*.local

Load in your Python app using python-dotenv:

from dotenv import load_dotenv
import os

load_dotenv()

DATABASE_URL = os.getenv("DATABASE_URL")
REDIS_URL = os.getenv("REDIS_URL")
API_KEY = os.getenv("API_KEY")
DEBUG = os.getenv("DEBUG", "false").lower() == "true"

In docker-compose.yml, reference the .env file:

version: '3.9'

services:
app:
build: .
env_file:
- .env
environment:
- ENVIRONMENT=development

When you run docker compose up, Compose loads .env and passes variables to containers. This keeps secrets out of Dockerfiles and git.

Note: .env is only for development. For production, use environment variable injection from your deployment platform.

Pattern 2: Docker Secrets for Swarm (Production)

If you're using Docker Swarm (smaller teams, single-datacenter deployments), Docker provides a secrets feature. Secrets are encrypted and available only to authorized services.

First, create a secret from a file:

# Create a secret from a file
echo "superSecretDatabasePassword" | docker secret create db_password -

# Or from a file
docker secret create api_key /path/to/api_key.txt

Reference the secret in docker-compose.yml:

version: '3.9'

services:
app:
image: myapp:1.0
environment:
# Read from secret, exposed as /run/secrets/db_password
DATABASE_URL=postgresql://user:/run/secrets/db_password@postgres:5432/myapp
secrets:
- db_password
- api_key

secrets:
db_password:
external: true
api_key:
external: true

In your Python code, read from the mounted secrets file:

import os

# For Docker Swarm secrets
if os.path.exists("/run/secrets/db_password"):
with open("/run/secrets/db_password", "r") as f:
db_password = f.read().strip()
DATABASE_URL = f"postgresql://user:{db_password}@postgres:5432/myapp"
else:
# Fall back to environment variable for development
DATABASE_URL = os.getenv("DATABASE_URL")

Secrets are encrypted at rest, accessible only to authorized services, and never leak into logs or environment variables.

Pattern 3: Cloud Secret Managers (AWS, GCP, Azure)

For scaling (multiple servers, Kubernetes), use cloud secret managers like AWS Secrets Manager, Google Cloud Secret Manager, or HashiCorp Vault. These centralize secrets, version them, and rotate them without redeploying.

Example using AWS Secrets Manager:

import json
import boto3
import os

def get_secret(secret_name, region="us-east-1"):
"""Fetch a secret from AWS Secrets Manager."""
client = boto3.client("secretsmanager", region_name=region)
try:
response = client.get_secret_value(SecretId=secret_name)
# If secret is stored as JSON, parse it
return json.loads(response["SecretString"])
except Exception as e:
print(f"Error fetching secret: {e}")
return None

# Load secrets at startup
secrets = get_secret("prod/database")
DATABASE_URL = f"postgresql://{secrets['username']}:{secrets['password']}@{secrets['host']}:{secrets['port']}/{secrets['dbname']}"

# Or individual secrets
api_key = get_secret("prod/api_key")["key"]

Your Docker container needs IAM permissions to fetch secrets:

FROM python:3.11-slim

RUN pip install boto3

COPY . /app
WORKDIR /app

# The container runs with an IAM role that can read Secrets Manager
CMD ["python", "app.py"]

When deployed to AWS ECS or EKS, the container inherits IAM credentials from the task role and can fetch secrets without hardcoding anything.

A Complete Example: Three Environments

Here's a best-practice structure for development, staging, and production:

Development (.env):

DATABASE_URL=postgresql://postgres:devpass@localhost:5432/myapp
API_KEY=dev_test_key_123
DEBUG=true

Docker Compose (docker-compose.yml):

version: '3.9'

services:
app:
build: .
env_file: .env
environment:
- ENVIRONMENT=development

Production (AWS ECS Task Definition):

{
"containerDefinitions": [
{
"name": "myapp",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:1.0",
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/database-url"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/api-key"
}
],
"taskRoleArn": "arn:aws:iam::123456789:role/ecsTaskRole"
}
]
}

Each environment uses the appropriate secrets mechanism:

  • Dev: .env file
  • Prod: AWS Secrets Manager via IAM role

No credentials are ever hardcoded.

Key Takeaways

  • Never hardcode secrets in Dockerfiles, Compose files, or code.
  • For development: use .env files (gitignored) with python-dotenv.
  • For Swarm: use Docker secrets (encrypted, service-scoped).
  • For cloud scale: use AWS Secrets Manager, GCP Secret Manager, or Vault.
  • Always use environment variable injection at runtime, never at build time.

Frequently Asked Questions

What if I accidentally commit a secret to git?

Use git-filter-repo to remove it from history: git filter-repo --path .env --invert-paths. For leaked credentials, rotate them immediately (change the password, regenerate the API key). Tools like GitGuardian alert you to exposed keys automatically.

Can I use environment variables directly in Dockerfiles?

No. ENV SECRET=value bakes it into the image. Environment variables must be passed at runtime with docker run -e SECRET=value or via Compose's env_file or environment sections.

How do I debug a Compose service that can't access a secret?

Run docker compose exec app env | grep SECRET to verify the variable exists. Check file permissions: docker compose exec app cat /run/secrets/db_password.

Is AWS Secrets Manager expensive?

AWS charges 0.40 per secret per month plus per-request fees. For small apps (1–10 secrets), it's negligible (a few dollars monthly). For large enterprises, the cost is worth the security and compliance.

Can I rotate secrets without redeploying?

Yes, in cloud secret managers (AWS, GCP, Vault). Update the secret, and applications that fetch it at runtime pick up the new value. Swarm and Compose require redeploying for changes.

Further Reading