# Caddy Ingress Controller: watching Ingress resources and issuing certificates

> caddyserver/ingress is a work in progress ingress controller that runs Caddy 2 inside a Kubernetes cluster, turns Ingress objects into routes, and handles certificates through a Helm chart. The project openly labels itself unfinished, and the release history shows a long gap between published chart versions.

**caddyserver/ingress** — WIP Caddy 2 ingress controller for Kubernetes

- Repository: https://github.com/caddyserver/ingress
- Stars: 825 · Forks: 91
- Language: Go
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/caddyserver-ingress

## A controller whose own description starts with WIP

The repository describes itself as a WIP Caddy 2 ingress controller for Kubernetes, and that disclaimer matters for anyone sizing up adoption. The scope it claims is narrow and specific: monitoring Ingress resources on a cluster and providing automatic HTTPS certificates for the hostnames defined in the ingress resources it manages. That is the entire contract, and it is narrower than what a general purpose ingress controller offers.

The implementation is Go. The module path is github.com/caddyserver/ingress, the build target lives under cmd/caddy, and the container entrypoint is the binary named ingress-controller. The Dockerfile exposes ports 80 and 443, which lines up with a proxy that terminates TLS and forwards plain HTTP upstream. A Makefile target builds a static linux binary with CGO disabled, and a separate target drives a skaffold dev loop with port forwarding for local work.

Because the project is Go and Kubernetes native, the audience is operators who already run Caddy elsewhere and want the same TLS behavior inside a cluster. It is not aimed at someone choosing an ingress controller from scratch, and the wording of the documentation never claims to be a replacement for a controller with a longer production record.

## The Helm chart is the only install path the project spells out

Installation is documented in three steps, and the middle one is the Helm chart that ships in the charts folder of the repository. The stated prerequisites are Helm 3 or newer and Kubernetes 1.19 or newer. Everything starts by isolating the controller in its own namespace.

```sh
kubectl create namespace caddy-system
```

With the namespace in place, the chart is pulled from a dedicated chart repository published under the project GitHub Pages domain, and the install is run in atomic mode so a failure rolls back rather than leaving a half applied release.

```sh
helm install \
  --namespace=caddy-system \
  --repo https://caddyserver.github.io/ingress/ \
  --atomic \
  mycaddy \
  caddy-ingress-controller
```

The release name mycaddy is arbitrary and appears again later when a generated manifest is used. If the install succeeds, the chart creates a service of type LoadBalancer in the caddy-system namespace, and the external address of that service is what DNS records for your cluster must point at.

## Rendering the chart to YAML and installing through Glasskube

Two alternate routes are documented alongside the Helm install. The first avoids the chart repository entirely by cloning the source and rendering the chart locally into a manifest file, then applying that file to the cluster. This is the path to take when you want to read the exact resources that will land in the cluster before anything is applied, or when you need to feed the output into a GitOps flow.

```sh
git clone https://github.com/caddyserver/ingress.git
cd ingress

# generate the yaml file
helm template mycaddy ./charts/caddy-ingress-controller \
  --namespace=caddy-system \
  > mycaddy.yaml

# apply the file
kubectl apply -f mycaddy.yaml
```

The second route uses Glasskube, where the controller appears in the ClusterPackages tab of the graphical interface and can be installed from there. The same package is available on the command line.

```console
glasskube install caddy-ingress-controller
```

Automatic HTTPS needs an email address, and the Glasskube path takes it as a package value rather than as a Helm set expression.

```
glasskube install caddy-ingress-controller --value "automaticHTTPS=your@email.com"
```

## Automatic HTTPS and On-Demand TLS are separate switches

Certificate handling is where the chart configuration gets interesting, and the project keeps two mechanisms deliberately separate. The documentation draws the distinction explicitly, saying that automatic HTTPS should not be confused with On-Demand TLS.

Automatic HTTPS is the default behavior once an email address is present, and the address goes into the controller configuration in the config map. With the Helm chart the value is set during install.

```sh
helm install ...\
  --set ingressController.config.email=your@email.com
```

On-Demand TLS is the Caddy feature that issues certificates on the fly for hostnames it has not seen before, and the controller exposes it by setting the onDemandTLS configuration to true. The documentation also points to a second option key, onDemandAsk, for the on-demand configuration. The details of what onDemandAsk accepts are not spelled out in the README, so an operator should expect to read the Caddy documentation for that key rather than guess.

For a team with a fixed set of public hostnames, the plain email based automatic HTTPS is the path to take. The on-demand mode is worth considering for shared or bursty hostnames, at the cost of accepting whatever the upstream Caddy defaults imply for abuse handling.

## Your own certificate means a TLS secret in the ingress spec

The escape hatch for a single host is documented with a complete example. The workflow is to create a Kubernetes TLS secret from a key and certificate pair that already cover the host, then reference that secret from the ingress resource. Automatic HTTPS is bypassed for that host, and the rest of the cluster keeps the default behavior.

```sh
kubectl create secret tls mycerts --key ./tls.key --cert ./tls.crt
```

The ingress object then carries the secret name under the tls section and lists the host it applies to.

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example
  annotations:
    kubernetes.io/ingress.class: caddy
spec:
  rules:
  - host: test.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: test
            port:
              number: 8080
  tls:
    - secretName: mycerts # use mycerts for host test.com
      hosts:
        - test.com
```

The annotation kubernetes.io/ingress.class with the value caddy is how the object is claimed by this controller rather than by another ingress implementation in the same cluster. The example uses a Prefix path type and a single backend service on port 8080, which is a plain service to service proxy setup with no rewrite or redirect rule shown.

## DNS is the handoff point and it is on you

The controller does not manage DNS. The installation notes are explicit that you want to set any DNS records for accessing the cluster to the external IP address of the LoadBalancer service, and that this applies when the external IP is provisioned by your cloud provider. On a local cluster or a setup without an external address, that step simply has nothing to point at yet, and routing stays unfinished until an address exists.

The address is read from the service in the caddy-system namespace.

```sh
kubectl get svc -n caddy-system
```

Debugging follows the same narrow path. Pod names come from the namespace listing, and the logs of both Caddy and the controller are read from the pod log stream.

```sh
kubectl get pods -n caddy-system
```

```sh
kubectl logs <pod-name> -n caddy-system
```

There is no documented flag for a static host, no documented way to front the controller with your own load balancer, and no mention of a proxy protocol option in the installation notes, even though the go.mod dependency list includes a proxy protocol library. Treat the LoadBalancer service plus DNS records as the only supported topology.

## What the dependency list says about the mechanism

The go.mod file gives a fair view of how the controller is put together without reading the source. The controller builds on Caddy itself, pinned at github.com/caddyserver/caddy/v2 v2.11.4, and pulls in certmagic v0.25.4, which is the library Caddy uses for certificate issuance and storage. Those two dependencies line up exactly with the two documented features: Caddy serves the traffic, certmagic obtains and renews the certificates.

The Kubernetes side uses the standard client libraries, with k8s.io/api, k8s.io/apimachinery and k8s.io/client-go all at v0.36.2, so the controller talks to the API server through client-go rather than through a hand written API client. The module also requires google/uuid for identifier generation, mitchellh/mapstructure for decoding configuration structures, pires/go-proxyproto for proxy protocol handling, zap for structured logging, and a connection pool from go-playground.

The top level directory layout matches that split. The cmd folder holds the entrypoint, internal and pkg hold the controller logic, kubernetes holds cluster facing assets, charts holds the Helm chart that the installation steps reference, and the skaffold and ct configuration files support local development and chart testing.

## Release gaps and the unfinished label are the real cost

Maintenance is the part that deserves the closest attention. The project is not archived, and the last push was on 2026-09-17, so work is landing in the repository. The published releases tell a slower story. The chart release caddy-ingress-controller-1.3.0 is dated 2025-01-05, the 1.2.0 chart release is dated 2024-11-15, and the earlier v0.2.1 tag is dated 2023-11-20. The most recent published chart release therefore predates the most recent code push by several months, which is what you would expect from a project that pushes work without cutting a new chart on every change.

The README does not document a rollback procedure, an upgrade path from one chart version to the next, leader election, replica counts, or a high availability story. It also does not list which Ingress annotations or Gateway API features are honored, so the supported surface of an ingress object is narrower than a mature controller's. The license is Apache License 2.0, and the source includes a LICENSE.txt file plus a CONTRIBUTING.md that points contributors at a contributing guideline.

Taken together, the picture is of an actively edited but pre-stabilized controller. The code moves, the chart versions lag, and the operational documentation stops at the happy path of installing, pointing DNS, and reading pod logs.

## Conclusion

Adopt the Caddy Ingress Controller if your cluster already runs Caddy 2, your platform is Kubernetes 1.19 or newer, and you want certificates issued automatically for the hostnames your Ingress objects declare. Skip it if you need a graduated controller with a documented high availability story, a rollback procedure, or a stable release cadence. Before deploying, verify two things: that the chart value ingressController.config.email receives a real address, and that a DNS record for each host resolves to the external IP that kubectl get svc -n caddy-system reports for the caddy-system LoadBalancer service.

## FAQ

### What does the Caddy Ingress Controller actually do?

It monitors Ingress resources on a Kubernetes cluster and provides automatic HTTPS certificates for the hostnames defined in the ingress resources it manages. The traffic is served by Caddy 2, which the controller embeds rather than proxying to an external one.

### How do I install the Caddy Ingress Controller with Helm?

You need Helm 3 or newer and Kubernetes 1.19 or newer, and you create a caddy-system namespace first. The chart then installs from the repository at https://caddyserver.github.io/ingress/ with the release name mycaddy, and it creates a service of type LoadBalancer in that namespace.

### How do I enable automatic HTTPS for the Caddy Ingress Controller?

Automatic HTTPS is driven by an email address, which you set in the config map, for example with the Helm value ingressController.config.email=your@email.com. On-Demand TLS is a different mechanism and is switched on separately with the onDemandTLS configuration set to true.

### Can I use my own certificate instead of an automatically issued one?

Yes, for a specific host. Create a TLS secret with kubectl create secret tls and point the ingress tls section at that secret name, and automatic HTTPS is not used for that host while other hosts in the cluster keep the default behavior.

### How do I find the address to point my DNS records at?

The chart creates a service of type LoadBalancer in the caddy-system namespace, and you read its external IP with kubectl get svc -n caddy-system. The external address exists only when your cloud provider provisions one, so the DNS step waits on that.

## Sources

- [caddyserver/ingress on GitHub](https://github.com/caddyserver/ingress)
- [Issues](https://github.com/caddyserver/ingress/issues)
- [License: Apache-2.0](https://github.com/caddyserver/ingress/blob/main/LICENSE)
- [README](https://github.com/caddyserver/ingress/blob/main/README.md)
- [Releases](https://github.com/caddyserver/ingress/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/caddyserver-ingress
