Caddy can be a surprisingly powerful Kubernetes ingress controller, but its real strength isn’t just routing traffic; it’s its ability to dynamically manage TLS certificates for every single service you expose, often without you ever touching them.

Let’s see Caddy in action. Imagine we have a simple deployment running a basic web app:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app-container
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80

And a corresponding Service to expose it internally within the cluster:

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

Now, to make this accessible from the outside world via Caddy as an ingress, we’ll deploy Caddy itself as a Deployment and Service, and then create a Caddyfile that tells it how to route traffic. This Caddyfile is usually mounted into the Caddy pods via a ConfigMap.

Here’s a sample Caddyfile configuration, which we’ll store in a ConfigMap named caddyfile:

apiVersion: v1
kind: ConfigMap
metadata:
  name: caddyfile
data:
  Caddyfile: |
    my.example.com {
      reverse_proxy my-app-service:80
    }

And here’s the Deployment and Service for Caddy:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: caddy-ingress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: caddy-ingress
  template:
    metadata:
      labels:
        app: caddy-ingress
    spec:
      containers:
      - name: caddy
        image: caddy:latest
        ports:
        - containerPort: 80
        - containerPort: 443
        volumeMounts:
        - name: caddyfile-volume
          mountPath: /etc/caddy
          readOnly: true
      volumes:
      - name: caddyfile-volume
        configMap:
          name: caddyfile
---
apiVersion: v1
kind: Service
metadata:
  name: caddy-ingress-service
spec:
  selector:
    app: caddy-ingress
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      name: http
    - protocol: TCP
      port: 443
      targetPort: 443
      name: https
  type: LoadBalancer # Or NodePort, depending on your cluster setup

When Caddy starts, it reads this Caddyfile. If my.example.com is accessed over HTTP, Caddy will automatically provision a TLS certificate for my.example.com using Let’s Encrypt (or ZeroSSL if Let’s Encrypt is unavailable) and then proxy the request to my-app-service on port 80. Subsequent requests to my.example.com will use the cached certificate.

The core problem Caddy solves here is the operational burden of TLS certificate management. In a traditional Kubernetes setup, you might use cert-manager to automate this, which is a separate, complex system. Caddy, with its built-in ACME client, handles this automatically. When you add a new domain to your Caddyfile that points to a service, Caddy will attempt to get a certificate for it. This is particularly powerful because it means your application developers don’t need to worry about certificate renewal or provisioning; Caddy handles it all transparently.

Internally, Caddy’s reverse_proxy directive is highly configurable. It can handle health checks, load balancing across multiple backend pods for a service, and even HTTP/2 or gRPC proxying. The key is that Caddy watches its Caddyfile (or receives configuration updates via its API) and dynamically reconfigures its internal server. When it sees a new domain, it triggers the ACME challenge flow. Once successful, it stores the certificate locally (typically in /data inside the container, which should be a persistent volume if you want certificates to survive pod restarts) and serves it.

The most surprising thing about Caddy’s TLS management is how it works with multiple domains and wildcards without explicit configuration for each individual certificate. You simply list the domains in your Caddyfile, and Caddy knows to obtain and manage certificates for them. For example, *.mycompany.com in your Caddyfile will result in Caddy obtaining a wildcard certificate that covers all subdomains, and it will do so automatically. This is a significant departure from systems where you often have to explicitly map domain names to certificate resources.

The primary lever you control is the Caddyfile itself, which can be updated directly or, more robustly, via Caddy’s administrative API. This API allows you to dynamically add, remove, or modify routes and entire Caddy configurations without restarting the Caddy process, making it a truly dynamic ingress controller.

The next step to explore is how to leverage Caddy’s administrative API for dynamic configuration updates rather than relying solely on ConfigMap reloads.

Want structured learning?

Take the full Caddy course →