{"post":{"slug":"how-to-deploy-a-django-app-2026","title":"How to deploy a Django app in 2026: the production checklist","excerpt":"Deploying Django in 2026 means a Gunicorn + Nginx stack or a managed platform, a hardened settings file, and migrations that run before traffic arrives.","category":"code","date":"February 23, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">D</span>jango is a mature framework, but production deployment still trips up teams who've only run `manage.py runserver`. In 2026, the path is well-understood: Gunicorn as the WSGI server, Whitenoise or a CDN for static files, a production settings module with no secrets, and migrations that run automatically before traffic arrives. This guide walks every step.\n\n## What changed in 2026\n\n- **Django 5.x** (current LTS branch) ships with async views and async ORM as first-class features — relevant if you're adding async endpoints.\n- **`uv`** is the dominant package manager; `uv sync --frozen` in Docker is faster than pip by an order of magnitude.\n- **Whitenoise 7** supports Brotli compression and HTTP/2 push hints, making it a viable CDN replacement for many small-to-medium apps.\n- **Managed platforms** (Railway, Fly.io, Render) handle TLS, zero-downtime deploys, and Postgres provisioning, dramatically reducing ops overhead for Django apps.\n\n## Settings split\n\n```\nmyproject/\n  settings/\n    __init__.py   # empty\n    base.py       # shared settings\n    development.py\n    production.py\n```\n\n```python\n# settings/base.py\nfrom pathlib import Path\nimport os\n\nBASE_DIR = Path(__file__).resolve().parent.parent.parent\nSECRET_KEY = os.environ[\"SECRET_KEY\"]  # never hardcode\nDEBUG = False  # override in development.py only\nALLOWED_HOSTS = os.environ.get(\"ALLOWED_HOSTS\", \"\").split(\",\")\n\nINSTALLED_APPS = [\n    \"django.contrib.admin\",\n    # ...\n    \"whitenoise.runserver_nostatic\",\n]\n\nMIDDLEWARE = [\n    \"django.middleware.security.SecurityMiddleware\",\n    \"whitenoise.middleware.WhiteNoiseMiddleware\",\n    # ...\n]\n\nSTATIC_ROOT = BASE_DIR / \"staticfiles\"\nSTATIC_URL = \"/static/\"\nSTORAGES = {\n    \"staticfiles\": {\n        \"BACKEND\": \"whitenoise.storage.CompressedManifestStaticFilesStorage\",\n    },\n}\n```\n\n```python\n# settings/production.py\nfrom .base import *\n\nSECURE_HSTS_SECONDS = 31536000\nSECURE_SSL_REDIRECT = True\nSESSION_COOKIE_SECURE = True\nCSRF_COOKIE_SECURE = True\n```\n\nSet `DJANGO_SETTINGS_MODULE=myproject.settings.production` via environment variable.\n\n## The Dockerfile\n\n```dockerfile\nFROM python:3.13-slim AS builder\nWORKDIR /app\nCOPY pyproject.toml uv.lock ./\nRUN pip install uv && uv sync --frozen --no-dev\n\nFROM python:3.13-slim AS runtime\nWORKDIR /app\nRUN adduser --disabled-password --gecos \"\" django\n\nCOPY --from=builder /app/.venv /app/.venv\nCOPY . .\n\nENV PATH=\"/app/.venv/bin:$PATH\"\nENV PYTHONDONTWRITEBYTECODE=1\nENV PYTHONUNBUFFERED=1\nENV DJANGO_SETTINGS_MODULE=myproject.settings.production\n\nUSER django\nEXPOSE 8000\n\n# Collect static, migrate, then start\nCMD [\"sh\", \"-c\", \"python manage.py collectstatic --noinput && python manage.py migrate && gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 4 --timeout 120 --access-logfile -\"]\n```\n\nFor production, separate the migration step into an init container or a pre-deploy hook so it runs once, not on every worker startup.\n\n## Gunicorn configuration\n\n```python\n# gunicorn.conf.py\nworkers = 4          # (2 × cores) + 1 is the heuristic\nworker_class = \"sync\"  # or \"gthread\" for threaded\ntimeout = 120\nbind = \"0.0.0.0:8000\"\naccesslog = \"-\"\nerrorlog = \"-\"\nloglevel = \"info\"\n```\n\nRun with: `gunicorn myproject.wsgi:application -c gunicorn.conf.py`\n\n## Django security checklist\n\n| Setting | Production value |\n| --- | --- |\n| `DEBUG` | `False` |\n| `SECRET_KEY` | Long random string, from env |\n| `ALLOWED_HOSTS` | Exact domain list, from env |\n| `SECURE_SSL_REDIRECT` | `True` |\n| `SECURE_HSTS_SECONDS` | `31536000` (1 year) |\n| `SESSION_COOKIE_SECURE` | `True` |\n| `CSRF_COOKIE_SECURE` | `True` |\n\nRun `python manage.py check --deploy` — Django outputs all security warnings automatically.\n\n## Static files options\n\n| Approach | When to use |\n| --- | --- |\n| Whitenoise | Simple apps; no CDN setup needed |\n| S3 + CloudFront | High-traffic; offload serving entirely |\n| R2 + Cloudflare CDN | S3-compatible, cheaper egress |\n\n## How to pick your deployment target\n\n1. **Railway** — drag-and-drop with a Dockerfile; provisions Postgres automatically. Fastest path to live.\n2. **Fly.io** — global PoPs, always-on, simple `fly deploy`. Good for latency-sensitive apps.\n3. **Render** — static sites, web services, and cron jobs in one dashboard; first-class Django support.\n4. **Heroku** — more expensive in 2026 but still has the most Django tutorials and ecosystem.\n5. **VPS (Hetzner, DigitalOcean)** — cheapest for steady-state traffic; you manage Nginx, Certbot, and supervisor.\n\n## Common mistakes\n\n**Running `runserver` in production.** It is not thread-safe, not multi-process, and runs in debug mode. Always Gunicorn.\n\n**Hardcoding `SECRET_KEY` in `settings.py`.** It ends up in git, then in your history forever. Read from env on startup.\n\n**Not collecting static files before deploy.** Django won't serve `/static/` through Gunicorn without Whitenoise, and it will 404.\n\n**Blocking the deploy on a long migration.** For tables with millions of rows, use `--fake` for schema-only changes and do data backfills separately.\n\n**Not setting `ALLOWED_HOSTS`.** Django raises `DisallowedHost` for every request; production will 400 all traffic.\n\n## What to skip\n\n- **`mod_wsgi` (Apache)** — valid historically, but Gunicorn + Nginx is far simpler and faster for new deployments.\n- **`daphne` unless you need channels** — ASGI overhead is not worth it for a pure WSGI Django app.\n- **Django's built-in development server for anything real** — even for load tests.\n\n## FAQ\n\n**Should I use WSGI or ASGI in 2026?**\nWSGI (Gunicorn) unless you need WebSockets or async views. Switching to ASGI (with Daphne or Uvicorn) adds complexity without benefit for standard request/response apps.\n\n**How do I run migrations safely?**\nUse an init container or a `release` phase command (Heroku, Railway support this). Run migrations before new app instances start to avoid schema mismatches.\n\n**How do I manage Django secrets in production?**\nAWS Secrets Manager, GCP Secret Manager, or Doppler — inject as environment variables at runtime. Never commit them.\n\n**How many Gunicorn workers for Django?**\nStart with `(2 × CPU cores) + 1`. Django views are typically I/O-bound (DB queries), so you can increase this, but watch RAM per worker (~50–150 MB each).\n\n## Where to go next\n\nSee [How to write a database migration in 2026](/blog/how-to-write-a-database-migration-2026), [How to set up Postgres locally in 2026](/blog/how-to-set-up-postgres-locally-2026), and [How to add full-text search in 2026](/blog/how-to-add-full-text-search-2026).","hue":321,"metaTitle":"How to Deploy a Django App 2026","metaDescription":"Complete 2026 guide to deploying Django in production. Covers Gunicorn, Docker, settings split, static files, migrations, and managed platform options.","keywords":["how to deploy django app 2026","django production deployment","django gunicorn nginx","django docker deployment","django settings production","django static files production","django database migrations deploy","deploy django to fly.io","django security checklist 2026","django railway deployment"],"tldr":["**Gunicorn is the production WSGI server** — never run `manage.py runserver` in production; it is single-threaded, debug-enabled, and not hardened.","**Separate settings for dev and prod** — use `DJANGO_SETTINGS_MODULE` or a `base.py` / `production.py` split; never commit `SECRET_KEY` or `DEBUG=True`.","**Run migrations before app startup** — use an init container or entrypoint script that runs `python manage.py migrate` then hands off to Gunicorn.","**Serve static files from a CDN or Whitenoise** — Django should not serve static assets through the WSGI layer in production.","**Skip:** deploying with `DEBUG=True`. It serves detailed error pages and disables several security headers — it is a data leak waiting to happen."],"readTime":"4 min read","wordCount":905},"related":[{"slug":"how-to-deploy-a-fastapi-app-2026","title":"How to deploy a FastAPI app in 2026: containers, cloud, and production config","excerpt":"Deploying FastAPI in 2026 means a minimal Docker image, a proper process manager, and the right cloud target. This guide covers every step from Dockerfile to live traffic.","category":"code","date":"April 15, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320},{"slug":"how-to-deploy-a-web-app-2026","title":"How to deploy a web app in 2026: from local to live in the right way","excerpt":"Deploying a web app in 2026 is faster than ever, but the choices are overwhelming. Here is the clear path from working locally to running reliably in production.","category":"code","date":"April 15, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":321},{"slug":"how-to-deploy-a-docker-container-2026","title":"How to Deploy a Docker Container in 2026: Real Steps","excerpt":"A plain-language 2026 walkthrough of how to deploy a docker container to a VPS, a managed platform, or an orchestrator, with the tradeoffs vendors skip.","category":"code","date":"May 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":145}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-deodorize-shoes-2026","title":"How to deodorize shoes in 2026: seven methods that actually eliminate odor","excerpt":"Shoe odor is caused by bacteria, not sweat itself. Here are seven methods ranked by effectiveness for deodorizing shoes and keeping them fresh in 2026.","category":"guides","date":"February 23, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":150},"older":{"slug":"how-to-be-more-productive-2026","title":"How to be more productive in 2026: systems that actually work","excerpt":"Most productivity advice is recycled busywork dressed up as insight. This is the 2026 framework for getting more done with less friction and more focus.","category":"guides","date":"February 22, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":150}}