Free Ebook cover Django Fundamentals: From First App to a Complete Backend

Django Fundamentals: From First App to a Complete Backend

New course

12 pages

Settings and Environment Configuration for Development and Production

Capítulo 9

Estimated reading time: 8 minutes

+ Exercise

Why settings need environment configuration

Django settings control behavior that must differ between development and production: debug output, allowed hosts, database credentials, cookie security, HTTPS handling, and more. If you keep a single settings.py with hard-coded values, you risk leaking secrets, deploying with unsafe defaults, or making changes that accidentally break another environment.

A maintainable approach has two goals:

  • Same codebase runs in multiple environments (local, staging, production).
  • Secrets and environment-specific values are injected at runtime (environment variables or secret managers), not committed to Git.

Two common patterns for multi-environment settings

Pattern A: Separate settings modules (recommended for clarity)

You create a base settings module and then import/override per environment.

config/  # your Django project package (name may differ)  settings/    __init__.py    base.py    dev.py    prod.py

Then you select the module via DJANGO_SETTINGS_MODULE:

# Development (example)export DJANGO_SETTINGS_MODULE=config.settings.dev# Production (example)export DJANGO_SETTINGS_MODULE=config.settings.prod

Pattern B: Single settings module driven by environment variables

You keep one settings.py and branch based on DEBUG or ENV. This can work, but tends to grow into many conditional blocks and becomes harder to audit. If you choose this pattern, keep the branching minimal and group related settings.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Step-by-step: Implement separate settings modules

1) Create a base settings file

Move shared configuration into config/settings/base.py. Keep it free of environment-specific secrets and values.

# config/settings/base.pyfrom pathlib import Pathimport osBASE_DIR = Path(__file__).resolve().parent.parent.parentdef env(name, default=None):    return os.environ.get(name, default)INSTALLED_APPS = [    # ... your apps ...]MIDDLEWARE = [    # ... your middleware ...]ROOT_URLCONF = "config.urls"TEMPLATES = [    # ...]WSGI_APPLICATION = "config.wsgi.application"LANGUAGE_CODE = "en-us"TIME_ZONE = "UTC"USE_I18N = TrueUSE_TZ = TrueSTATIC_URL = "static/"DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

2) Add development settings

Development typically enables debug, uses a local database, and relaxes some security constraints.

# config/settings/dev.pyfrom .base import *DEBUG = TrueSECRET_KEY = env("DJANGO_SECRET_KEY", "dev-only-insecure-key")ALLOWED_HOSTS = ["localhost", "127.0.0.1"]DATABASES = {    "default": {        "ENGINE": "django.db.backends.sqlite3",        "NAME": BASE_DIR / "db.sqlite3",    }}# Helpful in development (optional)EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

Note: The development SECRET_KEY can be a fallback, but it should never be used in production. In production, require it to be set.

3) Add production settings

Production should disable debug, require a strong secret key, restrict allowed hosts, use a production database, and enable security settings.

# config/settings/prod.pyfrom .base import *def require_env(name):    value = env(name)    if not value:        raise RuntimeError(f"Missing required environment variable: {name}")    return valueDEBUG = FalseSECRET_KEY = require_env("DJANGO_SECRET_KEY")ALLOWED_HOSTS = require_env("DJANGO_ALLOWED_HOSTS").split(",")# Database example: PostgreSQL via DATABASE_URL or individual varsDATABASES = {    "default": {        "ENGINE": "django.db.backends.postgresql",        "NAME": require_env("POSTGRES_DB"),        "USER": require_env("POSTGRES_USER"),        "PASSWORD": require_env("POSTGRES_PASSWORD"),        "HOST": require_env("POSTGRES_HOST"),        "PORT": env("POSTGRES_PORT", "5432"),        "CONN_MAX_AGE": 60,    }}# Security-related settings (typical baseline)SECURE_SSL_REDIRECT = TrueSESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = TrueSECURE_HSTS_SECONDS = int(env("DJANGO_HSTS_SECONDS", "31536000"))SECURE_HSTS_INCLUDE_SUBDOMAINS = TrueSECURE_HSTS_PRELOAD = TrueSECURE_CONTENT_TYPE_NOSNIFF = TrueSECURE_REFERRER_POLICY = "same-origin"CSRF_TRUSTED_ORIGINS = require_env("DJANGO_CSRF_TRUSTED_ORIGINS").split(",")

These values are examples; adjust based on your deployment (reverse proxy, load balancer, domain setup). For instance, if TLS is terminated at a proxy, you may also need:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")USE_X_FORWARDED_HOST = True

4) Point manage.py / wsgi.py / asgi.py to the default settings module

Typically, Django’s generated files reference config.settings. With a settings package, you can keep config/settings/__init__.py to select a default (often dev) for local work, but production should always set DJANGO_SETTINGS_MODULE explicitly.

# config/settings/__init__.py# Optional: default to dev for local convenience# In production, set DJANGO_SETTINGS_MODULE explicitlyfrom .dev import *

If you prefer not to have a default (safer), leave __init__.py empty and always set DJANGO_SETTINGS_MODULE in your environment.

Managing configuration without committing secrets

Use environment variables for secrets and environment-specific values

At minimum, treat these as secrets or environment-specific:

  • SECRET_KEY
  • Database credentials
  • Third-party API keys
  • Email provider credentials

In local development, you can use a .env file loaded by your process manager or tooling. Django does not load .env automatically; you can load it via a library (commonly python-dotenv) or by exporting variables in your shell. If you use a .env file, add it to .gitignore and commit a template instead.

# .gitignore.env
# .env.example (commit this, without real secrets)DJANGO_SETTINGS_MODULE=config.settings.devDJANGO_SECRET_KEY=change-meDJANGO_ALLOWED_HOSTS=example.com,www.example.comDJANGO_CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.comPOSTGRES_DB=appdbPOSTGRES_USER=appuserPOSTGRES_PASSWORD=change-mePOSTGRES_HOST=127.0.0.1POSTGRES_PORT=5432

Fail fast when required production values are missing

Production should raise an error at startup if critical variables are missing. This prevents silent insecure defaults (for example, running with a development secret key).

Use a helper like require_env (shown earlier) for production-only settings.

Core settings to configure per environment

SECRET_KEY

SECRET_KEY signs cookies and tokens. If it leaks, attackers may forge sessions or tamper with signed data. Requirements:

  • Unique per environment (dev/staging/prod).
  • Never committed to source control.
  • Rotated carefully (rotation invalidates existing sessions and signed data).

DEBUG

DEBUG=True shows detailed error pages and may expose sensitive information. In production, it must be False. Also ensure you have proper error logging/monitoring in production, because you won’t see debug pages.

ALLOWED_HOSTS

When DEBUG=False, Django requires ALLOWED_HOSTS to prevent Host header attacks. Configure it to your domain(s) and any internal hostnames used by your infrastructure.

# Example environment variableDJANGO_ALLOWED_HOSTS=example.com,www.example.com

Databases

Common approach:

  • Development: SQLite for simplicity, or local Postgres if you want parity.
  • Production: managed Postgres/MySQL with credentials in environment variables.

Also consider:

  • CONN_MAX_AGE to reuse DB connections in production.
  • Separate read replicas or transaction settings only when needed.

Security-related settings (production baseline)

SettingTypical production valuePurpose
SECURE_SSL_REDIRECTTrueRedirect HTTP to HTTPS
SESSION_COOKIE_SECURETrueSession cookie only over HTTPS
CSRF_COOKIE_SECURETrueCSRF cookie only over HTTPS
SECURE_HSTS_SECONDSe.g. 31536000Enable HSTS (after verifying HTTPS works)
SECURE_HSTS_INCLUDE_SUBDOMAINSTrueApply HSTS to subdomains
SECURE_HSTS_PRELOADTrue (optional)Allow HSTS preload (requires care)
SECURE_CONTENT_TYPE_NOSNIFFTruePrevent MIME sniffing
SECURE_REFERRER_POLICYsame-originControl referrer header behavior
CSRF_TRUSTED_ORIGINSYour HTTPS originsRequired when behind certain proxies and for cross-origin POSTs

If you serve static files via a CDN or separate domain, also review CORS and cookie domain settings carefully (only if your architecture requires it).

How to verify environment-specific behavior

Check which settings module is active

From the command line, you can verify the selected settings module and key values.

# Show active settings modulepython -c "import os; print(os.environ.get('DJANGO_SETTINGS_MODULE'))"# Inspect settings via Django shellpython manage.py shell -c "from django.conf import settings; print(settings.DEBUG, settings.ALLOWED_HOSTS)"

Confirm production safety checks

Django includes a deployment check framework. Run it with your production settings:

DJANGO_SETTINGS_MODULE=config.settings.prod python manage.py check --deploy

This flags common issues such as missing HTTPS-related settings, insecure cookie flags, and debug configuration.

Smoke-test host and HTTPS assumptions

  • With DEBUG=False, request the site using a host not in ALLOWED_HOSTS and confirm it is rejected.
  • If using a reverse proxy, confirm Django sees the request as HTTPS when appropriate (may require SECURE_PROXY_SSL_HEADER).

Maintainability techniques for settings

Group settings by concern

In base.py, keep sections like: core, apps, middleware, templates, database defaults, internationalization, static/media, logging, security defaults. In dev.py and prod.py, only override what differs.

Use small helper functions for parsing env vars

Parsing booleans and lists repeatedly leads to mistakes. Keep helpers in base.py:

def env_bool(name, default="0"):    return env(name, default).lower() in {"1", "true", "yes", "on"}def env_list(name, default=""):    value = env(name, default)    return [item.strip() for item in value.split(",") if item.strip()]

Then:

DEBUG = env_bool("DJANGO_DEBUG", "0")ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS")

Keep secrets out of logs and error pages

Avoid printing environment variables or settings in logs. If you add custom diagnostics, redact values like database passwords and secret keys.

Checklist: what changes between development and production

AreaDevelopmentProduction
DEBUGTrueFalse
SECRET_KEYMay use local fallbackRequired env var, strong and private
ALLOWED_HOSTSlocalhost, 127.0.0.1Real domains only
DatabaseSQLite or local DBManaged DB with credentials in env vars
HTTPSOften offOn; redirect HTTP to HTTPS
Cookies*_SECURE=False acceptableSESSION_COOKIE_SECURE=True, CSRF_COOKIE_SECURE=True
HSTSOffOn after verifying HTTPS is correct
Error handlingDebug pagesLogging/monitoring; no debug pages
Config sourceLocal env or .env (ignored)Environment variables / secret manager
VerificationBasic runserver checksmanage.py check --deploy + smoke tests

Now answer the exercise about the content:

In a Django project that uses separate settings modules (base/dev/prod), what is the safest way to ensure production never starts with insecure defaults like a development SECRET_KEY?

You are right! Congratulations, now go to the next page

You missed! Try again.

Production should inject secrets via environment variables and fail fast when required values are missing. This prevents accidentally running with unsafe development fallbacks, such as an insecure SECRET_KEY.

Next chapter

Static and Media Files in Django: Handling Assets and User Uploads

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.