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.
Quick answer
- Run
python manage.py check --deploywith the production settings and environment variables, not development settings. - Fix critical warnings immediately:
W018(DEBUG),W020(ALLOWED_HOSTS),W009andW025(weak keys). - Fix cookie and HTTPS warnings (
W008,W012,W016,W004) once HTTPS works end to end. - Decide deliberately on HSTS subdomains and preload (
W005,W021). - Silence only checks that are covered elsewhere, with a comment explaining why, in
SILENCED_SYSTEM_CHECKS. - Run the check in CI with
--fail-level WARNINGso 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
--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 WARNINGmakes the command exit with a non-zero status on warnings, for CI.--database defaultalso runs database-related checks (for example MySQL strict mode).--tag securitylimits 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
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
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",
]
Guidelines:
- never silence
W018(DEBUG) orW009(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:
- W018 first:
DEBUGis 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. - W009: generate a proper key, store it in the secret store, and restart. Re-running should clear it.
- W012 and W016: set both cookie settings to
Trueonce HTTPS is confirmed working. - 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.
Related checks worth running
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
W004before HTTPS works everywhere. - Using a short placeholder
SECRET_KEYin CI and silencingW009globally.
Next steps
- Review the wider settings list in the Django Production Settings Checklist.
- Configure headers beyond the check with the Django Security Headers Checklist.
- Load settings safely with Configure Django Environment Variables for Production.
- Run through the full Django Deployment Checklist for Production.
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.