Django check --deploy Explained: Every Warning and Fix

Understand every warning from python manage.py check --deploy: HSTS, SSL redirect, cookies, SECRET_KEY, DEBUG, ALLOWED_HOSTS, and when silencing is justified.

Problem statement

Django ships a deployment check that inspects your settings for common security mistakes:

python manage.py check --deploy

The first time you run it on a real project, it often prints a wall of warnings with IDs like security.W004 and security.W008. Some are critical (debug mode on, weak secret key), some depend on your architecture (HTTPS redirects handled by a proxy), and some are deliberate choices (HSTS preload). Teams either ignore the output entirely or silence everything to make it quiet, and both lose the value of the check.

This guide explains every warning the deployment check can produce in current Django versions, what risk each one addresses, how to fix it, and when silencing a specific check is reasonable.

What the deploy check doesmanage.py check--deploySecurity checkssettings inspectedWarningsW001 and othersFix or silencewith a reasonThe check reads settings only; it cannot see your proxy or server configuration.

Quick answer

  1. Run python manage.py check --deploy with the production settings and environment variables, not development settings.
  2. Fix critical warnings immediately: W018 (DEBUG), W020 (ALLOWED_HOSTS), W009 and W025 (weak keys).
  3. Fix cookie and HTTPS warnings (W008, W012, W016, W004) once HTTPS works end to end.
  4. Decide deliberately on HSTS subdomains and preload (W005, W021).
  5. Silence only checks that are covered elsewhere, with a comment explaining why, in SILENCED_SYSTEM_CHECKS.
  6. Run the check in CI with --fail-level WARNING so new problems block merges.

Running the check correctly

DJANGO_SETTINGS_MODULE=config.settings \
DJANGO_DEBUG=0 \
DJANGO_SECRET_KEY="$PRODUCTION_LIKE_SECRET" \
DJANGO_ALLOWED_HOSTS=www.example.com \
python manage.py check --deploy
run it with production settingsDJANGO_SETTINGS_MODULE=config.settings.production\python manage.py check--deploy--fail-level WARNING1231The same settings the server runs, with its env loaded.2Adds the security checks to the normal system checks.3Warnings exit non-zero, so CI fails.Run against development settings, it reports problems production does not havedjango-deployment.com
Without --fail-level, the command exits 0 even with every warning present.

--deploy runs the deployment-tagged checks in addition to the normal system checks. The results depend entirely on the settings loaded, so running it on a laptop with development settings produces misleading output. On the server, load the same environment as the web process.

Useful options:

  • --fail-level WARNING makes the command exit with a non-zero status on warnings, for CI.
  • --database default also runs database-related checks (for example MySQL strict mode).
  • --tag security limits output to security checks.

Warnings and fixes

Middleware checks

security.W001: SecurityMiddleware missing. Without it, Django cannot send HSTS, nosniff, referrer policy, or cross-origin opener policy headers, or perform SSL redirects. Fix: keep django.middleware.security.SecurityMiddleware near the top of MIDDLEWARE.

security.W002: XFrameOptionsMiddleware missing. Pages can be framed by other sites, enabling clickjacking. Fix: add django.middleware.clickjacking.XFrameOptionsMiddleware.

security.W003: CSRF middleware not detected. Forms are unprotected against cross-site request forgery. Fix: keep django.middleware.csrf.CsrfViewMiddleware. If you protect specific views differently, make sure every state-changing endpoint is covered before silencing.

HTTPS and HSTS

security.W004: SECURE_HSTS_SECONDS not set. Browsers may connect over plain HTTP first, allowing downgrade attacks. Fix, once the site is fully HTTPS:

SECURE_HSTS_SECONDS = 31536000
HSTS: raise it in steps3600one hour, test86400one day604800one week31536000one yearSubdomains + preloadonly when all are HTTPSBrowsers remember the value; a mistake lasts as long as max-agedjango-deployment.com
Preload lists are slow to leave, so treat preload as permanent.

Start small (for example 300) and increase after confirming HTTPS works everywhere.

security.W005: SECURE_HSTS_INCLUDE_SUBDOMAINS not True. Subdomains remain reachable over HTTP. Fix only if every subdomain serves HTTPS:

SECURE_HSTS_INCLUDE_SUBDOMAINS = True

security.W021: SECURE_HSTS_PRELOAD not True. Required to submit the domain to browser preload lists. Preload is a long-term commitment. Silencing this warning is reasonable if you have not decided to preload.

security.W008: SECURE_SSL_REDIRECT not True. Plain HTTP requests are served instead of redirected. Fix:

SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")  # behind a trusted TLS proxy

If the reverse proxy or load balancer already redirects all HTTP traffic and Django is unreachable over plain HTTP, silencing W008 can be justified; enabling it anyway is a harmless safety net once proxy headers are configured.

Content type and framing

security.W006: SECURE_CONTENT_TYPE_NOSNIFF not True. Browsers may guess content types, for example executing uploaded text as script. Fix: SECURE_CONTENT_TYPE_NOSNIFF = True (the default).

security.W019: X_FRAME_OPTIONS not DENY. SAMEORIGIN allows your own pages to frame each other. Fix: set X_FRAME_OPTIONS = "DENY" unless you rely on same-origin framing, in which case silencing is reasonable.

Keys and debug mode

security.W009: weak SECRET_KEY. The key is shorter than 50 characters, has fewer than 5 unique characters, or starts with django-insecure- (the auto-generated development key). Fix: generate a new key and load it from the environment:

python -c "import secrets; print(secrets.token_urlsafe(50))"

Changing the key invalidates sessions and password reset links unless you use SECRET_KEY_FALLBACKS temporarily.

security.W025: weak entry in SECRET_KEY_FALLBACKS. Same criteria as W009 for fallback keys. Fix: remove old weak keys from fallbacks once their signed data has expired.

security.W018: DEBUG is True. Debug pages expose settings, source code, and local variables. Fix: DEBUG = False in production, parsed correctly from the environment. Never silence this check.

security.W020: ALLOWED_HOSTS is empty. With DEBUG = False, every request is rejected; and an empty list suggests host validation was not configured. Fix: list your domains explicitly.

Cookies

security.W010, W011, W012: SESSION_COOKIE_SECURE not True. The session cookie can be sent over plain HTTP and intercepted. The three IDs differ only in context (sessions app installed, session middleware present, or neither). Fix:

SESSION_COOKIE_SECURE = True
cookie warnings and their fixesSettingFixW010, W011, W012SESSION_COOKIE_SECURE✓TrueW013, W014, W015SESSION_COOKIE_HTTPONLY✓True (the default)W016CSRF_COOKIE_SECURE✓TrueBehind a proxy these also need SECURE_PROXY_SSL_HEADER to workdjango-deployment.com
Secure cookies over plain HTTP are simply not sent, which looks like broken logins.

security.W013, W014, W015: SESSION_COOKIE_HTTPONLY not True. JavaScript can read the session cookie, which helps attackers exploiting XSS. Fix: leave SESSION_COOKIE_HTTPONLY = True (the default).

security.W016: CSRF_COOKIE_SECURE not True. The CSRF cookie can be sent over HTTP. Fix:

CSRF_COOKIE_SECURE = True

Referrer and opener policies

security.W022: SECURE_REFERRER_POLICY not set. No Referrer-Policy header is sent, so full URLs may leak to other sites. Fix: set a policy such as "same-origin" (the default in current Django) or "strict-origin-when-cross-origin".

security.E023: invalid SECURE_REFERRER_POLICY value. A typo or unsupported value. Fix: use valid policy names.

security.E024: invalid SECURE_CROSS_ORIGIN_OPENER_POLICY value. Fix: use "same-origin", "same-origin-allow-popups", or "unsafe-none".

CSRF failure view

security.E101 / E102: custom CSRF_FAILURE_VIEW problems. The configured view does not accept the expected arguments (request, reason) or cannot be imported. Fix the view signature or import path.

Checks removed in older versions

You may find references to security.W007 (SECURE_BROWSER_XSS_FILTER) and security.W017 (CSRF_COOKIE_HTTPONLY) in older articles. These checks were removed from Django (in 3.0 and 1.11 respectively) because the corresponding settings offered no practical benefit; current versions do not report them.

Silencing checks responsibly

When a warning does not apply because the risk is handled elsewhere, silence that specific check with a comment:

SILENCED_SYSTEM_CHECKS = [
    # HTTPS redirects are enforced by the load balancer; Django is not reachable over HTTP.
    "security.W008",
    # HSTS preload not planned; revisit in 2027.
    "security.W021",
]
silence one check, with a reasonCareless✕silence every security.* ID✕no comment explaining why✕never revisitedResponsible✓one ID, in production settings only✓comment names the compensating control✓reviewed when infrastructure changesW008 is a fair example when the proxy already forces HTTPSdjango-deployment.com
SILENCED_SYSTEM_CHECKS should be short enough to read in one glance.

Guidelines:

  • never silence W018 (DEBUG) or W009 (weak key)
  • document the compensating control next to each silenced ID
  • review silenced checks when infrastructure changes

Add the check to CI

- name: Django deployment checks
  env:
    DJANGO_SETTINGS_MODULE: config.settings
    DJANGO_DEBUG: "0"
    DJANGO_SECRET_KEY: ${{ secrets.CI_DJANGO_SECRET_KEY }}
    DJANGO_ALLOWED_HOSTS: www.example.com
  run: python manage.py check --deploy --fail-level WARNING

Use a long random key stored as a CI secret for this job so W009 reflects real configuration rather than a short placeholder. A pull request that sets SESSION_COOKIE_SECURE = False or removes a middleware then fails CI.

Verify

python manage.py check --deploy --fail-level WARNING && echo "deployment checks passed"
curl -sI https://www.example.com/ | grep -i -E "strict-transport|x-frame|x-content-type|referrer-policy"

Verification check:

  • the command reports no issues, or only silenced checks with documented reasons
  • response headers match the settings the checks expect
  • CI runs the check with production-like settings on every change

Explanation

check --deploy is a static review of settings against Django's security guidance. It cannot see your proxy, CDN, or network, which is why a few warnings may be handled elsewhere. Treating each warning as a question (is this risk addressed, and where?) keeps the check useful: real gaps get fixed, architectural exceptions get documented, and CI catches regressions before they reach production.

Edge cases / notes

Settings that differ per environment

If staging intentionally differs (for example no HSTS on a temporary domain), run the check with production settings in CI rather than against staging configuration.

Third-party checks

Some packages register their own deployment checks. Read their documentation for IDs outside the security. namespace.

The check does not cover everything

It does not validate CSRF_TRUSTED_ORIGINS, database TLS, file upload handling, admin exposure, or dependency vulnerabilities. Combine it with a production settings review and dependency scanning.

A worked example

A typical first run on a project that has just switched to production settings:

System check identified some issues:

WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting.
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True.
?: (security.W009) Your SECRET_KEY has less than 50 characters...
?: (security.W012) SESSION_COOKIE_SECURE is not set to True.
?: (security.W016) CSRF_COOKIE_SECURE is not set to True.
?: (security.W018) You should not have DEBUG set to True in deployment.

System check identified 6 issues (0 silenced).

Working through them in order of severity:

  1. W018 first: DEBUG is on, which means the environment variable is missing or parsed incorrectly. Fix the parsing (os.environ.get("DJANGO_DEBUG", "0") == "1"), redeploy, and re-run.
  2. W009: generate a proper key, store it in the secret store, and restart. Re-running should clear it.
  3. W012 and W016: set both cookie settings to True once HTTPS is confirmed working.
  4. W008 and W004: enable the SSL redirect and start HSTS at a small value, increasing after a few days.

A second run then shows either no issues or only checks you have deliberately silenced. Getting to that state once makes the check useful forever after, because any new warning represents an actual change.

python manage.py check --database default      # database-specific checks
python manage.py makemigrations --check --dry-run
python manage.py validate_templates            # if django-extensions is installed

Combining these in one CI job gives a broad configuration gate: settings, database compatibility, migration state, and template syntax, all before deployment.

Common mistakes

  • Running the check with development settings and dismissing the results.
  • Silencing all security warnings to get clean CI output.
  • Setting long HSTS values just to clear W004 before HTTPS works everywhere.
  • Using a short placeholder SECRET_KEY in CI and silencing W009 globally.

Next steps

FAQ

What does python manage.py check --deploy do?

It runs Django's system checks plus deployment-specific checks that inspect security-related settings such as DEBUG, ALLOWED_HOSTS, HTTPS redirects, HSTS, secure cookies, and the secret key strength.

Is it safe to silence security.W008 in Django?

Only when every request reaches Django through a proxy or load balancer that already redirects HTTP to HTTPS, and Django is not reachable over plain HTTP. Document the reason next to the silenced ID.

Why does check --deploy warn about my SECRET_KEY?

The key is shorter than 50 characters, has fewer than 5 unique characters, or begins with django-insecure-. Generate a long random key and load it from the environment.

Should check --deploy run in CI?

Yes, with production-like settings and --fail-level WARNING, so changes that weaken security settings fail before they are deployed.

Learning path · step 2 of 15

Hardening a Django deployment

Keep going