Django Integration
Use KeyEnv with Django applications for secure secrets management.
Django Integration
Integrate KeyEnv with your Django application to securely manage secrets like database credentials, API keys, and the Django secret key.
Features
- Load secrets before Django initializes
- Environment-aware configuration (development, staging, production)
- Works with django-environ and python-decouple patterns
- Supports DATABASE_URL parsing
- Type-safe with full Python type annotations
Installation
pip install keyenvOr with poetry:
poetry add keyenvFor DATABASE_URL parsing, also install dj-database-url:
pip install dj-database-urlQuick Start
Add KeyEnv to the top of your settings.py:
# settings.py
from keyenv import KeyEnv
import os
# Load secrets before anything else
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(
os.environ["KEYENV_PROJECT"],
os.environ.get("DJANGO_ENV", "development")
)
# Now use secrets in your settings
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"Django Settings Integration
Basic Setup
The simplest approach loads all secrets into os.environ at startup:
# settings.py
from keyenv import KeyEnv
import os
# Load secrets from KeyEnv
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
env = os.environ.get("DJANGO_ENV", "development")
client.load_env(os.environ["KEYENV_PROJECT"], env)
# Django settings
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")
# Database
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
"NAME": os.environ.get("DB_NAME", "myapp"),
"USER": os.environ.get("DB_USER", "postgres"),
"PASSWORD": os.environ.get("DB_PASSWORD", ""),
}
}
# Email
EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")With DATABASE_URL
Use dj-database-url for cleaner database configuration:
# settings.py
from keyenv import KeyEnv
import dj_database_url
import os
# Load secrets
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
# Parse DATABASE_URL automatically
DATABASES = {
"default": dj_database_url.config(
default="postgres://localhost/myapp",
conn_max_age=600,
conn_health_checks=True,
)
}Store your database URL in KeyEnv as:
DATABASE_URL=postgres://user:password@host:5432/dbnameEnvironment-Aware Configuration
Automatic Environment Detection
Detect the environment from various sources:
# settings.py
from keyenv import KeyEnv
import os
def get_environment():
"""Determine environment from various sources."""
# Check explicit setting first
if env := os.environ.get("DJANGO_ENV"):
return env
# Check common deployment indicators
if os.environ.get("RAILWAY_ENVIRONMENT"):
return os.environ["RAILWAY_ENVIRONMENT"]
if os.environ.get("RENDER"):
return "production"
if os.environ.get("HEROKU_APP_NAME"):
return "production"
# Default to development
return "development"
ENVIRONMENT = get_environment()
# Load environment-specific secrets
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], ENVIRONMENT)
# Environment-specific settings
DEBUG = ENVIRONMENT == "development"Split Settings Pattern
For larger projects, use environment-specific settings files:
# settings/base.py
from keyenv import KeyEnv
import os
ENVIRONMENT = os.environ.get("DJANGO_ENV", "development")
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], ENVIRONMENT)
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
# Common settings...
INSTALLED_APPS = [...]
MIDDLEWARE = [...]# settings/development.py
from .base import *
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]# settings/production.py
from .base import *
DEBUG = False
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
# Production-specific settings
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = TrueUsing with django-environ
If you prefer django-environ's API:
# settings.py
from keyenv import KeyEnv
import environ
import os
# Load KeyEnv secrets first
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))
# Initialize django-environ (reads from os.environ)
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, ["localhost"]),
)
# Use django-environ's typed accessors
SECRET_KEY = env("DJANGO_SECRET_KEY")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
DATABASES = {
"default": env.db("DATABASE_URL", default="postgres://localhost/myapp")
}
EMAIL_CONFIG = env.email_url("EMAIL_URL", default="smtp://localhost:25")Using with python-decouple
If you prefer python-decouple's pattern:
# settings.py
from keyenv import KeyEnv
from decouple import config, Csv
import os
# Load KeyEnv secrets first
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))
# python-decouple reads from os.environ (and .env as fallback)
SECRET_KEY = config("DJANGO_SECRET_KEY")
DEBUG = config("DEBUG", default=False, cast=bool)
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost", cast=Csv())
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"HOST": config("DB_HOST", default="localhost"),
"PORT": config("DB_PORT", default=5432, cast=int),
"NAME": config("DB_NAME", default="myapp"),
"USER": config("DB_USER", default="postgres"),
"PASSWORD": config("DB_PASSWORD", default=""),
}
}Complete Example
Here's a complete settings.py for a production Django application:
# settings.py
from keyenv import KeyEnv
from pathlib import Path
import dj_database_url
import os
# Build paths
BASE_DIR = Path(__file__).resolve().parent.parent
# Determine environment
ENVIRONMENT = os.environ.get("DJANGO_ENV", "development")
# Load secrets from KeyEnv
if token := os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=token)
project_id = os.environ.get("KEYENV_PROJECT")
if project_id:
client.load_env(project_id, ENVIRONMENT)
# Core settings
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost").split(",")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# Your apps
"myapp",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "myproject.urls"
WSGI_APPLICATION = "myproject.wsgi.application"
# Database
DATABASES = {
"default": dj_database_url.config(
default="postgres://localhost/myapp",
conn_max_age=600,
conn_health_checks=True,
)
}
# Cache (optional)
if redis_url := os.environ.get("REDIS_URL"):
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": redis_url,
}
}
# Email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "true").lower() == "true"
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "[email protected]")
# Security (production)
if ENVIRONMENT == "production":
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Static files
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Third-party services
STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "")
STRIPE_WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "")
SENTRY_DSN = os.environ.get("SENTRY_DSN", "")
# Initialize Sentry
if SENTRY_DSN:
import sentry_sdk
sentry_sdk.init(
dsn=SENTRY_DSN,
environment=ENVIRONMENT,
traces_sample_rate=0.1,
)Setting Up Your Deployment
Required Environment Variables
Set these in your deployment platform (not in KeyEnv):
| Variable | Description |
|---|---|
KEYENV_TOKEN | Your KeyEnv service token |
KEYENV_PROJECT | Your KeyEnv project ID |
DJANGO_ENV | Environment name (development, staging, production) |
Secrets to Store in KeyEnv
Store these secrets in KeyEnv for each environment:
| Secret | Description | Example |
|---|---|---|
DJANGO_SECRET_KEY | Django secret key | django-insecure-abc123... |
DATABASE_URL | Database connection string | postgres://user:pass@host/db |
REDIS_URL | Redis connection (optional) | redis://localhost:6379/0 |
EMAIL_HOST_PASSWORD | SMTP password | app-specific-password |
STRIPE_SECRET_KEY | Stripe API key | sk_live_... |
SENTRY_DSN | Sentry DSN | https://[email protected]/... |
ALLOWED_HOSTS | Comma-separated hosts | example.com,www.example.com |
Generate a Django Secret Key
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"Local Development
For local development without KeyEnv:
# settings.py - handles missing KEYENV_TOKEN gracefully
if os.environ.get("KEYENV_TOKEN"):
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], "development")
# Falls back to local .env file or defaults
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key-not-for-production")Or use the KeyEnv CLI to generate a .env file:
keyenv pull --env development --output .envThen load it with python-dotenv in development:
# settings.py
import os
if os.environ.get("KEYENV_TOKEN"):
from keyenv import KeyEnv
client = KeyEnv(token=os.environ["KEYENV_TOKEN"])
client.load_env(os.environ["KEYENV_PROJECT"], os.environ.get("DJANGO_ENV", "development"))
elif os.path.exists(".env"):
from dotenv import load_dotenv
load_dotenv()wsgi.py and asgi.py
No changes needed to wsgi.py or asgi.py. Secrets are loaded when settings.py is imported.
Testing
For tests, you can skip KeyEnv by not setting KEYENV_TOKEN:
# conftest.py (pytest)
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
# Test-specific secrets
os.environ["DJANGO_SECRET_KEY"] = "test-secret-key"
os.environ["DATABASE_URL"] = "postgres://localhost/test_db"Or use pytest-env:
# pytest.ini
[pytest]
env =
DJANGO_SECRET_KEY=test-secret-key
DATABASE_URL=postgres://localhost/test_dbTroubleshooting
"KEYENV_TOKEN not set"
In production, ensure KEYENV_TOKEN is set in your deployment platform's environment variables (Heroku Config Vars, Railway Variables, etc.).
"Project not found"
Verify KEYENV_PROJECT matches your project ID in the KeyEnv dashboard.
"Environment not found"
Check that the environment name (development, staging, production) exists in your KeyEnv project.
Secrets not loading
- Check that
load_env()is called before accessingos.environ - Verify the service token has read access to the environment
- Check for typos in secret key names
Performance
load_env() makes a single API call at startup. Secrets are cached in os.environ for the lifetime of the process. There's no runtime overhead after initialization.