Automate HTTPS for Django on Kubernetes With cert-manager

Use cert-manager to issue and renew Let’s Encrypt certificates for a Django app on Kubernetes: install it with Helm, create a ClusterIssuer with HTTP-01 or DNS-01, request certificates through Ingress or Gateway annotations, and troubleshoot stuck challenges.

Problem statement

Your Django app runs on Kubernetes behind an ingress controller or Gateway, and you need HTTPS certificates that renew automatically for every host name: the main site, the API subdomain, and maybe customer domains. Running Certbot by hand does not fit a cluster. cert-manager is the standard Kubernetes controller for this: it requests certificates from Let's Encrypt (or another ACME CA), stores them in Secrets, and renews them before expiry.

cert-manager for DjangoClusterIssuerLet’s Encrypt ACMECertificatehosts, secret nameChallengeHTTP-01 or DNS-01TLS Secretused by Gatewaycert-manager keeps a valid certificate in a Secret that the ingress serves.
  1. Install cert-manager with Helm, including CRDs: helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true.
  2. Create a ClusterIssuer for Let's Encrypt staging and one for production.
  3. Use HTTP-01 when the ingress is publicly reachable on port 80; use DNS-01 for wildcards or private clusters.
  4. Request certificates by annotating the Ingress (cert-manager.io/cluster-issuer: letsencrypt-prod) or Gateway, or with a Certificate resource.
  5. Check kubectl describe certificate,order,challenge when issuance stalls.

Step-by-step solution

1. Install cert-manager

helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
kubectl -n cert-manager get pods

Pin a chart version in production and upgrade deliberately; CRD changes come with major versions.

2. Create issuers

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account
    solvers:
      - http01:
          gatewayHTTPRoute:
            parentRefs:
              - name: public-gateway
                namespace: gateway

Create a matching letsencrypt-staging issuer with the staging directory URL and test with it first; Let's Encrypt production has rate limits. With a classic Ingress controller, the solver is http01: { ingress: { ingressClassName: <class> } } instead.

HTTP-01 compared with DNS-01HTTP-01DNS-01Needs public port 80yesnoWildcard certs!noyesPrivate clusters!noyesExtra setupnoneDNS API credentialsHTTP-01 is simplest; DNS-01 covers wildcards and internal hostsdjango-deployment.com
Cloudflare, Route 53, Google Cloud DNS, and others have built-in DNS-01 solvers.

3. Request a certificate

With Gateway API, reference a TLS Secret on the listener and let cert-manager create it via a Certificate:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: shop-tls
  namespace: gateway
spec:
  secretName: shop-tls
  issuerRef: { name: letsencrypt-prod, kind: ClusterIssuer }
  dnsNames: ["shop.example.com", "api.shop.example.com"]

With an Ingress, add the annotation and a tls block; cert-manager creates the Certificate automatically:

metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: ["shop.example.com"]
      secretName: shop-tls

4. Make Django HTTPS-aware

TLS terminates at the gateway or ingress, which forwards plain HTTP to Django pods. Set:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SECURE_REDIRECT_EXEMPT = [r"^healthz/"]

Exempting the health path keeps Kubernetes probes, which use plain HTTP, from getting redirects.

5. Troubleshoot issuance

kubectl describe certificate shop-tls -n gateway
kubectl get orders,challenges -A
kubectl describe challenge <name> -n gateway

Common causes of stuck challenges: DNS not yet pointing at the load balancer, port 80 blocked, an HTTP-to-HTTPS redirect applied before the challenge path, or the solver configured for the wrong ingress class or Gateway.

issuance flowCertificate createddnsNames listedOrder placedwith Let’s EncryptChallenge servedHTTP-01 or DNS-01CA validatesfrom the internetSecret updatedrenews at 2/3 lifeMost stuck issuances fail at the challenge stepdjango-deployment.com
Watch the Challenge resource events for the exact reason a validation fails.

Explanation

cert-manager adds custom resources (Issuer, ClusterIssuer, Certificate, Order, Challenge) and controllers that run the ACME protocol. For each Certificate, it creates an order, solves the challenges by creating temporary routes or DNS records, stores the issued certificate and key in a Secret, and renews it automatically, by default when two-thirds of the lifetime has passed. Ingress controllers and Gateways read the Secret, so Django never handles certificates.

Edge cases / notes

  • Rate limits. Let's Encrypt limits certificates per registered domain per week; use staging while experimenting.
  • Many customer domains. For per-tenant custom domains, each needs DNS pointing at your cluster and its own certificate; consider on-demand TLS with a proxy like Caddy for large numbers.
  • Secret replication. Gateways in another namespace need the Secret where the listener lives, or a ReferenceGrant.

Next steps

FAQ

Does cert-manager renew certificates automatically?

Yes, before they expire, and updates the Secret the ingress or Gateway uses.

HTTP-01 or DNS-01?

HTTP-01 for public sites without wildcards; DNS-01 for wildcard certificates or private clusters.

Why is my certificate stuck in pending?

Usually the challenge cannot be validated: DNS, port 80, redirects, or solver configuration. Describe the Challenge resource.

Does Django need to know about cert-manager?

No. Django only needs SECURE_PROXY_SSL_HEADER because TLS ends at the ingress.

Keep going