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:

  1. Gunicorn + WhiteNoise
  2. uWSGI
  3. Daphne / Hypercorn (ASGI)
  4. Eliminating the Nginx Sidecar Container
  5. Production Migration Checklist: Settings, Defaults & Rollback
  6. 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

ParadigmGunicorn CommandHurricane Equivalent
Basic WSGIgunicorn myproject.wsgi:application --bind 0.0.0.0:8000python manage.py serve --port=8000
Workers & Threadsgunicorn myproject.wsgi:application -w 4 --threads 2 -b 0.0.0.0:8000python manage.py serve --workers=8 --port=8000
With Static Filesgunicorn ... (relies on WhiteNoise middleware)python manage.py serve --port=8000 --static
With DB Migrationspython manage.py migrate && gunicorn ...python manage.py serve --check-migrations-apply

Step-by-Step Migration

  1. Add hurricane to INSTALLED_APPS in settings.py:
    settings.py
    INSTALLED_APPS = [
        # ...
        "hurricane",
    ]
    
  2. 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 from MIDDLEWARE:
    settings.py
    MIDDLEWARE = [
        "django.middleware.security.SecurityMiddleware",
        # "whitenoise.middleware.WhiteNoiseMiddleware",  # <-- Remove
        "django.contrib.sessions.middleware.SessionMiddleware",
        # ...
    ]
    
  3. Enable Hurricane's Native Static Streaming: Run Hurricane with the --static flag:
    python manage.py serve --port=8000 --static
    

    Hurricane streams static assets directly in Rust via kernel sendfile and 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 ParameterHurricane EquivalentRationale
http-socket = :8000--port=8000Direct HTTP socket binding.
master = trueBuilt-inHurricane natively manages worker processes.
processes = 4, threads = 2--workers=8Granian executes across a managed threadpool.
check-static = /path/to/static--staticAutomatically discovers STATIC_ROOT and STATIC_URL.
max-requests = 5000--max-memory=1024Soft memory monitoring prevents OOM crashes.
harakiri = 30Request timeoutDrops 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_APPLICATION in your settings.py and 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.

Rendering diagram...

Before vs. After: Pod Manifest Comparison

Before: Legacy Nginx Sidecar (2 Containers)
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
After: Hurricane (Single Container)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: django-app
spec:
  replicas: 5
  template:
    spec:
      containers:
      # Single Container handles App + Static + Probes
      - name: django
        image: my-django-app:latest
        command:
        - python
        - manage.py
        - serve
        - --port=8000
        - --probe-port=8001
        - --static
        - --check-migrations
        ports:
        - containerPort: 8000
          name: http
        - containerPort: 8001
          name: probes
        startupProbe:
          httpGet:
            path: /startup
            port: probes
        livenessProbe:
          httpGet:
            path: /alive
            port: probes
        readinessProbe:
          httpGet:
            path: /ready
            port: probes

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 emptyDir mounts and initialization race conditions between containers.
  • Single Log Stream: All access logs, static requests, and error traces flow into a unified stdout stream 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 / FeatureLegacy Stack (Gunicorn / Nginx / WhiteNoise)Hurricane BehaviorAction Required
SECURE_PROXY_SSL_HEADERConfigured in settings.pyCarries over 100%Keep as-is.
ALLOWED_HOSTSConfigured in settings.pyCarries over 100%Keep as-is.
STATIC_ROOT & MEDIA_ROOTConfigured in settings.pyAutomatically discovered when --static or --media is passedKeep as-is.
Proxy IP ExtractionNginx $proxy_add_x_forwarded_forResolves client IP from X-Forwarded-For (leftmost IP)Default is X-Forwarded-For. Pass --rate-limit-proxy-header if using CF-Connecting-IP.
Request LoggingGunicorn --access-logfile -Built-in structured access loggingEnabled out-of-the-box.
DB Connection PersistenceCONN_MAX_AGEFully respected across worker thread poolKeep as-is.

Phase 2: What Defaults Change Under Hurricane?

Before deploying Hurricane to production, review the following behavioral changes compared to traditional WSGI servers:

  1. Port Isolation (8000 vs. 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.
  2. 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-limit or set HURRICANE_RATE_LIMIT = False.
  3. 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 microseconds at 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, set HURRICANE_FAST_404 = "off".
  4. 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 returns HTTP 400 on /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-hurricane to your project dependencies (uv add django-hurricane or pip install django-hurricane).
  • 2. Register App: Add "hurricane" to INSTALLED_APPS in settings.py.
  • 3. Audit Middleware: (Optional) Remove whitenoise.middleware.WhiteNoiseMiddleware if 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 /ready probes to target port 8001.
  • 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:

  1. Zero Database Lock-in: Hurricane does not modify database schemas, alter Django models, or write proprietary migration state.
  2. Zero Code Lock-in: Adding "hurricane" to INSTALLED_APPS only registers Django management commands (manage.py serve). It has no runtime side-effects on Gunicorn, uWSGI, or standard Django code.
  3. Instant Rollback Command: To revert back to your previous runner at any time, simply switch the container command back:
    # Revert back to Gunicorn:
    command: ["gunicorn", "myproject.wsgi:application", "-b", "0.0.0.0:8000", "-w", "4"]
    
    No database rollbacks, migrations, or application code changes are required.

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:

  1. 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.
  2. 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.
  3. Zero Configuration: Unlike WAF rules that must be manually updated when routes change, Hurricane automatically introspects Django's urlpatterns dynamically.

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:

  1. 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.
  2. On-Premise / Private Storage: Many enterprise applications store files in cluster-local storage (ReadWriteMany PVCs, Ceph, MinIO) without external public cloud egress.
  3. Zero WhiteNoise CPU Bottlenecks: When you do serve assets from the container, Hurricane streams via kernel sendfile instead of Python worker threads.

Q3: "When should I use --check-migrations vs --check-migrations-apply?"

Deployment ScenarioRecommended FlagReason
Multi-Replica Kubernetes--check-migrationsPrevents 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-applyZero Overhead. Automatically applies schema updates before starting the web server.
Local Development--check-migrations-applySeamless 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:

  1. Migrations First, Then Code: Always apply schema migrations before rolling out new code. Old pods and new pods run simultaneously during the rollout.
  2. 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.
  3. Automate with django-migration-linter: Use django-migration-linter in 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.