Environment Variables in Production¶
Managing environment variables properly is critical for secure Django deployments across different environments.
Why Environment Variables?¶
Environment variables keep sensitive data out of your codebase:
- Database credentials
- API keys and secrets
- Django SECRET_KEY
- Third-party service credentials
- Environment-specific configurations
Environment Types¶
Development¶
Local machine, debug mode enabled, fake data acceptable.
Staging¶
Production-like environment for testing, real-like data, no debug mode.
Production¶
Live environment, real users, highest security, no debug mode.
Django Settings Pattern¶
Base Settings Structure¶
# settings/
# __init__.py
# base.py # Common settings
# dev.py # Development
# staging.py # Staging
# prod.py # Production
# settings/base.py
import os
from pathlib import Path
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent
# Load .env file
load_dotenv(BASE_DIR / '.env')
SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False').lower() in ('true', '1', 't')
ALLOWED_HOSTS = [h.strip() for h in os.getenv('ALLOWED_HOSTS', '').split(',') if h.strip()]
# Common settings for all environments
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
# ... your apps
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
# ... middleware
]
Development Settings¶
# settings/dev.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Development-only apps
INSTALLED_APPS += [
'debug_toolbar',
]
MIDDLEWARE += [
'debug_toolbar.middleware.DebugToolbarMiddleware',
]
# Email to console
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Production Settings¶
# settings/prod.py
import os
from .base import *
DEBUG = False
ALLOWED_HOSTS = [h.strip() for h in os.getenv('ALLOWED_HOSTS', '').split(',') if h.strip()]
# Security settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# HSTS
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
# Static files
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'ERROR',
'class': 'logging.FileHandler',
'filename': '/var/log/django/error.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'ERROR',
'propagate': True,
},
},
}
Using Different Settings¶
# manage.py
import os
import sys
if __name__ == '__main__':
# Default to development
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings.dev')
# ... rest of manage.py
# Run with specific settings
python manage.py runserver --settings=myproject.settings.dev
python manage.py migrate --settings=myproject.settings.prod
# Or set environment variable
export DJANGO_SETTINGS_MODULE=myproject.settings.prod
python manage.py runserver
AWS-Specific Solutions¶
AWS Systems Manager Parameter Store¶
Store secrets in AWS Parameter Store:
# Store parameters
aws ssm put-parameter \
--name "/myapp/prod/SECRET_KEY" \
--value "your-secret-key" \
--type "SecureString"
aws ssm put-parameter \
--name "/myapp/prod/DB_PASSWORD" \
--value "your-db-password" \
--type "SecureString"
# settings/prod.py
import boto3
def get_parameter(param_name):
"""Fetch parameter from AWS Parameter Store"""
ssm = boto3.client('ssm', region_name='us-east-1')
response = ssm.get_parameter(
Name=param_name,
WithDecryption=True
)
return response['Parameter']['Value']
# Use in settings
SECRET_KEY = get_parameter('/myapp/prod/SECRET_KEY')
DB_PASSWORD = get_parameter('/myapp/prod/DB_PASSWORD')
AWS Secrets Manager¶
More advanced secret management:
import boto3
import json
from botocore.exceptions import ClientError
def get_secret(secret_name, region_name='us-east-1'):
"""Retrieve secret from AWS Secrets Manager"""
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
response = client.get_secret_value(SecretId=secret_name)
if 'SecretString' in response:
return json.loads(response['SecretString'])
else:
return response['SecretBinary']
except ClientError as e:
raise e
# In settings
secrets = get_secret('myapp/prod/database')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': secrets['dbname'],
'USER': secrets['username'],
'PASSWORD': secrets['password'],
'HOST': secrets['host'],
'PORT': secrets['port'],
}
}
AWS Elastic Beanstalk¶
Set environment variables in EB configuration:
# Using EB CLI
eb setenv SECRET_KEY="your-secret-key" \
DEBUG=False \
DB_NAME=mydb \
DB_USER=myuser \
DB_PASSWORD=mypassword
# Or in .ebextensions/environment.config
option_settings:
aws:elasticbeanstalk:application:environment:
DJANGO_SETTINGS_MODULE: myproject.settings.prod
PYTHONPATH: /opt/python/current/app
AWS EC2 with User Data¶
Pass environment variables on instance launch:
#!/bin/bash
# User data script
export SECRET_KEY="your-secret-key"
export DB_PASSWORD="your-db-password"
# Or write to file
cat > /etc/environment << EOF
SECRET_KEY="your-secret-key"
DB_PASSWORD="your-db-password"
DJANGO_SETTINGS_MODULE="myproject.settings.prod"
EOF
AWS Lambda¶
Environment variables in Lambda configuration:
# serverless.yml (Serverless Framework)
provider:
name: aws
runtime: python3.11
environment:
SECRET_KEY: ${env:SECRET_KEY}
DB_HOST: ${env:DB_HOST}
DEBUG: false
# Or in AWS Console:
# Configuration > Environment variables
Using python-dotenv¶
Best practice for reading environment variables:
# settings.py
import os
from pathlib import Path
from dotenv import load_dotenv
# Build paths
BASE_DIR = Path(__file__).resolve().parent.parent
# Load environment variables from .env file
load_dotenv(BASE_DIR / '.env')
# String values
SECRET_KEY = os.getenv('SECRET_KEY')
DB_NAME = os.getenv('DB_NAME', 'mydb') # with default
# Boolean values
DEBUG = os.getenv('DEBUG', 'False').lower() in ('true', '1', 't')
# Integer values
MAX_CONNECTIONS = int(os.getenv('MAX_CONNECTIONS', '10'))
# Lists (comma-separated)
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
# Custom parsing
def parse_redis_url(url):
# Custom parsing logic
return parsed_config
REDIS_URL = parse_redis_url(os.getenv('REDIS_URL', ''))
# .env file (never commit this!)
SECRET_KEY=your-development-secret-key
DEBUG=True
DB_NAME=dev_database
ALLOWED_HOSTS=localhost,127.0.0.1
MAX_CONNECTIONS=5
Helper Functions for Type Conversion¶
Create utility functions for common conversions:
# settings/utils.py
import os
def get_bool(key, default='False'):
"""Convert environment variable to boolean"""
value = os.getenv(key, default)
return value.lower() in ('true', '1', 't', 'yes')
def get_int(key, default=0):
"""Convert environment variable to integer"""
try:
return int(os.getenv(key, str(default)))
except ValueError:
return default
def get_list(key, default='', separator=','):
"""Convert environment variable to list"""
value = os.getenv(key, default)
return [item.strip() for item in value.split(separator) if item.strip()]
def get_db_config():
"""Parse DATABASE_URL or individual database settings"""
database_url = os.getenv('DATABASE_URL')
if database_url:
# Parse DATABASE_URL format: postgresql://user:pass@host:port/dbname
import re
pattern = r'(?P<engine>\w+)://(?P<user>[^:]+):(?P<password>[^@]+)@(?P<host>[^:]+):(?P<port>\d+)/(?P<name>.+)'
match = re.match(pattern, database_url)
if match:
engine_map = {
'postgresql': 'django.db.backends.postgresql',
'mysql': 'django.db.backends.mysql',
'sqlite': 'django.db.backends.sqlite3',
}
return {
'ENGINE': engine_map.get(match.group('engine')),
'NAME': match.group('name'),
'USER': match.group('user'),
'PASSWORD': match.group('password'),
'HOST': match.group('host'),
'PORT': match.group('port'),
}
# Fallback to individual settings
return {
'ENGINE': os.getenv('DB_ENGINE', 'django.db.backends.postgresql'),
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': os.getenv('DB_PORT', '5432'),
}
# settings.py
from pathlib import Path
from dotenv import load_dotenv
from .utils import get_bool, get_int, get_list, get_db_config
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')
# Use helper functions
DEBUG = get_bool('DEBUG', 'False')
ALLOWED_HOSTS = get_list('ALLOWED_HOSTS')
MAX_CONNECTIONS = get_int('MAX_CONNECTIONS', 10)
# Database
DATABASES = {
'default': get_db_config()
}
DEBUG=False
SECRET_KEY=your-secret-key
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Or individual settings:
# DB_NAME=mydb
# DB_USER=myuser
# DB_PASSWORD=mypassword
# DB_HOST=localhost
# DB_PORT=5432
Environment File Templates¶
.env.example¶
# .env.example - Commit this to version control
# Copy to .env and fill in real values
# Django
SECRET_KEY=generate-a-random-secret-key
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
# Database
DB_ENGINE=django.db.backends.postgresql
DB_NAME=mydb
DB_USER=myuser
DB_PASSWORD=changeme
DB_HOST=localhost
DB_PORT=5432
# Email
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
EMAIL_USE_TLS=True
# AWS
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_STORAGE_BUCKET_NAME=your-bucket
AWS_S3_REGION_NAME=us-east-1
# External APIs
STRIPE_PUBLIC_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
SENTRY_DSN=https://xxx@sentry.io/xxx
Docker Environment Variables¶
Docker Compose¶
# docker-compose.yml
version: '3.8'
services:
web:
build: .
environment:
- SECRET_KEY=${SECRET_KEY}
- DEBUG=False
- DB_HOST=db
env_file:
- .env
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Dockerfile¶
# Dockerfile
FROM python:3.11-slim
# Accept build arguments
ARG DJANGO_SETTINGS_MODULE=myproject.settings.prod
ENV DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE}
# Environment variables set at runtime
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Build-time secrets (not recommended for sensitive data)
ARG SECRET_KEY
ENV SECRET_KEY=${SECRET_KEY}
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
Best Practices¶
1. Never Commit Secrets¶
2. Different Keys per Environment¶
# Each environment should have unique keys
# Development: dev-secret-key-xxx
# Staging: staging-secret-key-xxx
# Production: prod-secret-key-xxx
3. Rotate Secrets Regularly¶
# Implement secret rotation
from datetime import datetime, timedelta
def should_rotate_secret(last_rotation_date):
"""Check if secret should be rotated (every 90 days)"""
rotation_period = timedelta(days=90)
return datetime.now() - last_rotation_date > rotation_period
4. Use Secret Management Services¶
- AWS Secrets Manager
- AWS Parameter Store
- HashiCorp Vault
- Azure Key Vault
- Google Secret Manager
5. Least Privilege Principle¶
# IAM Policy for accessing secrets
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:account-id:secret:myapp/*"
]
}
]
}
6. Validate Environment Variables¶
# settings.py
import sys
REQUIRED_ENV_VARS = [
'SECRET_KEY',
'DB_NAME',
'DB_USER',
'DB_PASSWORD',
]
missing_vars = [var for var in REQUIRED_ENV_VARS if not config(var, default=None)]
if missing_vars:
print(f"Error: Missing required environment variables: {', '.join(missing_vars)}")
sys.exit(1)
7. Audit Access to Secrets¶
import logging
logger = logging.getLogger(__name__)
def get_secret_with_audit(secret_name):
"""Get secret and log access"""
logger.info(f"Accessing secret: {secret_name}")
secret = get_secret(secret_name)
logger.info(f"Secret {secret_name} accessed successfully")
return secret
Security Checklist¶
- All secrets in environment variables, not code
-
.envfile in.gitignore -
.env.examplecommitted for reference - Different secrets per environment
- Secrets encrypted at rest (AWS Secrets Manager, etc.)
- Minimal permissions for accessing secrets
- Regular secret rotation schedule
- Audit logging for secret access
- No secrets in Docker images
- Secrets removed from logs
- Backup strategy for secrets
Common Pitfalls to Avoid¶
1. Hardcoding Secrets¶
2. Committing .env Files¶
3. Exposing Secrets in Logs¶
# DON'T
logger.info(f"Database password: {DB_PASSWORD}")
# DO
logger.info("Database connection established")
4. Using Weak Default Values¶
# DON'T
SECRET_KEY = config('SECRET_KEY', default='weak-default')
# DO
SECRET_KEY = config('SECRET_KEY') # Fail if not set