Migrating from Gunicorn, uWSGI & Daphne
Migrating an existing Django project to Hurricane typically takes less than 15 minutes. Because Hurricane integrates directly via Django's management command interface (python manage.py serve), you can eliminate external server configuration files, custom WSGI/ASGI wrappers, and Nginx reverse proxy containers.
This guide provides side-by-side recipes for migrating from the three most common Django application server setups:
- Gunicorn + WhiteNoise
- uWSGI
- Daphne / Hypercorn (ASGI)
- Eliminating the Nginx Sidecar Container
- Production Migration Checklist: Settings, Defaults & Rollback
- Community FAQ & Architectural Trade-offs
1. Migrating from Gunicorn + WhiteNoise
Gunicorn is the most common WSGI runner for Django, frequently paired with WhiteNoise for static file serving.
Command Comparison
| Paradigm | Gunicorn Command | Hurricane Equivalent |
|---|---|---|
| Basic WSGI | gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 | python manage.py serve --port=8000 |
| Workers & Threads | gunicorn myproject.wsgi:application -w 4 --threads 2 -b 0.0.0.0:8000 | python manage.py serve --workers=8 --port=8000 |
| With Static Files | gunicorn ... (relies on WhiteNoise middleware) | python manage.py serve --port=8000 --static |
| With DB Migrations | python manage.py migrate && gunicorn ... | python manage.py serve --check-migrations-apply |
Step-by-Step Migration
- Add
hurricanetoINSTALLED_APPSinsettings.py:settings.pyINSTALLED_APPS = [ # ... "hurricane", ] - Remove WhiteNoise Middleware (Optional but Recommended):
WhiteNoise routes all static file requests through Python's WSGI middleware stack, consuming Python CPU and memory for binary file I/O.
Remove WhiteNoise fromMIDDLEWARE:settings.pyMIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", # "whitenoise.middleware.WhiteNoiseMiddleware", # <-- Remove "django.contrib.sessions.middleware.SessionMiddleware", # ... ] - Enable Hurricane's Native Static Streaming:
Run Hurricane with the
--staticflag:python manage.py serve --port=8000 --static
Hurricane streams static assets directly in Rust via kernelsendfileand automatic HTTP 304 caching, achieving Nginx-grade speed (< 20µs) without touching Python middleware.
2. Migrating from uWSGI
uWSGI provides extensive configuration options but carries a heavy C codebase, complex memory management, and esoteric .ini configuration files.
Configuration Mapping
uwsgi.ini Parameter | Hurricane Equivalent | Rationale |
|---|---|---|
http-socket = :8000 | --port=8000 | Direct HTTP socket binding. |
master = true | Built-in | Hurricane natively manages worker processes. |
processes = 4, threads = 2 | --workers=8 | Granian executes across a managed threadpool. |
check-static = /path/to/static | --static | Automatically discovers STATIC_ROOT and STATIC_URL. |
max-requests = 5000 | --max-memory=1024 | Soft memory monitoring prevents OOM crashes. |
harakiri = 30 | Request timeout | Drops stalled requests gracefully. |
Replacing uwsgi.ini
If your project currently starts via:
uwsgi --ini uwsgi.ini
Replace the .ini file with standard Django settings or CLI arguments:
python manage.py serve \
--port=8000 \
--workers=8 \
--static \
--media \
--check-migrations-apply
3. Migrating from Daphne / Hypercorn
When migrating an asynchronous Django project (using Django Channels, async views, or WebSockets):
- No Code Changes Required: Hurricane automatically detects
ASGI_APPLICATIONin yoursettings.pyand initializes Granian's native ASGI coroutine runtime. - WebSocket & Async View Support: WebSockets and asynchronous HTTP views run directly on the Tokio event loop, delivering non-blocking I/O and flat tail latencies under high concurrency.
# Daphne
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application
# Replaced with Hurricane
python manage.py serve --port=8000
4. Eliminating the Nginx Sidecar Container
In Kubernetes, the standard practice for Django has been deploying an Nginx sidecar container alongside Gunicorn in every single pod to serve static assets and handle proxy buffering.
Before vs. After: Pod Manifest Comparison
apiVersion: apps/v1
kind: Deployment
metadata:
name: django-app
spec:
replicas: 5
template:
spec:
volumes:
- name: static-volume
emptyDir: {}
- name: nginx-config
configMap:
name: nginx-conf
containers:
# Container 1: Nginx Sidecar
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: static-volume
mountPath: /var/www/static
- name: nginx-config
mountPath: /etc/nginx/conf.d
# Container 2: Django App
- name: django
image: my-django-app:latest
command: ["gunicorn", "myproject.wsgi:application", "-b", "127.0.0.1:8000"]
volumeMounts:
- name: static-volume
mountPath: /app/staticfiles
Operational Gains from Eliminating Nginx:
- 50% RAM Savings: Eliminates Nginx process overhead (~80–150 MB per pod). In a 20-pod cluster, this saves 2 to 3 GB of memory.
- No Shared Volumes: Eliminates
emptyDirmounts and initialization race conditions between containers. - Single Log Stream: All access logs, static requests, and error traces flow into a unified
stdoutstream with structured JSON formatting.
5. Production Migration Checklist: Settings, Defaults & Rollback
Consolidating multiple infrastructure layers (Gunicorn, Nginx, WhiteNoise, and Prometheus exporters) into a single runtime makes evaluation simpler—provided you know exactly what settings carry over, which defaults change, and how to roll back.
Phase 1: Settings Carry-Over Matrix
| Setting / Feature | Legacy Stack (Gunicorn / Nginx / WhiteNoise) | Hurricane Behavior | Action Required |
|---|---|---|---|
SECURE_PROXY_SSL_HEADER | Configured in settings.py | Carries over 100% | Keep as-is. |
ALLOWED_HOSTS | Configured in settings.py | Carries over 100% | Keep as-is. |
STATIC_ROOT & MEDIA_ROOT | Configured in settings.py | Automatically discovered when --static or --media is passed | Keep as-is. |
| Proxy IP Extraction | Nginx $proxy_add_x_forwarded_for | Resolves client IP from X-Forwarded-For (leftmost IP) | Default is X-Forwarded-For. Pass --rate-limit-proxy-header if using CF-Connecting-IP. |
| Request Logging | Gunicorn --access-logfile - | Built-in structured access logging | Enabled out-of-the-box. |
| DB Connection Persistence | CONN_MAX_AGE | Fully respected across worker thread pool | Keep as-is. |
Phase 2: What Defaults Change Under Hurricane?
Before deploying Hurricane to production, review the following behavioral changes compared to traditional WSGI servers:
- Port Isolation (
8000vs.8001):- Legacy Gunicorn: Typically serves all requests and health checks on port 8000.
- Hurricane Default: Application traffic runs on
--port=8000, while probes (/alive,/ready,/startup) and Prometheus metrics (/metrics) run on--probe-port=8001(port + 1). - Adjustment: If you require all endpoints on a single port (e.g. for Fly.io or AWS ECS), pass
--probe-port=8000.
- Rate Limiting is Enabled by Default:
- Legacy Gunicorn: No rate limiting.
- Hurricane Default: In-memory token bucket rate limiter is active at 10 requests/second with a burst of 30 per client IP.
- Adjustment: If your edge CDN/WAF already handles rate limiting and you wish to disable it at the container layer, pass
--no-rate-limitor setHURRICANE_RATE_LIMIT = False.
- Fast-404 Edge Defense is Active by Default:
- Legacy Gunicorn: Every automated scanner probe (
.php,/.env,/wp-admin/) enters Python middleware and executes database queries. - Hurricane Default: Intercepts scanner attacks in
< 0.35 microsecondsat Tier 2 and drops them silently. - Adjustment: Active by default (
auto). If you run a WordPress blog on the same domain behind a reverse proxy, setHURRICANE_FAST_404 = "off".
- Legacy Gunicorn: Every automated scanner probe (
- Dynamic Traffic Shedding Under Backlog Congestion:
- Legacy Gunicorn: Requests queue indefinitely in the listen backlog until reverse proxies time out with 504 errors.
- Hurricane Default: If pending request backlog exceeds
--req-queue-len(default:workers * 2), Hurricane returnsHTTP 400on/ready, prompting Ingress to shed traffic to healthy sibling pods before memory exhausts.
Phase 3: The 6-Step Migration Checklist
Use this checklist during your staging and production rollout:
- 1. Add Dependency: Add
django-hurricaneto your project dependencies (uv add django-hurricaneorpip install django-hurricane). - 2. Register App: Add
"hurricane"toINSTALLED_APPSinsettings.py. - 3. Audit Middleware: (Optional) Remove
whitenoise.middleware.WhiteNoiseMiddlewareif using--static. - 4. Configure Container Command: Update Dockerfile or Kubernetes manifest to:
python manage.py serve --port=8000 --probe-port=8001 --static --check-migrations - 5. Update Health Probes: Configure
/startup,/alive, and/readyprobes to target port8001. - 6. Run Canary Verification: Deploy a single canary replica alongside existing Gunicorn pods to verify logs, latency, and metrics before full 100% rollout.
Phase 4: Production Rollback Plan
If you ever need to roll back a deployment, Hurricane makes it completely painless because there is zero vendor or database lock-in:
- Zero Database Lock-in: Hurricane does not modify database schemas, alter Django models, or write proprietary migration state.
- Zero Code Lock-in: Adding
"hurricane"toINSTALLED_APPSonly registers Django management commands (manage.py serve). It has no runtime side-effects on Gunicorn, uWSGI, or standard Django code. - Instant Rollback Command: To revert back to your previous runner at any time, simply switch the container command back:
No database rollbacks, migrations, or application code changes are required.
# Revert back to Gunicorn: command: ["gunicorn", "myproject.wsgi:application", "-b", "0.0.0.0:8000", "-w", "4"]
6. Community FAQ & Architectural Trade-offs
Here are answers to the most common questions and architectural trade-offs raised by the Django and Kubernetes communities:
Q1: "Should I still use Cloudflare or an AWS WAF if Hurricane has Fast-404?"
Yes, absolutely. If your application is public-facing and your organization uses Cloudflare, AWS WAF, or Fastly, keep it!
Hurricane's Fast-404 is designed as defense-in-depth:
- WAF Bypass Protection: Any scan that slips through a WAF rule or targets an unshielded origin IP will hit a standard Gunicorn worker, spinning up middleware and exhausting database connections. Hurricane drops it before touching Django.
- Private & Sovereign Clouds: Many enterprise systems run on on-premise Kubernetes bare-metal, air-gapped intranets, or European sovereign clouds where Cloudflare is forbidden by compliance.
- Zero Configuration: Unlike WAF rules that must be manually updated when routes change, Hurricane automatically introspects Django's
urlpatternsdynamically.
Q2: "Why serve static/media from the pod instead of S3 + CloudFront?"
For hyperscale public e-commerce or media sites, offloading media to S3 and a global CDN is standard best practice.
However, Hurricane's native static serving solves major operational pain points:
- Dev, Staging & Ephemeral PR Review Pods: Provisioning dedicated S3 buckets, IAM roles, cross-account permissions, and CORS configurations for every staging or PR preview environment is operational overkill.
- On-Premise / Private Storage: Many enterprise applications store files in cluster-local storage (ReadWriteMany PVCs, Ceph, MinIO) without external public cloud egress.
- Zero WhiteNoise CPU Bottlenecks: When you do serve assets from the container, Hurricane streams via kernel
sendfileinstead of Python worker threads.
Q3: "When should I use --check-migrations vs --check-migrations-apply?"
| Deployment Scenario | Recommended Flag | Reason |
|---|---|---|
| Multi-Replica Kubernetes | --check-migrations | Prevents Race Conditions. Pods verify migrations are applied but do not execute them. Run migrations in a single-instance Kubernetes Job. |
| Single-Container Docker Compose | --check-migrations-apply | Zero Overhead. Automatically applies schema updates before starting the web server. |
| Local Development | --check-migrations-apply | Seamless developer experience. |
| PaaS (Fly.io, AWS ECS) | --check-migrations-apply (or release command) | Convenient automated deployment for single-task rolling updates. |
Q4: "How do you achieve true zero-downtime deploys with database schema changes?"
The secret to zero-downtime deployments with Django is two-fold:
- Migrations First, Then Code: Always apply schema migrations before rolling out new code. Old pods and new pods run simultaneously during the rollout.
- Backward-Compatible Migrations (Expand/Contract): Every migration must be compatible with the old running code. Breaking schema changes (renaming fields, adding NOT NULL without defaults, dropping columns) must be split across multiple sequential deployments.
- Automate with
django-migration-linter: Usedjango-migration-linterin CI to block non-backward-compatible DDL operations before they merge.
Read the complete guide in our Kubernetes Production Guide: Zero-Downtime Deployments & Backward-Compatible Migrations.
