Deploy Django on DigitalOcean Kubernetes (DOKS)

Deploy a Django app on DigitalOcean Kubernetes: build and push to DigitalOcean Container Registry, connect to Managed PostgreSQL and Valkey, expose it with a load balancer and HTTPS, run migrations as a Job, and store media in Spaces.

Problem statement

You want Kubernetes for a Django app without running the control plane yourself, and you already use DigitalOcean or want predictable pricing. DigitalOcean Kubernetes (DOKS) provides a managed control plane, node pools of Droplets, integrated load balancers, and block storage. The pieces Django needs, a container image, a database, a cache, static and media storage, migrations, and HTTPS, each map to a DigitalOcean service or a small Kubernetes object.

Django on DOKSBuild imagepush to DOCRDeploymentGunicorn podsLoad balancerIngress + TLSManaged dataPostgres, SpacesKubernetes runs the app; managed DigitalOcean services keep the state.
  1. Create a DOKS cluster and a DigitalOcean Container Registry (DOCR), and integrate the registry with the cluster.
  2. Build and push the Django image: docker buildx build --platform linux/amd64 -t registry.digitalocean.com/acme/web:$SHA --push .
  3. Create Managed PostgreSQL (and Managed Valkey or Redis if needed) in the same region and VPC; add the cluster as a trusted source.
  4. Store settings in a Secret, deploy with a Deployment and Service, and run migrate as a Job before each rollout.
  5. Install an ingress controller with cert-manager for HTTPS, and keep media in Spaces.

Step-by-step solution

1. Create the cluster and registry

doctl kubernetes cluster create acme-prod --region fra1 --version latest \
  --node-pool "name=web;size=s-2vcpu-4gb;count=2;auto-scale=true;min-nodes=2;max-nodes=4"
doctl registry create acme
doctl kubernetes cluster registry add acme-prod
doctl kubernetes cluster kubeconfig save acme-prod

registry add creates image pull credentials in the cluster, so pods can pull from DOCR without manual imagePullSecrets.

2. Build and push the image

doctl registry login
SHA=$(git rev-parse --short HEAD)
docker buildx build --platform linux/amd64 -t registry.digitalocean.com/acme/web:$SHA --push .

Use immutable tags (commit SHAs) so rollouts and rollbacks are predictable. Building with --platform linux/amd64 matters on Apple Silicon laptops, because DOKS nodes are amd64.

3. Connect managed databases

Create Managed PostgreSQL in the same region. In its settings, add the Kubernetes cluster as a trusted source, which restricts connections to the cluster's nodes. Use the private connection string and keep TLS on:

DATABASE_URL=postgres://doadmin:...@private-db-acme-do-user-123.db.ondigitalocean.com:25060/defaultdb?sslmode=require

Enable the managed connection pool (PgBouncer) for many pods, and point Django at the pool's port. Managed Valkey (Redis-compatible) works the same way for caching and Celery.

cluster and managed servicesLoad balancer + ingressTLS via cert-managerDjango pods (Deployment)Gunicorn, HPACelery podssame image, other commandManaged Postgres, Valkeyprivate network, trusted sourcesSpaces + CDNmedia and static filesStateless pods in the cluster, state in managed servicesdjango-deployment.com
Nothing inside the cluster needs a persistent volume in this layout.

4. Deploy the app

Create the Secret from an env file kept outside git:

kubectl create namespace prod
kubectl -n prod create secret generic django-env --from-env-file=prod.env
apiVersion: apps/v1
kind: Deployment
metadata: { name: web, namespace: prod }
spec:
  replicas: 2
  selector: { matchLabels: { app: web } }
  template:
    metadata: { labels: { app: web } }
    spec:
      containers:
        - name: web
          image: registry.digitalocean.com/acme/web:3f2a1c9
          command: ["gunicorn", "config.wsgi", "--bind", "0.0.0.0:8000", "--workers", "3"]
          envFrom: [{ secretRef: { name: django-env } }]
          ports: [{ containerPort: 8000 }]
          readinessProbe:
            httpGet:
              path: /healthz/
              port: 8000
              httpHeaders: [{ name: Host, value: app.example.com }]
          resources:
            requests: { cpu: 250m, memory: 384Mi }
            limits: { memory: 768Mi }
---
apiVersion: v1
kind: Service
metadata: { name: web, namespace: prod }
spec:
  selector: { app: web }
  ports: [{ port: 80, targetPort: 8000 }]

5. Run migrations as a Job

kubectl -n prod create job migrate-3f2a1c9 --image=registry.digitalocean.com/acme/web:3f2a1c9 \
  -- python manage.py migrate --noinput
kubectl -n prod wait --for=condition=complete job/migrate-3f2a1c9 --timeout=300s

The Job needs the same environment; in practice define it as YAML with envFrom and apply it from CI before updating the Deployment image. See Run Django Migrations as a Kubernetes Job.

6. Ingress, HTTPS, and media

Install a Gateway API implementation or ingress controller, such as Envoy Gateway or Traefik; its Service of type LoadBalancer provisions a DigitalOcean load balancer. The community ingress-nginx project has been retired, so prefer a maintained controller for new clusters. Add cert-manager with a Let's Encrypt issuer, then point your domain's DNS at the load balancer IP. Store media in Spaces with django-storages so pods stay stateless; see Store Django Media on DigitalOcean Spaces.

from commit to rollout1Build and push to DOCRtag = commit SHA2Run migrate Jobwait for completion3Update Deployment imagekubectl set image4Rolling updatereadiness gates traffic5Verify and keep old tagfor rollbackMigrations finish before new pods receive trafficdjango-deployment.com
Backward-compatible migrations let old pods keep working during the rollout.

Explanation

DOKS is upstream Kubernetes with DigitalOcean integrations: Services of type LoadBalancer create DigitalOcean load balancers, PersistentVolumeClaims create block storage volumes, and the registry integration handles image pulls. Keeping PostgreSQL, Valkey, and files in managed services means the cluster holds only replaceable pods, so node upgrades, which DOKS performs by draining and replacing Droplets, do not risk data.

Edge cases / notes

  • Node upgrades. Enable surge upgrades and run at least two replicas with a PodDisruptionBudget so upgrades do not cause downtime.
  • Volumes. DigitalOcean block storage is ReadWriteOnce; it cannot be shared by several pods. Do not use it for media.
  • Costs. The control plane is free on standard clusters; you pay for nodes, load balancers, and managed databases. The high-availability control plane option costs extra.

Next steps

FAQ

Is DOKS cheaper than App Platform for Django?

For a single small app, App Platform is usually simpler and similar in cost. DOKS pays off with several services, Celery workers, or custom networking.

Should I run PostgreSQL inside the cluster?

Not for most teams. Managed PostgreSQL handles backups, failover, and upgrades, which are hard to do well in Kubernetes.

How do pods pull from DigitalOcean Container Registry?

Run doctl kubernetes cluster registry add, which installs pull credentials for the registry in the cluster.

Where should media files go?

In Spaces, through django-storages. Pod file systems are temporary and not shared.

Keep going