Manage Django Kubernetes Manifests With Kustomize Overlays

Structure Django Kubernetes manifests with Kustomize: a base with Deployment, Service, and migration Job, overlays for staging and production with replicas, resources, and hosts, image tags set from CI, ConfigMap generators with hash suffixes, and use with Argo CD.

Problem statement

Your Django app has nearly identical Kubernetes manifests for staging and production, copied and edited by hand: different replica counts, host names, resource limits, and image tags. Copies drift, and a fix applied to one environment is forgotten in the other. Kustomize, built into kubectl, lets you keep one base and small per-environment patches, with no templating language.

Kustomize for Djangobase/shared manifestsoverlays/stagingpatches, 1 replicaoverlays/prodpatches, HPA, hostskubectl apply -krendered YAMLOne base holds the shared manifests; overlays change only what differs.
  1. Put shared manifests in base/: web Deployment, worker Deployment, Service, migration Job, and a kustomization.yaml listing them.
  2. Create overlays/staging and overlays/production, each referencing ../../base and adding patches.
  3. Generate ConfigMaps with configMapGenerator; the hash suffix triggers rollouts when config changes.
  4. Set the image tag per release with kustomize edit set image in CI, or let Argo CD do it.
  5. Preview with kubectl kustomize overlays/production and apply with kubectl apply -k overlays/production.

Step-by-step solution

1. Lay out the repository

deploy/
  base/
    kustomization.yaml
    web-deployment.yaml
    worker-deployment.yaml
    service.yaml
  overlays/
    staging/
      kustomization.yaml
      replicas.yaml
    production/
      kustomization.yaml
      replicas.yaml
      resources.yaml
      hpa.yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - web-deployment.yaml
  - worker-deployment.yaml
  - service.yaml
labels:
  - pairs: { app.kubernetes.io/name: shop }
    includeSelectors: true
images:
  - name: shop
    newName: ghcr.io/acme/shop

In the base manifests, refer to the image simply as shop; the images field rewrites it.

2. Write overlays

# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop-prod
resources:
  - ../../base
  - hpa.yaml
patches:
  - path: replicas.yaml
  - path: resources.yaml
configMapGenerator:
  - name: django-config
    literals:
      - DJANGO_SETTINGS_MODULE=config.settings.production
      - ALLOWED_HOSTS=shop.example.com
images:
  - name: shop
    newTag: 3f2a1c9
# overlays/production/replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: web }
spec:
  replicas: 3
from base to rendered YAMLRendered manifestskubectl apply -koverlays/productionreplicas, HPA, hosts, tagconfigMapGeneratorhashed name, rolloutsbase/Deployment, Service, JobOverlays patch the base; nothing is copieddjango-deployment.com
Staging uses the same base with a smaller overlay.

3. Let config changes roll pods

configMapGenerator names the ConfigMap django-config-7g2kfh9m4b, with a hash of its content, and rewrites references in the Deployment. Changing a value changes the name, which changes the pod template, which triggers a rolling update. That fixes the common problem of edited ConfigMaps not reaching running pods. Use secretGenerator only for non-sensitive values or with encrypted files; real secrets belong in External Secrets or SOPS.

4. Migrations per release

A migration Job with a fixed name cannot be re-run, because Jobs are immutable. Options:

  • Add nameSuffix per release in CI, or generate the Job name with the image tag.
  • With Argo CD, use a PreSync hook annotation on the Job so it runs before each sync and is deleted afterwards.

5. Set images from CI and apply

cd deploy/overlays/production
kustomize edit set image shop=ghcr.io/acme/shop:$GIT_SHA
git commit -am "Deploy $GIT_SHA" && git push        # GitOps
# or directly:
kubectl apply -k deploy/overlays/production

Review the rendered output in pull requests with kubectl kustomize deploy/overlays/production | kubectl diff -f -.

two ways to vary manifestsKustomize overlays✓plain YAML, no templates✓built into kubectl✕no packaging or versionsHelm chart✓values file per env✓shareable, versioned✕templating logic to maintainKustomize fits apps you own; Helm fits charts you distributedjango-deployment.com
Many teams install third-party charts with Helm and manage their own app with Kustomize.

Explanation

Kustomize reads a kustomization.yaml, loads resources, applies transformations (namespace, labels, image rewrites, name suffixes), and merges patches by matching kind and name. The output is plain Kubernetes YAML. Because there is no templating, manifests remain valid YAML you can read and lint, and overlays express differences explicitly. kubectl apply -k runs the same engine.

Edge cases / notes

  • Selector labels. Changing labels with includeSelectors: true after the first deploy alters immutable selectors; set them once.
  • Version skew. The kustomize CLI may be newer than kubectl's embedded version; pin one in CI.
  • Components. Kustomize components let optional features (for example a debug sidecar) be added to some overlays only.

Next steps

FAQ

Is Kustomize built into kubectl?

Yes. kubectl apply -k and kubectl kustomize use an embedded version.

How do I change the image tag per release?

Use kustomize edit set image in CI, or the images field in the overlay.

Why does changing a ConfigMap not restart pods?

Plain ConfigMaps do not change the pod template. Use configMapGenerator, which adds a content hash to the name.

Should secrets go in secretGenerator?

Not real secrets in plain files. Use External Secrets Operator or encrypted files with SOPS.

Keep going