Home › Python › ImproperlyConfigured: Fix Django Settings Boot Errors
Beginner 5 min · September 23, 2026

ImproperlyConfigured: Fix Django Settings Boot Errors

Export DJANGO_SETTINGS_MODULE, call django.setup() before models, then retry.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
✓ Production
production tested
October 31, 2026
last updated
2,099
articles · all by Naren
Before you start⏱ 11 min
  • ✓Basic Django: projects, apps, and manage.py
  • ✓Running scripts with python3 and environment variables
  • ✓How web servers load Python apps (WSGI basics)
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Export DJANGO_SETTINGS_MODULE in the process env and keep a setdefault in wsgi.py so every boot path resolves it
  • Call django.setup() before importing any model — env var, setup, models, in that order, always
  • Give standalone scripts the 3-line bootstrap header; prefer management commands for scheduled work
  • Read SECRET_KEY strictly from env and list real hostnames in ALLOWED_HOSTS before the next deploy
✦ Definition~90s read
What is Django ImproperlyConfigured Fix?

Django's configuration system centers on a settings module — a Python module (usually mysite/settings.py) selected by the DJANGO_SETTINGS_MODULE environment variable and loaded once at process start by django.setup(). That call configures settings, populates the app registry from INSTALLED_APPS, and runs system checks; everything downstream (models, views, ORM queries, template rendering) assumes it already happened. ImproperlyConfigured is the exception Django raises when code demands configuration that was never established or contradicts itself.

★
Think of Django as a restaurant kitchen that won't start cooking until someone hands it the recipe book and turns on the gas.

The exception covers a family of boot failures, not one bug. An unset settings module, model imports before setup(), an app missing from INSTALLED_APPS, a placeholder SECRET_KEY, and empty ALLOWED_HOSTS with DEBUG off all surface as ImproperlyConfigured variants (with AppRegistryNotReady as the close cousin for ordering bugs).

The common thread is explicitness: Django refuses to guess which database, which apps, or which hosts you meant, and fails at boot instead of misbehaving at request time.

Each execution context bootstraps differently, which is why the error feels inconsistent. manage.py, the shell, and tests configure settings automatically; standalone scripts, notebooks, Celery workers, and WSGI servers require the variable plus setup() through their own paths. Production bugs cluster at these seams: code that passes under runserver fails under gunicorn, scripts that work in the shell fail in cron.

The mental model is one sentence: no Django API before the settings module is named and setup() has run — in that process, in that order.

Plain-English First

Think of Django as a restaurant kitchen that won't start cooking until someone hands it the recipe book and turns on the gas. DJANGO_SETTINGS_MODULE is naming the recipe book. django.setup() is turning on the gas — the ovens (apps and models) can't work before that. SECRET_KEY and ALLOWED_HOSTS are the locks on the doors: forget them and the restaurant either won't open or won't let customers in.

You run a script, hit an endpoint, or deploy to a fresh server, and Django answers: ImproperlyConfigured: .... The message names a setting, an app, or a setup step, and the app won't boot until you satisfy it. Newcomers read this as Django being fussy; veterans read it as Django refusing to guess about configuration that must be explicit.

This error clusters around five situations: the settings module was never named, models got imported before django.setup(), a standalone script skipped the bootstrap header, SECRET_KEY or ALLOWED_HOSTS diverged between environments, or the WSGI server boots through a different path than your dev server. Each one passes locally and fails in production, which is why this exception pages more often than it should.

This guide walks all five with the exact fix for each, plus a 60-second triage that resolves most pages. You'll learn the bootstrap order Django requires, the standalone-script pattern that always works, and how to keep local and server environments from silently diverging.

DJANGO_SETTINGS_MODULE Unset: Name Every Boot Path

Every Django process begins with one question: which settings module describes this deployment? The answer comes from the DJANGO_SETTINGS_MODULE environment variable, a dotted path like mysite.settings. Without it, Django has no database configuration, no installed apps, no middleware — nothing it needs to serve a request or touch a model. Any settings access then raises ImproperlyConfigured naming the requested setting, which reads like a missing value but is really a missing identity.

The variable must be visible in the failing process, not just your shell. Exporting it in .bashrc helps your terminal but not systemd, Docker, or cron, each of which starts with its own environment. That's why Django's own templates belt-and-suspenders it: both manage.py and wsgi.py call os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings'), giving every boot path a default while letting the process env override per deployment.

Your check is two commands. First echo $DJANGO_SETTINGS_MODULE (plus the Python-level read) in the exact context that fails — the service shell, the container, the cron job. Then import the named module directly to prove it's on sys.path. If the variable is empty, export it where the process starts; if the import fails, fix the path or working directory. Most settings-module pages end right here.

django_settings_module.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os

# Correct: name the settings module BEFORE touching Django.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")

print("settings module:", os.environ["DJANGO_SETTINGS_MODULE"])

# What Django does on boot (simplified):
mod = os.environ.get("DJANGO_SETTINGS_MODULE")
if not mod:
    raise RuntimeError("ImproperlyConfigured: DJANGO_SETTINGS_MODULE is unset")
print("boot would load:", mod)

# Shell equivalents:
#   export DJANGO_SETTINGS_MODULE=mysite.settings
#   DJANGO_SETTINGS_MODULE=mysite.settings python script.py
🎯 Key Takeaway
The settings module must resolve in the failing process itself — export it at process start and keep the setdefault fallback.

Models Before django.setup(): Order Is Everything

Importing a Django model isn't a passive statement — it registers the model with the app registry, which only exists after settings load and apps populate. Call the import first and Django raises AppRegistryNotReady (or ImproperlyConfigured, depending on version and path), naming the step you skipped. The traceback points at your innocent-looking from myapp.models import Order on line 1, while the defect is the missing django.setup() that should have run before it.

The safe order never varies: set the environment variable, call django.setup(), then import models. django.setup() loads settings, populates the app registry from INSTALLED_APPS, and wires up checks — the three preconditions every model import assumes. Management commands, the shell, and tests run this for you; standalone scripts, notebooks, and worker entry points must run it themselves.

When triaging, grep the failing file for import order: model imports above the setup() call are the smoking gun. Move all Django imports below setup(), keep stdlib imports (os, sys) at top, and re-run the one-liner that imports a model after setup. If it prints, your order is fixed; if it still fails, the settings module or INSTALLED_APPS is the next suspect, not the import itself.

django_setup_order.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os

ORDER = ["set DJANGO_SETTINGS_MODULE", "django.setup()", "import models"]
print("safe order:", " -> ".join(ORDER))

# Simulate the registry guard without Django installed:
_setup_done = False

def setup():
    global _setup_done
    _setup_done = True

def import_models():
    if not _setup_done:
        raise RuntimeError("AppRegistryNotReady: models before django.setup()")
    return "Order, Customer"

try:
    import_models()
except RuntimeError as exc:
    print("early import fails:", exc)

setup()
print("after setup:", import_models())
🎯 Key Takeaway
Env var, setup(), models — import order is the contract; grep top-of-file imports when this error appears.

Standalone Scripts: The 3-Line Bootstrap Header

Standalone scripts are the most common source of this error because they boot Django by hand. A cron job, a data-fix script, or a queue worker entry point starts as a plain Python process with no settings loaded, and the first model import detonates. The fix is a three-line header at the top: os.environ.setdefault for the settings module, django.setup(), and only then the model imports. Everything Django-related lives below that header.

Two details break the header in practice. First, placement: the model imports must physically sit below setup() in the file — Python executes top to bottom, so an import on line 1 runs before setup() on line 5 no matter what you intended. Second, context: the script must run from the repo root (so mysite.settings is importable) with the project's virtualenv interpreter (so Django and deps exist). A script that's correct under your laptop's Python fails under the system Python with a ModuleNotFoundError wearing an ImproperlyConfigured costume.

For anything scheduled, skip the header entirely and write a management command. Commands run inside Django's configured context with logging, argument parsing, and transaction handling provided. The standalone header is for one-off fixes; recurring work deserves python manage.py nightly_sync, tested like any other Django code.

django_standalone_script.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
import sys

# Minimal standalone header (stdlib-only simulation).
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
print("1. settings module:", os.environ["DJANGO_SETTINGS_MODULE"])
print("2. django.setup() would run here (apps populate)")
print("3. from myapp.models import Order  # only AFTER setup")
print("interpreter:", sys.executable)
print("cwd on sys.path:", sys.path[0])

# Run from the repo root with the project interpreter:
#   cd /srv/myapp && /srv/venvs/myapp/bin/python scripts/nightly_sync.py
# Prefer a management command for scheduled work:
#   python manage.py nightly_sync  (setup handled for you)
🎯 Key Takeaway
setdefault, setup(), models — run from the repo root with the project interpreter, or use a management command.

SECRET_KEY and ALLOWED_HOSTS Variants That Reject Traffic

Two settings produce their own ImproperlyConfigured flavors. An empty SECRET_KEY raises ImproperlyConfigured('The SECRET_KEY setting must not be empty'), and a hardcoded or weak key breaks sessions, password resets, and cryptographic signing — and Django's check framework loudly warns (security.W009) about the default placeholder in production, though it does not refuse to run. The operational failure is subtler than the check: local .env has a dev key, the server's process env lacks one, and the deploy either crashes at boot or, worse, ships with a committed test key every session can forge.

ALLOWED_HOSTS fails differently: with DEBUG=False and an empty list, Django returns 400 Bad Request for every request, which looks like a network or proxy outage rather than a setting. The page that should have listed hostnames instead shows a blank 400, and teams burn an hour on load-balancer logs before checking settings. The rule is simple: local dev may use empty hosts with DEBUG=True; every deployed environment lists its real hostnames explicitly.

Handle both at boot, not at request time. Read SECRET_KEY strictly so absence aborts startup with a clear message, generate real secrets with secrets.token_urlsafe(50) stored in a vault, and assert non-empty ALLOWED_HOSTS whenever DEBUG is off. A 10-line boot check in settings (or the deploy smoke test) converts both failure modes from midnight mysteries into deploy-time rejections.

django_secret_hosts.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import secrets

# Strict read: missing SECRET_KEY fails fast at boot, not mid-request.
key = os.environ.get("SECRET_KEY", "")
print("SECRET_KEY set:", bool(key), "length:", len(key))
if not key:
    print("BOOT ABORT: set SECRET_KEY env var (never commit it)")

debug = os.environ.get("DEBUG", "False") == "True"
hosts = [h for h in os.environ.get("ALLOWED_HOSTS", "").split(",") if h]
print("DEBUG:", debug, "ALLOWED_HOSTS:", hosts or "(empty)")
if not debug and not hosts:
    print("BOOT ABORT: production needs explicit ALLOWED_HOSTS")

# Generate a fresh secret (run once, store in vault):
# python -c "import secrets; print(secrets.token_urlsafe(50))"
print("sample token length:", len(secrets.token_urlsafe(50)))
📊 Production Insight
A deploy shipped the committed dev SECRET_KEY to production; sessions forged in staging worked against prod until rotation. Strict env reads plus a committed-key grep in CI closed the hole permanently.
🎯 Key Takeaway
Fail fast at boot on missing secrets; production always needs explicit ALLOWED_HOSTS with DEBUG off.

WSGI vs Management Contexts: Two Doors, One Setting

The same codebase can pass under runserver and crash under gunicorn because the two servers boot Django through different doors. Management commands and runserver start from manage.py, which sets the settings default for you. Gunicorn, uWSGI, and systemd start from wsgi.py (or an ASGI module) plus the service's own environment — if neither names the settings module, workers die at import while your laptop stays green.

Systemd, Docker, and cron each add their own twist. Systemd units start with a minimal environment, so anything you exported in .bashrc is invisible; the unit needs an explicit Environment= line or an EnvironmentFile. Docker bakes ENV into the image, which is convenient until staging and production images diverge. Cron strips the environment almost bare, so Django cron jobs need the variable inline or in a wrapper script.

The durable fix declares the module in both layers: setdefault in code (wsgi.py, asgi.py, manage.py) and the explicit variable in process config (systemd unit, Dockerfile ENV, container task definition). Then add a smoke test that exercises the production boot path — importing the WSGI application and a model — rather than only testing through manage.py. When staging and production share the identical boot declaration, this error class disappears.

🎯 Key Takeaway
Declare the settings module in code defaults AND process config, then smoke-test the production boot path.

Reading the Traceback: The Quoted NAME Tells All

Every ImproperlyConfigured message quotes a NAME — the setting, app, or model it wanted. That quoted string is your fastest lead: Requested setting INSTALLED_APPS means no settings loaded (env var), No module named 'orders' means the app isn't installed or importable, Model class ... doesn't declare means app-label mismatch. Copy the quoted name verbatim instead of paraphrasing it in the ticket.

Run the triage in fixed order. First, the env var in the failing context (echo plus the Python-level read). Second, the setup order in the failing file (grep model imports vs setup call). Third, INSTALLED_APPS membership and importability of the named app. Fourth, SECRET_KEY/ALLOWED_HOSTS only if the message names them. This sequence resolves the large majority of reports because it follows Django's own boot sequence.

Write the triage down as a runbook, not tribal knowledge. A 5-line checklist in the on-call docs (with the exact commands from the debug guide above) turns a scary framework error into a routine check. The teams that stop fearing ImproperlyConfigured are the ones that made its diagnosis boring.

💡The 60-second triage
When ImproperlyConfigured pages you, read the quoted NAME first, then check env, setup order, and INSTALLED_APPS in that order. Most pages resolve in under a minute without touching app logic.
🎯 Key Takeaway
Follow Django's boot order in triage: env var, setup order, INSTALLED_APPS, then named secrets — read the quoted NAME first.
● Production incidentPOST-MORTEMseverity: high

One Missing Env Var Looped 6 Workers for 52 Minutes

Symptom
At 10:02 a.m. on a release Tuesday, checkout 500s spiked to 100% across all 6 gunicorn workers while the homepage (served from CDN cache) looked fine. CPU sat near idle because workers died at import, and systemd logged 340 restart events in 52 minutes. The on-call engineer saw green load-balancer checks for 20 minutes before testing a real POST to /checkout/ and watching it fail. Staging was fully green the entire time.
Assumption
The team assumed the deploy was config-identical because the code was identical and staging passed. The systemd service file was provisioned months earlier by a different playbook that never set the Django variable, while staging used Docker with the variable baked into the image. Nobody diffed process environments, and the health check only hit the load balancer TCP port, which stayed green.
Root cause
The production systemd unit for gunicorn never set DJANGO_SETTINGS_MODULE. Under runserver and Docker-based staging the variable came from manage.py and the image ENV, so everything passed. On the systemd host, each of the 6 workers raised ImproperlyConfigured: Requested setting INSTALLED_APPS during import and exited; systemd restarted them in a loop. The load balancer TCP check passed because the socket briefly accepted, but every request got a 500. The variable had been dropped 3 weeks earlier during a playbook refactor that split web and worker roles, and 214 checkout orders failed in the 52-minute window.
Fix
The fix was 1 line in the systemd unit plus a redeploy taking 14 minutes. Environment=DJANGO_SETTINGS_MODULE=mysite.settings was added to myapp.service, the daemon reloaded, and all 6 workers booted cleanly. A /health/ endpoint that imports a model was added to the deploy smoke test, and the Ansible role now asserts the variable in both the image and the service file. The 214 queued orders processed in 6 minutes and the error rate returned to 0.02%.
Key lesson
  • Declare the settings module in every boot path (code default plus process env), not just the developer's favorite one.
  • Smoke-test what the app does (import a model), not just that the port is open.
  • Diff process environments between staging and production, not just code revisions.
Production debug guideSymptom → Root cause → Fix5 entries
Symptom · 01
Requested setting errors or DJANGO_SETTINGS_MODULE complaints
→
Fix
Check the variable in the failing context: echo $DJANGO_SETTINGS_MODULE; python -c "import os; print(os.environ.get('DJANGO_SETTINGS_MODULE'))". If either is empty, export it (export DJANGO_SETTINGS_MODULE=mysite.settings) and retry. Then confirm the module imports: python -c "import mysite.settings; print('settings ok', mysite.settings.__name__)".
Symptom · 02
AppRegistryNotReady or models imported before setup
→
Fix
Find top-of-file model imports above setup with grep -n "from.*models import\|import django" script.py | head -20. The model import must come after django.setup(). Fix the order, then verify: python -c "import os, django; os.environ.setdefault('DJANGO_SETTINGS_MODULE','mysite.settings'); django.setup(); from myapp.models import Order; print('models ok')".
Symptom · 03
Standalone script fails while manage.py commands work
→
Fix
Run the script header standalone: python -c "import os, django; os.environ.setdefault('DJANGO_SETTINGS_MODULE','mysite.settings'); django.setup(); print('boot ok')". If that passes but script.py fails, the script's own imports or sys.path are wrong — run it from the repo root with the project interpreter: which python && python script.py.
Symptom · 04
SECRET_KEY empty or 400 Bad Request on every page
→
Fix
Compare env against code without leaking secrets: python -c "import os; print('KEY set:', bool(os.environ.get('SECRET_KEY'))); print('HOSTS:', os.environ.get('ALLOWED_HOSTS'))" and grep -rn "SECRET_KEY\|ALLOWED_HOSTS" mysite/settings.py | head. Missing KEY or empty HOSTS with DEBUG=False is the failure; inject via process env or secret manager, never by committing values.
Symptom · 05
Works under runserver, fails under gunicorn/systemd
→
Fix
Inspect both boot paths: grep -n "DJANGO_SETTINGS_MODULE" mysite/wsgi.py manage.py; grep -rn "Environment=" /etc/systemd/system/myapp.service 2>/dev/null. Then simulate production boot: python -c "import os, django; os.environ.setdefault('DJANGO_SETTINGS_MODULE','mysite.settings'); django.setup(); import mysite.wsgi; print('wsgi boot ok')". Add the missing Environment= line and restart the service.
ImproperlyConfigured Causes at a Glance
Root CauseHow to ConfirmFixPrevention
DJANGO_SETTINGS_MODULE unsetecho + python env read both empty in failing contextExport it at process start; keep setdefault in codeDeclare in code AND process config; assert in deploy
Models imported before setup()grep shows model import above django.setup() lineReorder: env, setup(), then model importsLint import order; one-liner model-import smoke test
Standalone script skips bootstrapHeader lacks setdefault/setup; works via manage.py onlyAdd 3-line header or convert to management commandScheduled work as commands; run scripts from repo root
SECRET_KEY / ALLOWED_HOSTS driftStrict read empty; 400s with DEBUG=False and empty hostsVault-backed env vars; explicit hostnames per envBoot-time asserts; never commit secrets to the repo
WSGI boot path misses the modulewsgi.py/service file lack the variable; runserver passesEnvironment= in unit + setdefault in wsgi.pySmoke-test production boot path, not just manage.py
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
django_settings_module.pyos.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")DJANGO_SETTINGS_MODULE Unset
django_setup_order.pyORDER = ["set DJANGO_SETTINGS_MODULE", "django.setup()", "import models"]Models Before django.setup()
django_standalone_script.pyos.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")Standalone Scripts
django_secret_hosts.pykey = os.environ.get("SECRET_KEY", "")SECRET_KEY and ALLOWED_HOSTS Variants That Reject Traffic

Key takeaways

1
Name the settings module everywhere
process env plus setdefault in code, verified with a one-line import check.
2
Setup order is law
env var, django.setup(), then model imports — never reversed.
3
Standalone scripts need the 3-line bootstrap header; scheduled work belongs in management commands.
4
Read SECRET_KEY strictly from env and list real hosts in ALLOWED_HOSTS for production.
5
WSGI servers boot through wsgi.py and service env, not manage.py
declare the module in both.
6
Triage by reading the quoted NAME, then checking env, setup order, and INSTALLED_APPS in that order.

Common mistakes to avoid

6 patterns
×

Exporting the settings var only in .bashrc

Symptom
Terminal works but systemd, Docker, and cron all raise ImproperlyConfigured with identical code.
Fix
Export the variable where the process starts (unit file, image ENV, cron line) and keep setdefault in code.
×

Top-of-file model imports above setup()

Symptom
AppRegistryNotReady on line 1 while the setup() call on line 5 never runs.
Fix
Move every Django import below django.setup(); keep only os/sys above it.
×

Running scripts with the system Python outside the repo

Symptom
ModuleNotFoundError or settings errors that vanish when run via manage.py.
Fix
Run from the repo root with the project venv Python; better, convert recurring scripts to management commands.
×

Committing SECRET_KEY to the repo

Symptom
Dev key ships to prod; session forgery works across environments until emergency rotation.
Fix
Generate per-env secrets, inject via env or vault, and fail fast when absent.
×

Deploying with empty ALLOWED_HOSTS and DEBUG=False

Symptom
Every request returns a blank 400 that looks like a proxy outage, not a setting.
Fix
List real hostnames per environment; keep empty hosts strictly local with DEBUG=True.
×

Testing only through runserver before release

Symptom
Green staging, crash-looping production workers, and a 52-minute checkout outage.
Fix
Add Environment= to the unit and setdefault in wsgi.py; smoke-test the deployed boot path.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is DJANGO_SETTINGS_MODULE and why is it required?
Q02JUNIOR
Why do model imports fail before django.setup()?
Q03SENIOR
How do you write a standalone script that uses Django models?
Q04SENIOR
How do SECRET_KEY and ALLOWED_HOSTS break deploys?
Q05SENIOR
Why does code pass under runserver but fail under WSGI?
Q01 of 05JUNIOR

What is DJANGO_SETTINGS_MODULE and why is it required?

ANSWER
It tells Django which settings module to load, like mysite.settings. Without it Django has no database, apps, or secret config, so any settings access raises ImproperlyConfigured. You set it as an environment variable, and wsgi.py plus manage.py both default it via setdefault. Every context — shell, scripts, servers, tests — must resolve it before touching Django.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How do I set DJANGO_SETTINGS_MODULE correctly?
02
Why can't I import models before django.setup()?
03
What's the minimal standalone Django script pattern?
04
How should SECRET_KEY and ALLOWED_HOSTS be configured?
05
Why does my app work with runserver but fail under gunicorn?
06
What's the fastest triage when this error pages me?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.

Follow
✓ Verified
production tested
October 31, 2026
last updated
2,099
articles · all by Naren
🔥

That's Python Libraries. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Django OperationalError No Such Table Fix
59 / 59 · Python Libraries
Next
Python uv Packaging and Workflows
→