Skip to main content

Migrating a Production Cluster Off ingress-nginx to Traefik v3

· 8 min read

Short answer: Traefik v3.7's kubernetesIngressNGINX provider reads Ingresses on the ingress-nginx class and translates its annotations, so the cutover itself is a load balancer IP handover — no annotation rewrite across your Ingresses, no dual-IngressClass trick. What it does not cover is the ingress-nginx controller configuration: TLS redirect, global error pages, the default certificate, auth annotations after you switch class. Those you port by hand.

We moved a production cluster on 23 August 2026: many Ingresses, multi-tenant, customer apps. Here is what we did and what it cost.


Why we moved, and why we waited

ingress-nginx is retired upstream and has not received patches since March 2026. Running an unpatched component in the request path of every customer app is not a position you can hold indefinitely, so the move was going to happen.

We waited about six months anyway. Being early on a migration like this means finding the bugs yourself and writing the guide instead of reading it. There was no upside to going first: ingress-nginx kept working the whole time, and every month of waiting meant more people had hit the sharp edges before us and written them down. That trade only stops making sense once the security exposure outweighs it.


Why Traefik

Our general rule is to stay close to Kubernetes best practice rather than pick something clever. If upstream converges fully on Gateway API, we want to follow without another migration.

That is most of the reasoning:

  • Mature. Long track record as an ingress controller, not a recent entrant.
  • Supports both Ingress and Gateway API. We run Ingress today. The option to move to Gateway API later without changing controllers again is the point.

The ingress-nginx compatibility provider was a bonus we found while planning, not the reason for the choice. It did make the cutover much cheaper than it would otherwise have been.


How the cutover worked

Traefik v3.7 added providers.kubernetesIngressNGINX. It serves Ingresses whose IngressClass object names the ingress-nginx controller, and translates the nginx.ingress.kubernetes.io/* annotations itself:

providers:
kubernetesIngressNGINX:
enabled: true
controllerClass: "k8s.io/ingress-nginx"
ingressClass: "nginx"
defaultBackendService: "traefik/default-backend"
publishService:
enabled: true

With that enabled, every existing Ingress — including the ones our operator generates for customer apps and cert-manager's HTTP-01 solver Ingresses — needed no edit. Deploy Traefik alongside ingress-nginx, then move the MetalLB load balancer IP from one Service to the other. That is the cutover.

It does not overlap with the normal kubernetesIngress provider: that one matches only IngressClass objects naming Traefik's controller, this one only k8s.io/ingress-nginx. We left it enabled after the migration. Turning it off means editing every Ingress in the cluster to buy nothing, and it keeps a rollback cheap.


What we ported by hand

The compatibility provider covers Ingress annotations. It does not cover what was configured on the ingress-nginx controller itself, and it stops applying to any Ingress the moment you switch that Ingress to ingressClassName: traefik. Four things needed doing.

Basic auth on platform endpoints

Our internal services were protected with:

nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: internal-basic-auth

Traefik's native provider does not implement these annotations. It does not error and it does not fall back — the router is created without authentication. So the Middleware has to exist before the class flips, not after:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: internal-basic-auth
namespace: internal
spec:
basicAuth:
secret: internal-basic-auth-traefik
realm: "Authentication Required"

referenced from the Ingress:

annotations:
traefik.ingress.kubernetes.io/router.middlewares: "internal-internal-basic-auth@kubernetescrd"

Two details. Traefik reads the key users, ingress-nginx reads auth. And Traefik's BasicAuth rejects a Secret with more than one key (must be single element exactly), so you cannot just add a second key to the existing Secret — we created a separate -traefik Secret and left the original intact for rollback.

Global error pages

ingress-nginx has custom-http-errors in the controller ConfigMap. Traefik has no global equivalent: errors are a Middleware, and the compatibility provider only honours the per-Ingress annotation. We serve a branded page for stopped, broken or suspended apps, and we are not going to add an annotation to every Ingress the operator generates, so the Middleware hangs off the entrypoint instead:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: errorpage
namespace: traefik
spec:
errors:
status:
- "503"
service:
name: default-backend
port: 80
query: "/"
ports:
web:
http:
middlewares:
- traefik-errorpage@kubernetescrd
websecure:
http:
middlewares:
- traefik-errorpage@kubernetescrd

This needs providers.kubernetesCRD.allowEmptyServices: true. Without it, a Service with no endpoints returns 404 rather than 503, and the Middleware never fires.

Unknown hostnames are a separate mechanism: defaultBackendService on the compatibility provider, replacing ingress-nginx's defaultBackend.

The default certificate

ingress-nginx serves a fallback certificate via --default-ssl-certificate. We rely on it: the operator leaves the built-in app domains out of the Ingress TLS block on purpose, because a wildcard covers them.

In Traefik this is a TLSStore named default, and the Secret must live in Traefik's namespace:

tlsStore:
default:
defaultCertificate:
secretName: wildcard-tls

Miss it and those hostnames are served Traefik's own self-signed certificate. Nothing in the control plane reports an error; the failure is a browser warning.

Metrics and dashboards

Metric names change completely: nginx_ingress_controller_* becomes traefik_service_* and traefik_entrypoint_*. Traefik's ServiceMonitor relabels its own service label to exported_service, so panels grouping by service have to group on that instead. Our networking dashboard was empty until it was rewritten — worth knowing in advance so nobody reads blank panels as an outage.

One setting we deliberately did not port: proxy-body-size: 50m. Traefik does not limit request body size by default.


What went wrong

ingress-nginx redirects HTTP to HTTPS by default. Traefik does not, and nothing in the cutover carried that behaviour across. Every app in the cluster answered on port 80 in cleartext. Nothing was down, no health check failed, and traffic on 443 looked normal — plain HTTP was simply being served instead of redirected.

Our monitoring caught it, because it checks for the redirect. The fix belongs at the entrypoint rather than per Ingress:

ports:
web:
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true

It does not break certificate issuance — both Let's Encrypt and cert-manager 1.21 follow the redirect during HTTP-01 without validating the certificate at the target.

The general lesson is not about Traefik. Controller-level defaults are invisible in your manifests, so a migration diff of your Ingresses shows nothing missing. What ingress-nginx did for you by default is exactly what nobody wrote down.


Uninstalling ingress-nginx

Do it through Helm, before removing the namespace.

Its admission webhook is cluster-scoped with failurePolicy: Fail on every Ingress CREATE and UPDATE. If you delete the namespace and orphan the webhook, every Ingress and certificate change in the cluster stops working, cert-manager's included. The chart removes the webhook in the right order. kubectl delete ns does not.


Checklist

Before switching any IngressClass to traefik:

  1. Auth annotations on that Ingress → Middleware created first, same namespace, single-key users Secret.
  2. TLS redirect → entrypoint redirection configured.
  3. Custom error pages → entrypoint Middleware plus allowEmptyServices: true.
  4. Default certificate → TLSStore/default, Secret in Traefik's namespace.
  5. Dashboards and alerts on nginx_ingress_controller_* → rewritten.
  6. Body size and timeout tuning from the nginx ConfigMap → re-applied if you need it.

After the cutover, check the redirect and the auth explicitly rather than trusting a green rollout:

curl -sI http://app.example.com/ | head -1                       # expect 308
curl -o /dev/null -w '%{http_code}\n' https://internal.example.com/ # expect 401

And confirm the certificate served on a domain that has no TLS block of its own is your wildcard, not the controller's self-signed default.


FAQ

Is ingress-nginx dead? Retired upstream, unpatched since March 2026. It keeps running; security fixes are not coming.

Do I have to rewrite my nginx annotations to move to Traefik? No. Traefik v3.7+ ships providers.kubernetesIngressNGINX, which serves Ingresses on the ingress-nginx class and translates its annotations. Controller-level configuration is not covered.

What breaks when I switch an Ingress from class nginx to class traefik? Anything relying on an annotation the native Traefik provider does not implement. Auth annotations are the one to check first: the endpoint is served without authentication rather than erroring.

Does an HTTP-to-HTTPS redirect break Let's Encrypt HTTP-01? No. Let's Encrypt and cert-manager follow the redirect and do not validate the certificate at the target.

How do I uninstall ingress-nginx safely? Through Helm, before deleting the namespace. Its cluster-scoped admission webhook has failurePolicy: Fail, so orphaning it blocks every Ingress change in the cluster.


If you would rather not run any of this yourself, that is the point of Hostim — you push a container, we run the ingress, the certificates and the cluster underneath. EU-hosted, flat monthly price.

👉 Deploy an app on Hostim, free 5-day trial, no card

We're building Hostim.dev to make this simpler, and we are happy to answer any questions.

Last updated: 31 August 2026. Versions: Traefik chart 41.3.0 (v3.7.11), cert-manager 1.21. Configuration snippets are from our production cluster, trimmed for readability. Check the Traefik documentation for the current list of translated ingress-nginx annotations before planning your own cutover.