Checklist
#django

Django Production Settings Checklist (DEBUG, ALLOWED_HOSTS, CSRF)

A lot of Django production incidents come from settings.py, not application code.

Problem statement

A lot of Django production incidents come from settings.py, not application code.

The common pattern is simple: a project works locally, gets deployed, and production still carries development defaults or half-finished security settings. That usually shows up in one of two ways:

  • unsafe exposure, such as DEBUG = True leaking detailed error pages
  • broken traffic, such as bad ALLOWED_HOSTS or CSRF settings causing 400 or 403 errors after deployment

This page is a practical Django production settings checklist focused on the minimum settings you should verify before and after a release:

  • SECRET_KEY
  • DEBUG
  • ALLOWED_HOSTS
  • CSRF_TRUSTED_ORIGINS
  • proxy and HTTPS settings that affect host and CSRF validation

It does not try to cover full database tuning, complete secrets management, or every security header Django supports.

Quick answer

Before your first production deploy:

  • load a unique production SECRET_KEY from the environment or secret manager
  • set DEBUG = False
  • define explicit ALLOWED_HOSTS
  • configure CSRF_TRUSTED_ORIGINS only when your deployment actually needs it
  • make sure Django correctly detects HTTPS when you are behind Nginx, Caddy, or a load balancer
  • run python manage.py check --deploy
  • verify with real requests, a form POST, and log inspection after restart

If any of these changes break traffic, roll back to the previous environment file or previous release artifact, restart the app, confirm the old service is healthy, and re-run smoke tests.

Step-by-step solution

1. Start with separate production settings

Do not let production inherit local defaults by accident.

Two common layouts are fine:

  • one settings.py with environment-variable driven values
  • split modules such as base.py, development.py, and production.py

A simple split example:

# mysite/settings/production.py
from .base import *
import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = False
ALLOWED_HOSTS = [h.strip() for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if h.strip()]
CSRF_TRUSTED_ORIGINS = [o.strip() for o in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",") if o.strip()]

A safer boolean parser for environment variables:

import os

def env_bool(name, default=False):
    return os.environ.get(name, str(default)).lower() in {"1", "true", "yes", "on"}

DEBUG = env_bool("DJANGO_DEBUG", default=False)

If you use a single settings file, still default to safe production behavior unless you explicitly enable development mode.

Verify

Run a deploy check before restart:

python manage.py check --deploy

Rollback note

Keep the previous .env file or previous release config available. If host or CSRF validation breaks production, restore the last known-good values, restart the application server, and confirm the previous process came back cleanly in logs and health checks.

2. Load a real production SECRET_KEY

A production deployment should never use a development, test, or placeholder secret key.

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

Example environment value generation:

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

Why it matters

SECRET_KEY is used for Django cryptographic signing, including sessions, password reset tokens, and other security-sensitive features. A reused or committed key weakens the whole deployment.

What to avoid

Do not:

  • hardcode a shared dev key into production settings
  • commit the production key to Git
  • reuse the same key across unrelated projects

Verify

Confirm the environment variable is present in the runtime environment your app actually uses, then restart and check logs:

journalctl -u gunicorn -n 50 --no-pager

Or in Docker:

docker compose logs web --tail=50

Rollback note

If a bad key value was deployed and the app fails to start, restore the previous environment file or secret reference, restart the app, and verify the old process is serving traffic again.

3. Set DEBUG = False

This is the first item in any Django production checklist.

DEBUG = False

If you load it from the environment:

DEBUG = env_bool("DJANGO_DEBUG", default=False)

Why it matters

With DEBUG = True, Django may expose stack traces, settings context, paths, and other internal details through browser error pages. It also changes assumptions around static files and error handling.

Verify

Trigger a harmless missing page and confirm you do not see a Django traceback:

curl -I https://example.com/this-page-does-not-exist

For server-side confirmation, inspect logs instead of browser output:

journalctl -u gunicorn -n 100 --no-pager

Or in Docker:

docker compose logs web --tail=100

Failure patterns when DEBUG is left on

  • detailed exception pages visible to users
  • leaked configuration details
  • confusion around static file behavior that only worked in development

4. Set ALLOWED_HOSTS explicitly

ALLOWED_HOSTS protects Django against invalid or unexpected Host headers.

A typical production example:

ALLOWED_HOSTS = [
    "example.com",
    "www.example.com",
]

From an environment variable:

ALLOWED_HOSTS = [
    h.strip()
    for h in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
    if h.strip()
]

Example environment value:

DJANGO_ALLOWED_HOSTS=example.com,www.example.com

What to include

Include only hosts that should actually serve the app:

  • primary domain
  • www domain if used
  • internal hostname only if your architecture really sends that host to Django
  • IP address only if you intentionally support direct IP access

What to avoid

Do not use:

ALLOWED_HOSTS = ["*"]

That removes an important validation layer and can hide routing or proxy mistakes.

Also remove stale domains from old staging or preview environments.

Reverse proxy note

Your reverse proxy should pass the original host header through to Django. For Nginx:

location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

If the proxy rewrites Host incorrectly, Django host validation becomes misleading.

Verify

Check the expected host:

curl -I https://example.com/

Then check an unexpected host using a method that still reaches Django, such as plain HTTP on the server IP inside the trusted network or curl --resolve for HTTPS:

curl -I --resolve bad.example.com:443:server-ip https://bad.example.com/

The bad host request should fail cleanly rather than being served as your real site.

Rollback

If valid traffic suddenly starts returning 400 Bad Request, restore the previous host list, restart the app, verify the proxy is forwarding the correct Host header, and confirm the old process is healthy before ending the rollback.

5. Configure CSRF safely for production

CSRF_TRUSTED_ORIGINS is not always required, but when it is required, it must be exact.

Typical example:

CSRF_TRUSTED_ORIGINS = [
    "https://example.com",
    "https://www.example.com",
]

Environment-driven version:

CSRF_TRUSTED_ORIGINS = [
    o.strip()
    for o in os.environ.get("DJANGO_CSRF_TRUSTED_ORIGINS", "").split(",")
    if o.strip()
]

Example environment value:

DJANGO_CSRF_TRUSTED_ORIGINS=https://example.com,https://www.example.com

When it is needed

You commonly need this when:

  • admin or forms are accessed through a different trusted origin
  • you intentionally allow cross-origin browser POSTs
  • Django is rejecting secure form submissions because the request origin seen by Django does not match the trusted origin, often due to proxy/scheme configuration

Correct format

Use full origins with scheme:

  • https://example.com
  • https://admin.example.com

Do not omit the scheme.

Common mistakes

  • missing https://
  • trusting too many origins
  • adding domains that should not be posting to the app
  • assuming HTTPS behind a proxy automatically requires this setting
  • forgetting proxy SSL configuration, so Django thinks the request is HTTP and rejects the origin check

Test after deploy

Use a browser for real CSRF validation:

  • load the admin login page over HTTPS
  • log in
  • submit an authenticated form
  • confirm no 403 CSRF failure occurs

Then inspect logs if it fails:

journalctl -u gunicorn -n 100 --no-pager

or

docker compose logs web --tail=100

These settings affect both CSRF and secure request handling.

SECURE_PROXY_SSL_HEADER

If TLS terminates at Nginx, Caddy, or a load balancer and Django receives plain HTTP from that proxy, Django needs a trusted signal for the original scheme.

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Only set this if your proxy actually sends X-Forwarded-Proto: https and you control that proxy layer. If multiple proxies or load balancers are involved, only trust forwarded headers from your own upstream infrastructure.

USE_X_FORWARDED_HOST

Leave this off unless your proxy architecture requires Django to trust X-Forwarded-Host.

USE_X_FORWARDED_HOST = False

Enabling it unnecessarily widens the set of upstream headers that influence host validation.

SECURE_SSL_REDIRECT and secure cookies

If the public site should always use HTTPS:

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

These settings help keep login and form traffic on HTTPS and prevent insecure cookie transport.

Verify

curl -I https://example.com/

Check for expected redirects and behavior. Then test login and form submission in a browser, because cookie and CSRF issues usually appear there first.

7. Run the deployment verification checklist

Required checks

  • production SECRET_KEY is unique, non-default, and not committed in code
  • DEBUG = False
  • explicit ALLOWED_HOSTS
  • valid CSRF_TRUSTED_ORIGINS if needed
  • correct SECURE_PROXY_SSL_HEADER behind a TLS-terminating proxy
  • secure cookie settings reviewed
  • custom error handling and logs confirmed

Useful adjacent checks

Also review:

  • SECURE_HSTS_SECONDS
  • SECURE_CONTENT_TYPE_NOSNIFF
  • X_FRAME_OPTIONS

Pre-release commands

python manage.py check --deploy

If using systemd:

sudo systemctl restart gunicorn
sudo systemctl status gunicorn --no-pager

If using Docker Compose:

docker compose restart web
docker compose ps

Post-release smoke tests

curl -I https://example.com/
curl -I --resolve bad.example.com:443:server-ip https://bad.example.com/

Then verify manually:

  • homepage loads on the correct domain
  • admin login works
  • a form POST succeeds
  • bad host is rejected
  • no debug page is visible anywhere

Recovery

If the release breaks host routing or form submissions:

  1. revert the changed environment variables or settings file
  2. redeploy the previous release if needed
  3. restart the app server
  4. confirm the previous service is healthy in logs or process status
  5. re-run the same smoke tests

Explanation

This setup works because it aligns Django’s request validation with the way production traffic actually reaches the app.

  • SECRET_KEY protects signed data and must be unique per production deployment
  • DEBUG = False removes development-only behavior
  • ALLOWED_HOSTS makes host routing explicit
  • CSRF_TRUSTED_ORIGINS allows trusted browser origins when needed, but only when listed precisely
  • proxy SSL settings let Django understand whether the original request was HTTPS

The main alternative is settings structure, not behavior. You can use one settings file or split modules, but the production values should still be explicit and reviewable.

When to automate this

Once you deploy more than one environment, this checklist should stop being manual. Good first automation targets are environment-variable validation, manage.py check --deploy in CI, and post-deploy smoke tests for expected host, bad host, admin login, and a CSRF-protected form flow. A reusable production settings template and reverse proxy template also reduce repeated mistakes.

Edge cases / notes

  • Multiple domains or tenant-style apps: keep the host validation strategy explicit. Dynamic host logic can be correct, but it is easier to get wrong than a fixed host list.
  • Preview environments: do not weaken production defaults just to support temporary URLs. Handle preview hosts separately from production.
  • Docker deployments: keep images immutable and inject environment-specific values at runtime through env vars or orchestration config.
  • Load balancers and health checks: some platforms probe with internal hosts or IPs. Only add those to ALLOWED_HOSTS if those requests really hit Django and must be accepted.
  • Static and media files: turning off DEBUG also means you should not rely on development-style static serving assumptions in production. Verify your web server or storage setup separately.
  • Migrations and app restarts: a settings change can fail only after restart, so always pair config changes with log inspection and smoke tests.

If you need the broader settings structure behind this checklist, see Django settings for production: environment variables, split settings, and secrets.

For full web server integration, continue with Deploy Django with Gunicorn and Nginx and How to configure HTTPS for Django behind Nginx or Caddy.

If valid traffic is failing with host errors, use How to fix Django 400 Bad Request (Invalid HTTP_HOST header) in production.

For adjacent production topics, see Django Deployment Checklist for Production, Django WSGI vs ASGI: Which One Should You Deploy?, and Django Static vs Media Files in Production.

FAQ

Do I always need CSRF_TRUSTED_ORIGINS in Django production?

No. You need it when Django must trust specific origins for unsafe requests, such as cross-origin form flows or when proxy/scheme handling causes the request origin seen by Django to differ from the browser origin. If your app works correctly without it, do not add unnecessary origins.

What should I put in ALLOWED_HOSTS behind Nginx or a load balancer?

Usually your public domains only, such as example.com and www.example.com. Add internal hosts only if the proxy or platform truly sends those hosts to Django and they must be accepted.

Why does Django admin login fail with a CSRF error after enabling HTTPS?

The common causes are:

  • missing or incorrect CSRF_TRUSTED_ORIGINS when the browser origin must be explicitly trusted
  • missing SECURE_PROXY_SSL_HEADER
  • proxy not sending X-Forwarded-Proto: https
  • scheme or host mismatch between what the browser uses and what Django sees
  • cookies not marked secure in an HTTPS-only deployment

Is ALLOWED_HOSTS = ['*'] ever acceptable in production?

As a general production setting, no. It disables an important safety check and can hide proxy or routing problems. Use an explicit host list instead.

How do I verify that DEBUG is really off after deployment?

Request a missing URL or trigger a controlled error path and confirm you do not see a Django traceback page. Then inspect application logs for the real error details.

2026 · django-deployment.com - Django Deployment knowledge base