Django Deployment Checklist for Production
A Django app that works locally is not automatically safe to run in production. Most deployment failures come from a small set of issues: wrong settings, missing secrets, unsafe...
Problem statement
A Django app that works locally is not automatically safe to run in production. Most deployment failures come from a small set of issues: wrong settings, missing secrets, unsafe TLS or proxy handling, unreviewed migrations, broken static files, or no rollback plan.
This Django deployment checklist for production is a practical pre-release and post-release runbook. It helps you verify that your app is ready to go live on a Linux server, VM, or container-based stack using common components like Gunicorn, Uvicorn, Nginx, Caddy, PostgreSQL, and Redis.
What this Django deployment checklist covers
This checklist focuses on production readiness:
- Django settings and environment separation
- secrets and credentials
- database readiness and backups
- static and media file handling
- app server, reverse proxy, and TLS checks
- logging, monitoring, and background workers
- deployment order, rollback, and post-deploy smoke tests
What this checklist does not replace
It does not replace:
- a full infrastructure design
- app-specific load testing
- a disaster recovery plan for your entire platform
- platform-specific docs from your host or managed database provider
Quick answer
Before go-live, verify these minimum items:
DEBUG = FalseALLOWED_HOSTSis correctSECRET_KEYand database credentials come from environment variables or a secret manager- PostgreSQL is reachable and backups are enabled
python manage.py check --deploypasses- migrations are reviewed before applying
collectstaticsucceeds- Gunicorn or Uvicorn starts cleanly
- Nginx or Caddy serves the app over HTTPS
- logs, health checks, and rollback steps are ready
The highest-risk items to verify first
If time is limited, check these first:
- secrets are not hardcoded or committed
- database migrations are safe for the current release
- TLS and proxy headers are correct
- static files and media storage are configured correctly
- rollback is possible if the release fails
Django deployment checklist for production
1. Confirm environment separation
Production settings must be separate from local development behavior.
Minimum checks:
DEBUG = FalseALLOWED_HOSTSincludes the real domain names- production settings are not mixed with dev defaults
- secrets are not stored in Git
- required environment variables must exist at startup
Example settings:
import os
DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
Verification:
env | grep DJANGO
python manage.py check --deploy
Rollback note: if the app fails at startup after a settings change, revert the settings file or restore the previous environment file before restarting the process.
2. Verify secret and credential handling
Do not hardcode production credentials in Django settings, Compose files committed to Git, or service files.
Check that these come from environment variables or a secret manager:
SECRET_KEY- database username and password
- email credentials
- third-party API keys
- Redis URLs if used
Environment file structure example:
DJANGO_SECRET_KEY=replace-with-a-real-secret
DATABASE_URL=postgresql://appuser:strongpassword@127.0.0.1:5432/appdb
REDIS_URL=redis://127.0.0.1:6379/0
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
Also confirm a basic rotation process exists. Even if it is manual, document where secrets live and how they are updated.
3. Validate database readiness
Before deployment:
- production PostgreSQL database exists
- connectivity works
- the application user has the right permissions
- backups are enabled
- restore has been tested at least once
- migration impact has been reviewed
Useful commands:
python manage.py dbshell
python manage.py showmigrations
Apply migrations only when you understand what they will do:
python manage.py migrate
Rollback note: database rollbacks are often harder than code rollbacks. If a migration is destructive, locks large tables, or is not backward-compatible with older code, plan that release separately.
4. Review static and media file handling
Static files and user uploads are different problems.
Check static files:
python manage.py collectstatic --noinput
Verify:
- static files are served by Nginx, Caddy, or object storage
- hashed filenames or cache-safe asset versioning is in place where relevant
- browser requests for CSS and JS return
200
For media:
- uploads use persistent storage
- the storage path survives container restarts or server redeploys
- file permissions are correct
If user uploads are stored on local disk, confirm the mount or directory is backed up and not tied to disposable releases.
5. Check application server configuration
Your app server should start cleanly before you route live traffic to it.
Gunicorn test start:
gunicorn config.wsgi:application --bind 127.0.0.1:8000
systemd checks:
sudo systemctl status gunicorn
sudo systemctl restart gunicorn
sudo journalctl -u gunicorn -n 100 --no-pager
Example systemd excerpt:
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/srv/myapp/shared/.env
ExecStart=/srv/myapp/venv/bin/gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3 --timeout 60
Restart=always
Verify:
- worker count is chosen intentionally
- timeout is not left at an unsuitable default
- restart policy exists
- a health endpoint such as
/healthzexists if possible
6. Check reverse proxy and TLS
Your reverse proxy must route requests correctly and preserve HTTPS information for Django.
Nginx checks:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://example.com/
curl https://example.com/healthz
Example Nginx server block:
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;
location /static/ {
alias /srv/myapp/shared/static/;
}
location /media/ {
alias /srv/myapp/shared/media/;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}
In Django, review:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
Only set SECURE_PROXY_SSL_HEADER if your reverse proxy is trusted to set and sanitize X-Forwarded-Proto. Do not trust client-supplied forwarding headers directly.
Also verify secure behavior from the browser side:
- HTTP redirects to HTTPS
- secure cookies are set over HTTPS
- login and CSRF-protected forms work behind the proxy
- redirect loops do not occur
If your deployment depends on forwarded host headers, review whether USE_X_FORWARDED_HOST = True is actually needed. Do not enable it unless your proxy setup requires it.
7. Harden Django security settings
At minimum, review these settings:
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = False
Enable HSTS only after HTTPS is working correctly for the domain, and be careful with SECURE_HSTS_INCLUDE_SUBDOMAINS if not all subdomains are HTTPS-ready.
Also check:
CSRF_TRUSTED_ORIGINS = ["https://example.com", "https://www.example.com"]
And review:
- admin exposure is intentional
- unnecessary debug tools are removed
- CSRF and session settings match your real HTTPS origins
Run:
python manage.py check --deploy
8. Run pre-deployment verification
Before release, run a short verification block:
python manage.py check --deploy
python manage.py showmigrations
python manage.py collectstatic --noinput
Confirm:
- dependency installation succeeds
- required environment variables exist
- external services are reachable
- release artifact or image tag is correct
For Docker:
docker compose ps
docker compose logs web --tail=100
docker compose exec web python manage.py check --deploy
9. Plan the deployment sequence
A simple safe sequence is:
- pull code or deploy image
- install dependencies if needed
- run migrations
- run
collectstatic - restart or reload app process
- confirm web health
- confirm workers and scheduled jobs
Non-Docker example:
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn
sudo systemctl status gunicorn
curl https://example.com/healthz
Docker example:
docker compose pull
docker compose up -d db redis
docker compose run --rm web python manage.py migrate
docker compose run --rm web python manage.py collectstatic --noinput
docker compose up -d web
docker compose logs web --tail=100
Do not assume docker compose up -d before migrations is safe. Make the order explicit so new app containers do not start against an incompatible schema.
If your deployment process depends on migrations completing before app restart, keep that order explicit and documented.
10. Confirm observability and logging
At minimum, make sure you can access:
- Django application logs
- Gunicorn or Uvicorn logs
- Nginx or Caddy logs
- error tracking if configured
- uptime or health monitoring
Verification:
sudo journalctl -u gunicorn -n 100 --no-pager
docker compose logs web --tail=100
If you cannot inspect logs quickly during a release, production debugging will be slow.
11. Verify background jobs and async components
If you use Celery, Redis, Channels, or scheduled jobs, include them in the release checklist.
Verify:
- Celery workers start
- Celery beat is enabled if used
- Redis is reachable
- retry behavior and queue backlog are understood
A deployment is not complete if the web app is healthy but background jobs are dead.
12. Prepare rollback and recovery steps
Before deploying, define:
- how to restore the previous release
- how to redeploy a previous image tag or release directory
- whether the new code is compatible with current database schema
- who decides rollback
Important: code rollback may not fully undo a migration. If the migration changed schema in a non-backward-compatible way, rollback may require a separate database plan.
13. Run post-deployment checks
After release, smoke test the app:
- homepage loads over HTTPS
- login works
- admin login works if enabled
- a database write succeeds
- CSS and JS load correctly
- one critical user flow completes
- background workers are processing jobs
Useful checks:
curl -I https://example.com/
curl https://example.com/healthz
Example production verification workflow
Manual checklist for a non-Docker server
- SSH into the server
- confirm
.envor secret source is updated - activate the virtualenv
- run
check --deploy - review migrations
- run
migrate - run
collectstatic - restart Gunicorn
- test Nginx config and reload if changed
- smoke test the live site
- watch logs for a few minutes
Manual checklist for a Docker-based deployment
- confirm image tag or release commit
- confirm environment variables for containers
docker compose pull- start only required dependencies first if needed
- run
migratein a one-off web container or controlled release step - run
collectstaticif your setup requires it - start or update the web container
- inspect container logs
- test HTTPS and health endpoints
- verify worker containers separately
Where teams usually miss production checks
Common misses:
ALLOWED_HOSTSnot updated- no persistent media storage
- migrations applied without review
- TLS works externally but proxy headers are wrong internally
- Celery not restarted with the release
- no tested rollback path
When to convert this process into scripts or templates
Once your release steps become repetitive, script the checks in the same order every time. Good first targets are environment validation, check --deploy, migration execution, collectstatic, service restarts, and smoke tests. Reusable templates also help for Django settings, systemd units, reverse proxy configs, and CI/CD workflows.
Explanation
Why this checklist is ordered by deployment risk
The highest-risk items come first: settings, secrets, database state, and TLS. If any of those are wrong, the deployment can fail completely or expose data. Static assets, workers, and monitoring matter too, but they are easier to diagnose after the core release path is safe.
Why migrations, secrets, and TLS should be verified before release
Migrations can lock tables or make old code incompatible. Secrets can stop the app from starting or expose production systems. TLS and proxy header mistakes can break redirects, secure cookies, CSRF validation, and login behavior.
Why rollback must be planned before the deployment starts
Rollback is a release requirement, not a cleanup step. If you only think about recovery after a failed deployment, you are already in incident mode.
Edge cases and notes
Deploying behind a load balancer or platform proxy
If Django is behind Nginx, Caddy, a cloud load balancer, or another proxy, confirm forwarded protocol headers are set correctly and Django trusts the right header via SECURE_PROXY_SSL_HEADER.
If your app needs original host preservation through multiple proxy layers, review USE_X_FORWARDED_HOST carefully rather than enabling it by default.
Zero-downtime vs simple restart deployments
A small app can often tolerate a short restart. For larger apps, use a deployment model that keeps old processes serving traffic until new ones pass health checks. Do not assume zero downtime if migrations are blocking.
Handling apps with user uploads
Treat media as persistent data. Do not store uploads inside a release directory that gets replaced on deploy. Use a shared mount or object storage.
Multi-service apps with Celery, Redis, or WebSockets
Include every service in the checklist. A healthy Django web process is only one part of the system if your app also depends on workers, schedulers, Redis, or ASGI or WebSocket components.
Internal links
For production settings, see Django Production Settings Checklist (DEBUG, ALLOWED_HOSTS, CSRF).
For deployment stack decisions, see Django WSGI vs ASGI: Which One Should You Deploy?.
For file handling in production, see Django Static vs Media Files in Production.
For step-by-step server deployment examples, see deploy Django with Gunicorn and Nginx and deploy Django with Docker Compose.
For recovery planning, see Django deployment rollbacks and recovery.
FAQ
What is the minimum Django deployment checklist for a small app?
At minimum: DEBUG = False, correct ALLOWED_HOSTS, secrets from environment variables, PostgreSQL backups, HTTPS enabled, check --deploy, reviewed migrations, working static files, accessible logs, and a basic rollback plan.
Should I run migrations before or after restarting Gunicorn?
Usually before restarting Gunicorn, so the new process starts against the expected schema. But the safest order depends on whether the release is backward-compatible with the current database state. For risky schema changes, plan the deployment carefully rather than relying on a default order.
What is the safest way to store Django production secrets?
Use a secret manager if available. If not, use environment variables or a protected environment file outside the repository, with restricted permissions and a documented rotation process.
What should I verify immediately after deploying Django?
Check HTTPS, homepage response, login, one write to the database, static asset loading, health endpoint response, worker status, and recent application logs.
Do small Django apps still need TLS, backups, and monitoring?
Yes. Small apps fail in the same ways as larger ones. TLS protects sessions and logins, backups protect your data, and basic monitoring helps you detect failures quickly.