Skip to main content

🌍 Environment Setup

Django-CFG uses pydantic-settings for type-safe environment configuration. Configuration is loaded from environment variables and .env files automatically.


Core Principles

Single Configuration Method

Environment variables for all environments:

Development → .env file (gitignored)
Production → System ENV (Docker, K8s, CI/CD)
Testing → Defaults + minimal ENV overrides

Priority System

Configuration loading follows this priority (highest wins):

1. System environment variables (highest priority)
2. .env file values
3. Default values in code (lowest priority)

Environment Detection

Automatic detection based on environment variables:

# Development (default if nothing set)
IS_DEV=true

# Production
IS_PROD=true

# Testing
IS_TEST=true

Configuration Structure

Development Config (.env)

api/environment/.env
# === Environment Mode ===
IS_DEV=true

# === Core Settings ===
SECRET_KEY="dev-secret-key-at-least-50-chars-change-in-production"
DEBUG=true

# === Database ===
DATABASE__URL="postgresql://postgres:postgres@localhost:5432/djangocfg"

# === Application URLs ===
APP__NAME="My App"
APP__DOMAIN="localhost"
APP__API_URL="http://localhost:8000"
APP__SITE_URL="http://localhost:3000"

# === Email ===
EMAIL__BACKEND="console" # Prints to console
EMAIL__DEFAULT_FROM="My App <noreply@localhost.dev>"

# === Cache ===
REDIS_URL="redis://localhost:6379/0"

# === API Keys (optional) ===
# API_KEYS__OPENAI=""
# API_KEYS__SENDGRID=""

Development benefits:

  • Console email backend (no SMTP setup needed)
  • Local database (PostgreSQL or SQLite)
  • Gitignored - safe for local secrets
  • Quick setup - copy .env.example

Production Config (System ENV)

Production Environment Variables
# Set in Docker/K8s - NEVER in .env file!

# === Environment Mode ===
IS_PROD=true

# === Core Settings ===
SECRET_KEY="production-secret-from-secrets-manager-min-50-chars"
DEBUG=false

# === Database ===
DATABASE__URL="postgresql://prod_user:prod_pass@db.example.com:5432/prod_db"

# === Application URLs ===
APP__NAME="My App"
APP__DOMAIN="example.com"
APP__API_URL="https://api.example.com"
APP__SITE_URL="https://example.com"

# === Security Domains ===
# REQUIRED in production!
SECURITY_DOMAINS="example.com,api.example.com,www.example.com"

# === Email ===
EMAIL__BACKEND="smtp"
EMAIL__HOST="smtp.sendgrid.net"
EMAIL__PORT=587
EMAIL__USERNAME="apikey"
EMAIL__PASSWORD="${SENDGRID_API_KEY}"
EMAIL__USE_TLS=true
EMAIL__DEFAULT_FROM="My App <noreply@example.com>"

# === Cache ===
REDIS_URL="redis://redis:6379/1"

# === API Keys ===
API_KEYS__OPENAI="${OPENAI_API_KEY}"
API_KEYS__SENDGRID="${SENDGRID_API_KEY}"

Production practices:

  • ✅ Use secrets managers (AWS Secrets Manager, Vault)
  • ✅ Set in Docker environment or K8s Secrets
  • Never commit to version control
  • Rotate secrets regularly

Test Config

Test Environment (pytest.ini or CI)
# === Environment Mode ===
IS_TEST=true

# === Core Settings ===
SECRET_KEY="test-key-for-testing-only-min-50-chars-long"
DEBUG=false

# === Database (in-memory for speed) ===
DATABASE__URL="sqlite:///:memory:"

# === Email (don't send real emails) ===
EMAIL__BACKEND="console"

# === Disable external services ===
API_KEYS__OPENAI=""
REDIS_URL=""

Testing optimizations:

  • In-memory SQLite for fast tests
  • Console email backend
  • Disabled external services
  • Minimal logging

Environment Variables

Standard Variables

Django-CFG supports standard environment variables:

# Core
SECRET_KEY="your-secret-key-here"
DEBUG=true
DATABASE__URL="postgresql://user:pass@host:5432/db"
REDIS_URL="redis://localhost:6379/0"

# API Keys
API_KEYS__OPENAI="sk-..."
API_KEYS__SENDGRID="SG..."

Nested Configuration

Use __ (double underscore) for nested config:

# email.host
EMAIL__HOST="smtp.gmail.com"

# email.port
EMAIL__PORT=587

# api_keys.openai
API_KEYS__OPENAI="sk-..."

# app.domain
APP__DOMAIN="example.com"

Pattern: SECTION__FIELD=value maps to section.field = value


Loading Configuration

Automatic Loading with Pydantic

Django-CFG automatically loads configuration using pydantic-settings:

# api/environment/loader.py
from pathlib import Path
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseConfig(BaseSettings):
"""Database configuration."""
url: str = Field(default="sqlite:///db/default.sqlite3")

model_config = SettingsConfigDict(
env_prefix="DATABASE__",
env_nested_delimiter="__",
)


class EmailConfig(BaseSettings):
"""Email configuration."""
backend: str = Field(default="console")
host: str = Field(default="localhost")
port: int = Field(default=587)
username: str | None = Field(default=None)
password: str | None = Field(default=None)
use_tls: bool = Field(default=True)
default_from: str = Field(default="noreply@example.com")

model_config = SettingsConfigDict(
env_prefix="EMAIL__",
env_nested_delimiter="__",
)


class EnvironmentConfig(BaseSettings):
"""Complete environment configuration."""
secret_key: str = Field(
default="django-cfg-dev-key-change-in-production-min-50-chars"
)
debug: bool = Field(default=True)

database: DatabaseConfig = Field(default_factory=DatabaseConfig)
email: EmailConfig = Field(default_factory=EmailConfig)

model_config = SettingsConfigDict(
env_file=str(Path(__file__).parent / ".env"),
env_file_encoding="utf-8",
env_nested_delimiter="__",
case_sensitive=False,
extra="ignore",
)


# Global instance - auto-loads from ENV > .env > defaults
env = EnvironmentConfig()

How it works:

  1. Checks system environment variables (highest priority)
  2. Loads .env file if exists
  3. Uses defaults from Field definitions
  4. Validates types automatically with Pydantic

Using Configuration

# api/config.py
from django_cfg import DjangoConfig, DatabaseConfig
from .environment import env


class MyDjangoConfig(DjangoConfig):
project_name: str = env.app.name
secret_key: str = env.secret_key
debug: bool = env.debug
security_domains: list[str] = env.security_domains or []

# Database from environment
databases = {
"default": DatabaseConfig.from_url(url=env.database.url)
}


# Generate Django settings
config = MyDjangoConfig()

Environment-Specific Behavior

Development

Automatic configuration:

  • security_domains: ["localhost", "127.0.0.1"] → CORS open for local dev
  • email.backend: "console" → Emails printed to console
  • debug: true → Verbose logging, detailed errors
  • Relaxed security settings

Setup:

# Copy .env.example to .env
cp api/environment/.env.example api/environment/.env

# Edit with your values
vim api/environment/.env

# Run development server
python manage.py runserver

Production

Automatic configuration:

  • security_domains → REQUIRED, strict CORS/CSRF
  • email.backend: "smtp" → Real email sending
  • debug: false → Structured logging, minimal output
  • Strict security (HTTPS, HSTS, secure cookies)

Setup:

# Set environment variables in Docker/K8s
export IS_PROD=true
export SECRET_KEY="from-secrets-manager"
export DATABASE__URL="postgresql://..."

# Or in docker-compose.yml
services:
django:
environment:
IS_PROD: "true"
SECRET_KEY: "${SECRET_KEY}"
DATABASE__URL: "${DATABASE_URL}"

Docker

Automatic features:

  • Service name resolution (postgres, redis)
  • Health check endpoints enabled
  • Private IP ranges allowed for internal calls
  • Container-aware resource monitoring

Docker Compose example:

# docker-compose.yml
version: '3.8'

services:
django:
build: .
environment:
# Environment mode
IS_PROD: "true"

# Core settings
SECRET_KEY: "${SECRET_KEY}"
DEBUG: "false"

# Database (Docker service name)
DATABASE__URL: "postgresql://postgres:postgres@postgres:5432/db"

# Cache (Docker service name)
REDIS_URL: "redis://redis:6379/0"

# Email
EMAIL__BACKEND: "smtp"
EMAIL__HOST: "${EMAIL_HOST}"
EMAIL__PORT: "587"
EMAIL__USERNAME: "${EMAIL_USERNAME}"
EMAIL__PASSWORD: "${EMAIL_PASSWORD}"

postgres:
image: postgres:16
environment:
POSTGRES_DB: db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

redis:
image: redis:7-alpine

Best Practices

1. Never Commit Secrets

# .gitignore
.env
.env.*
environment/.env
secrets/

Good:

# In .env file (gitignored)
SECRET_KEY="my-secret-key"
DATABASE__URL="postgresql://..."

Bad:

# Hardcoded in code - NEVER DO THIS!
SECRET_KEY = "hardcoded-secret-key"
DATABASE_URL = "postgresql://hardcoded-pass@..."

2. Use .env for Development Only

# Development (local)
.env file → gitignored, safe for local secrets

# Production
System ENV → Docker, K8s, CI/CD
Secrets Manager → AWS Secrets Manager, Vault

# Testing
Defaults → Minimal ENV overrides

3. Provide Defaults for Development

# Good - has sensible defaults
class DatabaseConfig(BaseSettings):
url: str = Field(
default="sqlite:///db/default.sqlite3" # Dev default
)

# Override in production
DATABASE__URL="postgresql://prod-db:5432/db"

4. Validate in Production

from api.environment import env

if env.env.is_prod:
# Validate critical settings
assert len(env.secret_key) >= 50, "Secret key too short!"
assert not env.debug, "DEBUG must be False in production!"
assert "postgresql" in env.database.url.lower(), "Use PostgreSQL in production!"
assert env.security_domains, "SECURITY_DOMAINS required in production!"

5. Document Required Variables

.env.example
# === Required Environment Variables ===
# Production deployment requires:
# - SECRET_KEY: Django secret key (50+ chars)
# - DATABASE__URL: PostgreSQL connection string
# - REDIS_URL: Redis connection string
# - EMAIL__HOST: SMTP server hostname
# - EMAIL__PASSWORD: SMTP password
# - API_KEYS__OPENAI: OpenAI API key (if using AI features)

# === Core Settings ===
SECRET_KEY="your-secret-key-minimum-50-characters-long"
DEBUG=false

# === Database ===
DATABASE__URL="postgresql://user:pass@host:5432/db"

Docker Deployment

For complete Docker deployment guide, see Docker Guide →

Quick Reference

Docker Compose with ENV:

version: '3.8'

services:
django:
build: .
env_file:
- .env.production # Load from file
environment:
# Or set directly
IS_PROD: "true"
DATABASE__URL: "postgresql://postgres@db:5432/prod"

Kubernetes with Secrets:

apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: django
env:
- name: IS_PROD
value: "true"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: django-secrets
key: secret-key
- name: DATABASE__URL
valueFrom:
secretKeyRef:
name: django-secrets
key: database-url

Troubleshooting

Environment Variables Not Loading

Check:

# Verify .env file location
ls api/environment/.env

# Check environment variable
echo $DATABASE__URL

# Test in Python
python manage.py shell
>>> from api.environment import env
>>> print(env.database.url)

Variable Format Errors

# ✅ Correct (double underscore)
EMAIL__HOST="smtp.gmail.com"

# ❌ Wrong (single underscore)
EMAIL_HOST="smtp.gmail.com"

# ✅ Correct (nested)
API_KEYS__OPENAI="sk-..."

# ❌ Wrong
API_KEYS_OPENAI="sk-..."

Type Conversion Errors

# ✅ Correct
EMAIL__PORT=587 # Automatically converted to int
DEBUG=true # Automatically converted to bool

# ❌ Wrong
EMAIL__PORT=abc # Not a valid integer!
DEBUG=yes # Use "true" or "false"

Secret Key Error

# Generate secure key
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

# Set in .env (development)
SECRET_KEY="generated-key-here-min-50-chars"

# Set in environment (production)
export SECRET_KEY="generated-key-here-min-50-chars"

Migration from YAML

Migrating from YAML configs?

Old approach used config.dev.yaml, config.prod.yaml, etc.

New approach: Everything via ENV variables!

Before (YAML)

config.prod.yaml
secret_key: "${SECRET_KEY}"
debug: false
database:
url: "${DATABASE_URL}"
email:
backend: "smtp"
host: "smtp.example.com"

After (ENV)

System ENV or .env
SECRET_KEY="my-secret-key"
DEBUG=false
DATABASE__URL="postgresql://..."
EMAIL__BACKEND="smtp"
EMAIL__HOST="smtp.example.com"

Benefits:

  • ✅ Simpler - one method for all environments
  • ✅ 12-factor app compliant
  • ✅ Works everywhere (Docker, K8s, CI/CD)
  • ✅ No file management overhead

See Also


TAGS: environment, configuration, pydantic-settings, env-vars, deployment DEPENDS_ON: [django-cfg, pydantic-settings] USED_BY: [deployment, docker, production]