Django on Kubernetes: The Complete Production Guide

Deploying Django applications into production Kubernetes clusters requires harmonizing Django's application lifecycle with Kubernetes container orchestration: synchronizing database migrations, establishing robust probe lifecycles, and configuring isolated ports for health monitoring.


Production Deployment Manifest

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: django-app
  labels:
    app: django-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: django-app
  template:
    metadata:
      labels:
        app: django-app
    spec:
      containers:
      - name: django
        image: my-registry.com/my-django-app:v1.0.0
        imagePullPolicy: IfNotPresent
        command:
        - "python"
        - "manage.py"
        - "serve"
        - "--port=8000"
        - "--probe-port=8001"
        - "--check-migrations-apply"
        - "--req-queue-len=15"
        ports:
        - name: http
          containerPort: 8000
        - name: probes
          containerPort: 8001
        resources:
          requests:
            cpu: "250m"
            memory: "256Mi"
          limits:
            cpu: "1000m"
            memory: "512Mi"
        startupProbe:
          httpGet:
            path: /startup
            port: probes
          initialDelaySeconds: 2
          periodSeconds: 2
          failureThreshold: 30
          timeoutSeconds: 2
        livenessProbe:
          httpGet:
            path: /alive
            port: probes
          periodSeconds: 5
          timeoutSeconds: 2
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: probes
          periodSeconds: 2
          timeoutSeconds: 2
          failureThreshold: 2

Kubernetes Service Manifest

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: django-app-service
spec:
  type: ClusterIP
  selector:
    app: django-app
  ports:
  - name: http
    port: 80
    targetPort: http
  - name: metrics
    port: 8001
    targetPort: probes

Probe Tuning Best Practices

  1. Always Use a Startup Probe: Without a startup probe, Kubernetes starts evaluating the liveness probe immediately upon container start. If Django takes 10 seconds to load models or apply database migrations, the liveness probe will kill the container before it finishes booting.
  2. Set a Short Period for Readiness: Configure periodSeconds: 2 on /ready so Kubernetes immediately sheds load when a worker pool experiences a congestion spike and restores traffic within 2 seconds of recovery.
  3. Isolate Probes with --probe-port: Running probes on a dedicated port (e.g. 8001) prevents heavy file uploads or long-running queries on port 8000 from starving kubelet health check requests.

Production Best Practices

1. Sizing Workers and Threads: Cost Efficiency vs. Horizontal Scaling

Choosing the optimal worker and thread configuration depends on your cloud economics and scaling strategy:

  • Cost-Optimized Strategy (High Node Density): Running larger Pods with higher concurrency (--workers and --runtime-threads) is substantially more cost-effective than running dozens of tiny micro-pods. Every additional Pod incurs Kubernetes baseline overhead (kubelet tracking, CoreDNS endpoint registrations, container runtime memory, CNI networking, and sidecar footprints). Sizing --workers to match your container's allocated CPU cores allows a single Pod to handle hundreds of concurrent requests, maximizing throughput per dollar and driving down cloud infrastructure bills.
  • Horizontal Autoscaling Strategy (Traffic Volatility): If traffic is bursty or highly unpredictable, combine multi-worker Pods with the Horizontal Pod Autoscaler (HPA) to scale out replica counts smoothly under peak traffic waves.
  • Dynamic Sizing Defaults: By default, Granian's Python worker threadpool automatically sizes to min(32, CPUs + 4). For ASGI mode, --runtime-threads automatically scales Tokio event loop threads across all allocated container CPU cores.

2. Zero-Downtime Rolling Deployments (preStop Hook)

When Kubernetes terminates a Pod during a rolling update, the endpoint removal in kube-proxy is asynchronous and takes a few seconds. If the application terminates immediately upon receiving SIGTERM, clients may experience 502 Bad Gateway errors.

Add a simple preStop sleep hook in your container spec to ensure zero-downtime traffic draining:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5"]

This guarantees the Pod is completely removed from all Service and Ingress endpoints before Hurricane initiates its graceful shutdown and finishes in-flight requests.

3. Memory Guardrails vs. Kernel OOM Killer

If a memory leak occurs, the Linux kernel's Out-Of-Memory (OOM) killer abruptly terminates the container with an uncatchable SIGKILL (exit code 137).

To prevent unexpected crashes, set Hurricane's --max-memory parameter to approximately 75-80% of the Pod's cgroup limit:

resources:
  limits:
    memory: "512Mi"
command:
- "python"
- "manage.py"
- "serve"
- "--max-memory=400"

When worker RSS exceeds 400 MB, Granian initiates a graceful worker respawn without dropping in-flight requests.

4. Horizontal Pod Autoscaling (HPA)

Scale your deployment dynamically using CPU metrics or Hurricane's Prometheus queue backlog:

hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: django-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: django-app
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 75

Frequently Asked Questions (FAQ)

How should Django database migrations be handled in Kubernetes?

Rather than relying on complex Helm pre-install hooks or fragile initContainers that run unconditionally on every replica, use Hurricane's --check-migrations-apply flag in the container command:

command: ["python", "manage.py", "serve", "--check-migrations-apply"]

This executes pending migrations during container initialization before marking /startup healthy, ensuring no pod receives traffic until database schemas match the deployed code.

Why does Django need a Kubernetes startup probe?

Django applications often perform database migrations, model cache warming, and initial asset checks during boot. If only a liveness probe is configured with short timeouts, Kubernetes will kill the container while it is legitimately applying migrations. A startupProbe with a generous failureThreshold: 60 and periodSeconds: 2 grants up to 120 seconds for initialization while keeping steady-state liveness checks fast (e.g. 5 seconds).

How do you eliminate 502 Bad Gateway errors during rolling updates in Django?

When Kubernetes terminates an old pod, kube-proxy takes 2 to 5 seconds to propagate endpoint removal to the Ingress Controller. If your application terminates immediately upon SIGTERM, clients whose requests are in transit receive 502 Bad Gateway. Adding a lifecycle.preStop hook with sleep 5 ensures the pod stops receiving new ingress traffic before Hurricane finishes in-flight requests and exits.

How should I configure workers and concurrency for Django in Kubernetes?

For optimal cost efficiency, size worker threads (--workers) to match or slightly exceed the CPU cores allocated to your Pod. Running multi-worker concurrency per container maximizes throughput per node and drastically reduces cloud spend by avoiding the CPU and memory tax of spinning up unnecessary extra pods. For horizontal resilience, deploy at least 2–3 multi-worker replicas behind an HPA to ensure high availability during rolling updates.

How do you serve Django static and media files in Kubernetes without an Nginx sidecar?

Hurricane includes built-in static and media file serving directly in its Rust/Granian gateway. Passing --static and --media enables Hurricane to serve assets directly from disk with sub-millisecond latency, completely eliminating the need for an auxiliary Nginx sidecar container in your Pod.