Skip to Content
DocsGetting StartedConfiguration

Configuration Guide

Simple & Modern Django-CFG uses pydantic-settings for type-safe environment configuration. No YAML files, no complex loaders. Just ENV variables, .env files, and defaults in code.

Configuration Flow

Overview

Django-CFG uses pydantic-settings to load configuration from environment variables and .env files. This provides type-safe configuration with automatic validation.

Key benefits:

  • ✅ Simple - one configuration method for all environments
  • Type-safe loader with Pydantic models
  • ✅ Single import point via environment module
  • ✅ 12-factor app compliant
  • ✅ Works everywhere (Docker, K8s, CI/CD)

Security First Never commit secrets to version control. Use environment variables or secrets managers for sensitive data.

Quick Start

1. Create Environment Module

project/ ├── config.py # Main DjangoConfig class ├── settings.py # Django settings └── environment/ # Environment configuration ├── __init__.py # Exports `env` instance ├── loader.py # Pydantic models with BaseSettings ├── .env # Local development config (gitignored!) └── .env.example # Template for team

2. Create Pydantic Models (loader.py)

# environment/loader.py from pathlib import Path from typing import Optional from pydantic import Field, computed_field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class DatabaseConfig(BaseSettings): """Database connection configuration.""" url: str = Field( default="sqlite:///db/default.sqlite3", description="Database connection URL" ) 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: Optional[str] = Field(default=None) password: Optional[str] = Field(default=None) use_tls: bool = Field(default=True) use_ssl: bool = Field(default=False) default_from: str = Field(default="[email protected]") model_config = SettingsConfigDict( env_prefix="EMAIL__", env_nested_delimiter="__", ) class TelegramConfig(BaseSettings): """Telegram bot configuration.""" bot_token: str = Field(default="") chat_id: int = Field(default=0) model_config = SettingsConfigDict( env_prefix="TELEGRAM__", env_nested_delimiter="__", ) class ApiKeysConfig(BaseSettings): """API keys configuration.""" openrouter: str = Field(default="") openai: str = Field(default="") model_config = SettingsConfigDict( env_prefix="API_KEYS__", env_nested_delimiter="__", ) class AppConfig(BaseSettings): """Application configuration.""" name: str = Field(default="My Project") logo_url: str = Field(default="") domain: str = Field(default="localhost") api_url: str = Field(default="http://localhost:8000") site_url: str = Field(default="http://localhost:3000") model_config = SettingsConfigDict( env_prefix="APP__", env_nested_delimiter="__", ) class EnvironmentMode(BaseSettings): """Environment mode detection via ENV variables.""" is_test: bool = Field(default=False) is_dev: bool = Field(default=False) is_prod: bool = Field(default=False) @model_validator(mode="after") def set_default_env(self): """Default to development if no environment is set.""" if not any([self.is_test, self.is_dev, self.is_prod]): self.is_dev = True return self @computed_field @property def env_mode(self) -> str: """Get current environment mode as string.""" if self.is_test: return "test" elif self.is_prod: return "production" return "development" model_config = SettingsConfigDict( env_nested_delimiter="__", ) class EnvironmentConfig(BaseSettings): """Complete environment configuration.""" # Core Django settings secret_key: str = Field( default="django-cfg-dev-key-change-in-production-min-50-chars" ) debug: bool = Field(default=True) # Admin Configuration admin_emails: Optional[list[str]] = Field(default=None) # Configuration sections (nested configs) database: DatabaseConfig = Field(default_factory=DatabaseConfig) email: EmailConfig = Field(default_factory=EmailConfig) telegram: TelegramConfig = Field(default_factory=TelegramConfig) api_keys: ApiKeysConfig = Field(default_factory=ApiKeysConfig) app: AppConfig = Field(default_factory=AppConfig) env: EnvironmentMode = Field(default_factory=EnvironmentMode) # Cache redis_url: Optional[str] = Field(default=None) # Security domains security_domains: Optional[list[str]] = Field( default=["localhost", "127.0.0.1"], description="Allowed security domains" ) model_config = SettingsConfigDict( env_file=str(Path(__file__).parent / ".env"), env_file_encoding="utf-8", env_nested_delimiter="__", case_sensitive=False, extra="ignore", ) # Global environment configuration instance # Auto-loads from: ENV variables > .env file > defaults env = EnvironmentConfig()

3. Export env Instance (init.py)

# environment/__init__.py from .loader import ( EnvironmentConfig, env, DatabaseConfig, EmailConfig, TelegramConfig, AppConfig, EnvironmentMode, ) __all__ = [ "EnvironmentConfig", "env", "DatabaseConfig", "EmailConfig", "TelegramConfig", "AppConfig", "EnvironmentMode", ]

4. Create .env File

Development (.env)

environment/.env
# === Environment Mode === IS_DEV=true # === Core Django Settings === SECRET_KEY="django-cfg-dev-secret-key-change-in-production-min-50-chars" DEBUG=true # === Database === # SQLite (default): # DATABASE__URL="sqlite:///db/default.sqlite3" # PostgreSQL (recommended for dev): DATABASE__URL="postgresql://postgres:postgres@localhost:5432/djangocfg" # === Cache === REDIS_URL="redis://localhost:6379/0" # === Email Configuration === EMAIL__BACKEND="console" EMAIL__DEFAULT_FROM="My Project <[email protected]>" # === Telegram (optional) === # TELEGRAM__BOT_TOKEN="your-bot-token" # TELEGRAM__CHAT_ID=0 # === API Keys (optional) === # API_KEYS__OPENROUTER="sk-or-xxx" # API_KEYS__OPENAI="sk-proj-xxx" # === Application URLs (optional - has defaults) === # APP__NAME="My Project" # APP__API_URL="http://localhost:8000" # APP__SITE_URL="http://localhost:3000"

Development Setup Development configuration uses:

  • PostgreSQL or SQLite databases (your choice)
  • Console email backend (emails print to console)
  • Localhost domains for CORS
  • All secrets in .env file (gitignored)

Production (ENV)

Production Environment Variables
# Set in Docker/K8s/CI - NEVER in .env file! # === Environment Mode === IS_PROD=true # === Core Django Settings === SECRET_KEY="production-secret-key-from-secrets-manager-min-50-chars" DEBUG=false # === Database === DATABASE__URL="postgresql://prod-user:[email protected]:5432/prod_db" # === Cache === REDIS_URL="redis://redis:6379/1" # === Email Configuration === EMAIL__BACKEND="smtp" EMAIL__HOST="smtp.sendgrid.net" EMAIL__PORT=587 EMAIL__USERNAME="apikey" EMAIL__PASSWORD="SG.xxxxxxxxxxxx" EMAIL__USE_TLS=true EMAIL__DEFAULT_FROM="My Project <[email protected]>" # === Application URLs === APP__NAME="My Project" APP__DOMAIN="myapp.com" APP__API_URL="https://api.myapp.com" APP__SITE_URL="https://myapp.com" # === Security Domains === SECURITY_DOMAINS="myapp.com,api.myapp.com,admin.myapp.com" # === Telegram === TELEGRAM__BOT_TOKEN="1234567890:ABCdefGHIjklMNOpqrsTUVwxyz" TELEGRAM__CHAT_ID=-1001234567890 # === API Keys === API_KEYS__OPENROUTER="sk-or-xxx" API_KEYS__OPENAI="sk-proj-xxx"

Production Secrets Critical: Production configuration must use environment variables for all secrets:

  • Set in Docker environment variables
  • Use Kubernetes Secrets
  • Use AWS Secrets Manager / HashiCorp Vault
  • NEVER commit to git!

.env.example

environment/.env.example
# Django-CFG Environment Variables - Example # Copy this file to .env and fill in your values # Priority: ENV variables > .env file > defaults in code # === Environment Mode === # IS_DEV=true # IS_PROD=false # IS_TEST=false # === Core Django Settings === # SECRET_KEY="your-secret-key-minimum-50-characters-long" # DEBUG=true # === Database === DATABASE__URL="postgresql://postgres:postgres@localhost:5432/djangocfg" # === Cache (optional) === # REDIS_URL="redis://localhost:6379/0" # === Email Configuration === EMAIL__BACKEND="smtp" EMAIL__HOST="smtp.example.com" EMAIL__PORT=587 EMAIL__USERNAME="[email protected]" EMAIL__PASSWORD="your-password" EMAIL__USE_TLS=true EMAIL__DEFAULT_FROM="[email protected]" # === Telegram (optional) === TELEGRAM__BOT_TOKEN="your-telegram-bot-token" TELEGRAM__CHAT_ID=0 # === API Keys (optional) === API_KEYS__OPENROUTER="your-openrouter-api-key" API_KEYS__OPENAI="your-openai-api-key" # === Application URLs (optional) === # APP__DOMAIN="localhost" # APP__API_URL="http://localhost:8000" # APP__SITE_URL="http://localhost:3000" # === Admin Configuration (optional) === # ADMIN_EMAILS="[email protected],[email protected]" # === Security Domains (optional) === # SECURITY_DOMAINS="localhost,127.0.0.1,example.com"

Team Onboarding Commit .env.example to git as a template for your team. Each developer copies it to .env and fills in their values.

5. Use in config.py

# config.py from typing import Dict from django_cfg import DjangoConfig, DatabaseConfig, EmailConfig, TelegramConfig # Import environment configuration from .environment import env class MyProjectConfig(DjangoConfig): """Project configuration using environment variables.""" # Core settings from ENV secret_key: str = env.secret_key debug: bool = env.debug env_mode: str = env.env.env_mode # Project info project_name: str = env.app.name project_logo: str = env.app.logo_url # URLs from ENV site_url: str = env.app.site_url api_url: str = env.app.api_url # Security domains from ENV security_domains: list[str] = env.security_domains or [] # Database from ENV URL databases: Dict[str, DatabaseConfig] = { "default": DatabaseConfig.from_url(url=env.database.url), } # Email from ENV email: EmailConfig | None = ( EmailConfig( host=env.email.host, port=env.email.port, use_tls=env.email.use_tls, username=env.email.username, password=env.email.password, default_from=env.email.default_from, ) if env.email.host else None ) # Telegram from ENV telegram: TelegramConfig | None = ( TelegramConfig( bot_token=env.telegram.bot_token, chat_id=env.telegram.chat_id, ) if env.telegram.bot_token and env.telegram.chat_id else None ) # Create configuration instance config = MyProjectConfig()

Environment Detection

Automatic Detection Django-CFG automatically detects the environment based on IS_DEV, IS_PROD, and IS_TEST environment variables. Defaults to development if none are set.

Development

Development Mode
# Development (default - no flag needed) python manage.py runserver # Or explicitly: IS_DEV=true python manage.py runserver # Docker Compose docker-compose up

Uses: Defaults + .env file

Production

Production Mode
# Set production flag export IS_PROD=true python manage.py runserver # Or inline: IS_PROD=true gunicorn myproject.wsgi:application # Docker production docker run -e IS_PROD=true myapp:latest

Uses: System ENV variables (no .env file!)

Production Checklist Before deploying to production:

  • ✅ Set IS_PROD=true
  • ✅ Configure all environment variables
  • ✅ Set DEBUG=false
  • ✅ Use PostgreSQL (not SQLite)
  • ✅ Set up reverse proxy for SSL/TLS
  • ✅ Configure SECURITY_DOMAINS

Test

Test Mode
# Run tests IS_TEST=true python manage.py test # With pytest IS_TEST=true pytest # CI/CD environment export IS_TEST=true pytest --cov=myapp

Uses: Defaults optimized for testing

CI/CD Integration Most CI/CD systems work seamlessly with environment variables. Set IS_TEST=true in your CI config.

ENV Variable Notation

Use double underscore (__) to access nested configurations:

# Flat config DEBUG=true SECRET_KEY="my-secret-key" # Nested: email.host EMAIL__HOST=smtp.gmail.com # Nested: database.url DATABASE__URL=postgresql://user:pass@localhost:5432/db # Nested: api_keys.openai API_KEYS__OPENAI=sk-proj-xxx

DatabaseConfig.from_url()

Django-CFG provides from_url() to create database configurations from URLs:

from django_cfg import DatabaseConfig # Basic usage db = DatabaseConfig.from_url(url="postgresql://user:pass@localhost/mydb") # With routing db = DatabaseConfig.from_url( url="postgresql://user:pass@localhost/analytics", apps=["analytics", "reports"], operations=["read", "write", "migrate"], routing_description="Analytics database", )

Supported URL schemes:

  • postgresql:// - PostgreSQL
  • mysql:// - MySQL
  • sqlite:/// - SQLite (use 3 slashes)
  • Other Django-supported backends

Best Practices

Configuration Best Practices Follow these patterns to ensure secure, maintainable configuration.

1. Never Commit Secrets

❌ Wrong - Secrets in .env committed to git:

.env (INSECURE - committed to git)
SECRET_KEY="actual-secret-key-abc123" # ❌ NEVER DO THIS DATABASE__URL="postgresql://user:[email protected]/prod" # ❌ EXPOSED

✅ Correct - Use .gitignore:

.gitignore
# Ignore .env file with secrets .env .env.local environment/.env # Keep .env.example for team !.env.example !environment/.env.example

✅ Correct - Production uses system ENV:

# Set in Docker/K8s - never in files export SECRET_KEY="from-secrets-manager" export DATABASE__URL="postgresql://..."

Secret Exposure Risk Secrets committed to git can be:

  • Leaked through GitHub/GitLab search
  • Found in git history (even after deletion)
  • Exposed in forks and mirrors

2. Use .env for Development Only

# Development (local) - use .env file .env # gitignored, safe for local secrets # Production - use system ENV variables Docker environment variables Kubernetes Secrets AWS Secrets Manager HashiCorp Vault

Never use .env files in production!

3. Validate Configuration Early

# Pydantic validates automatically at startup from api.environment import env # This will fail fast if invalid print(env.database.url) # ✅ Validated # Add custom validation if needed if env.env.is_prod: assert len(env.secret_key) >= 50, "Secret key too short!" assert not env.debug, "DEBUG must be False in production!"

Fail-Fast Validation Pydantic validates configuration at startup, catching errors before they reach production.

4. Environment-Specific Defaults

# Good defaults in code class DatabaseConfig(BaseSettings): url: str = Field( # SQLite for dev, override for prod default="sqlite:///db/default.sqlite3" ) class EmailConfig(BaseSettings): backend: str = Field( # Console for dev, override for prod default="console" )

Benefits:

  • Minimal configuration needed for development
  • Sensible defaults per environment
  • Type-safe with Pydantic validation

Integration with Django

Using in settings.py

# settings.py from config import config # Import all Django settings globals().update(config.get_all_settings()) # Optionally add custom settings STATIC_URL = '/static/' MEDIA_URL = '/media/'

Management Commands

# Validate configuration python manage.py check # Show current configuration python manage.py show_config # Check Django settings python manage.py check_settings

Troubleshooting

ENV Variables Not Loading

Check:

  1. .env file location (should be in environment/ folder)
  2. Variable naming (use __ for nesting)
  3. Case sensitivity (should be case-insensitive)
# Debug what's loaded from api.environment import env print(f"Database URL: {env.database.url}") print(f"Environment: {env.env.env_mode}")

Pydantic Validation Error

pydantic_core.ValidationError: 1 validation error for EnvironmentConfig email.port Input should be a valid integer [type=int_type]

Solution: Check type in .env file:

# ❌ Wrong EMAIL__PORT=abc # ✅ Correct EMAIL__PORT=587

Database URL Parse Error

ValueError: Invalid database URL format

Solution: Check URL format:

# ✅ Correct formats: DATABASE__URL="postgresql://user:pass@localhost:5432/db" DATABASE__URL="mysql://root:[email protected]:3306/db" DATABASE__URL="sqlite:///db.sqlite3" # 3 slashes!

Next Steps

TAGS: configuration, pydantic-settings, environment, django-cfg DEPENDS_ON: [installation, intro] USED_BY: [first-project, multi-database]