Django Deployment Security Basics You Should Not Skip
A lot of Django apps reach production with local-development assumptions still in place: DEBUG=True, weak host validation, secrets in .env files with broad permissions, Gunicorn...
Problem statement
A lot of Django apps reach production with local-development assumptions still in place: DEBUG=True, weak host validation, secrets in .env files with broad permissions, Gunicorn listening on a public port, or HTTPS handled inconsistently between Django and the reverse proxy.
That usually works until the app gets real traffic. Then small misconfigurations turn into avoidable security problems: detailed tracebacks exposed to users, session cookies sent over HTTP, leaked credentials, admin access from the public internet, or a server with more open ports than it needs.
This page covers a practical baseline for Django deployment security for a single production app on Linux. It is not a full security architecture guide. It is the minimum set of controls you should apply before sending production traffic to Django.
Quick answer
Before go-live, make sure you have all of the following in place:
DEBUG = FalseALLOWED_HOSTSrestricted to real domains- HTTPS enabled, with HTTP redirected to HTTPS
SECRET_KEYand credentials loaded from environment or a secret store- secure cookie and header settings enabled
- Django running behind Gunicorn/Uvicorn and Nginx or Caddy
- Gunicorn/Uvicorn bound only to localhost or a Unix socket
- firewall rules allowing only required ports
- SSH hardened with key-based access
- dependencies and OS packages patched
- backups and a rollback path verified
A good first validation step is:
python manage.py check --deploy
That will not secure everything for you, but it catches several common Django production security mistakes.
Step-by-step solution
1) Start with a production threat model
This baseline protects against:
- accidental data exposure from debug settings
- leaked secrets in source control or world-readable files
- insecure transport over plain HTTP
- weak host and proxy configuration
- missing cookie and browser header protections
- unnecessary server and admin exposure
It does not fully cover advanced WAF design, deep container isolation, compliance programs, or large multi-region architectures.
2) Lock down Django production settings
A minimal production settings example:
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = [
"example.com",
"www.example.com",
]
CSRF_TRUSTED_ORIGINS = [
"https://example.com",
"https://www.example.com",
]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SAMESITE = "Lax"
SECURE_SSL_REDIRECT = False # prefer proxy-level redirect when using Nginx; only safe if Django is not publicly reachable except through the proxy
SECURE_HSTS_SECONDS = 300
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
SECURE_HSTS_PRELOAD = False
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
Key points:
DEBUG = Falseprevents debug tracebacks and development behavior.ALLOWED_HOSTSshould list only your actual domains.SECRET_KEYshould come from runtime environment, not source control.CSRF_TRUSTED_ORIGINSis required only for specific trusted origins, such as cross-origin POSTs or certain proxy or host setups; do not add entries unless needed.SECURE_PROXY_SSL_HEADERshould only be used if your reverse proxy setsX-Forwarded-Protocorrectly.
Verification:
python manage.py check --deploy
Rollback note: bad ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, or proxy SSL settings can break requests and logins immediately. Keep the previous known-good settings file or release artifact available.
3) Enforce HTTPS and forward the right headers
In most Linux deployments, Nginx should handle HTTP-to-HTTPS redirects and proxy traffic to Gunicorn.
Example Nginx config:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location /static/ {
alias /srv/example/static/;
}
location /media/ {
alias /srv/example/media/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Verification:
curl -I http://example.com
curl -I https://example.com
You should see an HTTP redirect on port 80 and a valid HTTPS response on 443.
HSTS should be added carefully. Start with a low value like:
SECURE_HSTS_SECONDS = 300
After confirming HTTPS is stable, increase it gradually. Do not set SECURE_HSTS_INCLUDE_SUBDOMAINS or SECURE_HSTS_PRELOAD until you are sure every covered hostname is ready. HSTS mistakes are hard to roll back for users who already cached the policy.
4) Protect secrets and environment configuration
Do not store these in the repo:
DJANGO_SECRET_KEY- database password
- email credentials
- third-party API tokens
A common non-container approach is a restricted environment file consumed by systemd.
Example:
# /etc/example/example.env
DJANGO_SECRET_KEY=replace-with-real-secret
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=strongpassword
DB_HOST=127.0.0.1
DB_PORT=5432
Permissions:
sudo chown root:example /etc/example/example.env
sudo chmod 640 /etc/example/example.env
Treat this env file as sensitive operational data: restrict group membership, exclude it from source control, and secure any backups that include it.
Systemd service example:
[Unit]
Description=Gunicorn for example Django app
After=network.target
[Service]
User=example
Group=www-data
WorkingDirectory=/srv/example/app
EnvironmentFile=/etc/example/example.env
ExecStart=/srv/example/venv/bin/gunicorn config.wsgi:application --bind 127.0.0.1:8000
Restart=always
[Install]
WantedBy=multi-user.target
Use least privilege everywhere:
- database user should only have access to its own database
- Redis should not be publicly exposed
- app services should not run as root
Verification:
sudo systemctl daemon-reload
sudo systemctl restart gunicorn
sudo systemctl status gunicorn
5) Reduce server attack surface
Do not expose Gunicorn directly to the internet. Bind it to localhost or a Unix socket.
Check listening ports:
ss -tulpn
You should not see Gunicorn listening on a public IP.
Basic UFW example:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
That typically allows only SSH, HTTP, and HTTPS. Your database should not be public unless there is a deliberate reason and additional controls.
SSH hardening basics in /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
After changes:
sudo systemctl restart ssh
Be careful: test a second SSH session before closing the first one, or you can lock yourself out.
6) Secure static files, media, and admin exposure
Do not rely on Django debug static serving in production. Collect static files into a known path and let Nginx serve them:
python manage.py collectstatic --noinput
Treat user-uploaded media as untrusted:
- validate file types in application logic
- store uploads outside code directories
- do not allow execution from upload paths
- keep media and static paths separate
- if users can upload files, configure the web server so upload directories are served as data only and not treated as executable content
Admin exposure should be reduced where possible:
- strong unique passwords
- staff-only accounts
- no shared admin users
- consider IP restriction or VPN access for sensitive environments
7) Keep the stack patched and reproducible
Update Django and Python dependencies regularly. Pin versions so deploys are repeatable.
Dependency review basics:
pip list --outdated
On Debian or Ubuntu systems:
sudo apt update
sudo apt upgrade
Keep Nginx, OpenSSL, Python runtime, and database packages current. Review changelogs before deploying major updates.
Avoid one-off production edits that never make it back into versioned config. Security drift usually starts there.
When manual security setup becomes repetitive
If you keep repeating the same Django settings hardening, Nginx headers, systemd units, firewall rules, and post-deploy checks across multiple apps, standardize them. Good first candidates are a hardened Django production settings module, reverse proxy config templates, systemd service files, and a small validation script that runs check --deploy, curl, and port checks after each release.
8) Log enough to investigate without leaking secrets
At minimum, keep:
- Django application error logs
- Nginx access and error logs
- authentication failure visibility
- deploy event logs
Do not log:
- environment variable dumps
- raw secrets or tokens
- full request bodies containing credentials
- verbose debug tracebacks in user-facing responses
Add basic monitoring:
- uptime or health check endpoint
- alerting for 5xx spikes
- certificate expiry checks if possible
Explanation
This setup works because it splits responsibilities cleanly:
- Django handles application-level security settings, cookies, host validation, CSRF, and error behavior.
- Nginx handles public network exposure, TLS termination, redirects, static and media delivery, and forwarding trusted proxy headers.
- systemd runs the app with a non-root user and injects runtime secrets without committing them to source control.
- firewall and SSH config reduce how much of the server is reachable at all.
If you are using Docker, the same principles still apply. The exact file locations change, but the controls do not: no public app server port unless required, TLS handled deliberately, secrets injected at runtime, non-root execution where practical, and only required ports published.
Edge cases or notes
SECURE_SSL_REDIRECTvs Nginx redirect: If Nginx already redirects HTTP to HTTPS, keep the redirect there. That is usually simpler. But if Django can ever be reached directly, disablingSECURE_SSL_REDIRECTremoves an extra safety layer.- Proxy SSL header trust: Only set
SECURE_PROXY_SSL_HEADERwhen your proxy is under your control and always sends the expected header. - HSTS rollout: Start low. Bad HSTS settings can lock clients into HTTPS before your full setup is ready.
- Login or session issues: Overly strict cookie settings or wrong forwarded-proto handling can cause session loops or CSRF failures.
- Migrations before go-live: Security settings are not enough if the release itself is inconsistent. Run migrations in a predictable deploy sequence. Some schema migrations are not cleanly reversible. Take a verified backup before running production migrations, and do not assume rollback means
migratecan always undo the change. - Backups: Database dumps and env files are sensitive. Encrypt backups if possible and test restore steps, not just backup creation.
Minimum rollback plan:
- keep the previous Django settings and Nginx config
- revert the release artifact or config
- restart Gunicorn and Nginx
- re-run health and header checks
Internal links
For a broader baseline, see the Django production settings checklist.
If you need the full app server and proxy path, follow how to deploy Django with Gunicorn and Nginx.
For TLS-specific setup, see how to configure HTTPS for Django behind Nginx.
Before launch, use a final Django deployment checklist for production.
FAQ
What are the most important Django security settings for production?
At minimum: DEBUG=False, correct ALLOWED_HOSTS, SECRET_KEY from environment or secret storage, secure cookie flags, correct proxy SSL handling, and deliberate HSTS configuration.
Should Django handle HTTPS redirects or should Nginx handle them?
Usually Nginx should handle them. It keeps transport concerns at the proxy layer and avoids redirect confusion when Django is behind a reverse proxy.
Is python manage.py check --deploy enough to secure a Django deployment?
No. It is a useful baseline check, but it does not verify firewall rules, public port exposure, SSH hardening, patch levels, backup security, or whether your reverse proxy is configured safely.
How should I store SECRET_KEY and database credentials in production?
Load them from environment variables or a secret store at runtime. If you use an env file, keep it outside the repo with restricted ownership and permissions, and inject it through systemd or your container platform.
What is the safest way to add HSTS without breaking access?
Start with a small value like 300 seconds, verify HTTPS is stable across the site, then increase gradually. Delay includeSubDomains and preload until you are certain every covered hostname supports HTTPS correctly.