Migrating from NGINX Ingress Controller to Traefik: A Step-by-Step Guide
The Kubernetes community announced the retirement of Ingress NGINX with best-effort maintenance ending March 2026. This guide walks you through a safe, tested migration path to Traefik Proxy—with code examples, annotation mappings, and production tips.
⚠️ Important: The Kubernetes SIG Network and Security Response Committee announced that Ingress NGINX will be retired in March 2026. After this date, there will be no security patches, bug fixes, or new releases. Existing deployments will continue to function, but running unmaintained infrastructure in production carries significant risk.
If you’re using ingress-nginx today, now is the time to plan your migration. In this post, I’ll walk you through migrating to Traefik Proxy—a cloud-native, dynamic ingress controller with automatic service discovery and a clear migration path.
🔗 Sources:
Why Migrate? The Ingress NGINX Retirement Timeline
| Date | Milestone |
|---|---|
| Nov 2025 | Retirement announcement by Kubernetes SIG Network & SRC |
| Mar 2026 | Best-effort maintenance ends; no more releases or security fixes |
| Post-Mar 2026 | Repositories become read-only; artifacts remain available |
✅ Good news: Your existing
ingress-nginxdeployments won’t break. But without security updates, you’ll accumulate technical debt and exposure to newly discovered vulnerabilities.
Why Traefik?
Traefik isn’t just a replacement—it’s a modernization:
- 🚀 Automatic service discovery: Watches Kubernetes API; no manual config reloads
- 🔐 Let’s Encrypt built-in: Automatic TLS provisioning and renewal
- 🧩 Middleware architecture: Chainable components for auth, rate limiting, headers, redirects
- 📊 Native metrics: Prometheus, Datadog, OpenTelemetry—no exporters needed
- 🌐 Full protocol support: HTTP/1.1, HTTP/2, HTTP/3, gRPC, TCP, WebSocket
- 🆓 100% open source (Apache 2.0), with optional enterprise features
Migration Strategy: 4 Phases to Zero Downtime
Phase 1: Assess Your Current Setup
First, confirm you’re using Ingress NGINX:
kubectl get pods --all-namespaces \
--selector app.kubernetes.io/name=ingress-nginx
Then inventory your annotations:
kubectl get ingress -A -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations}{"\n"}{end}' \
| grep nginx.ingress.kubernetes.io
Phase 2: Deploy Traefik in Parallel
# Add the Traefik Helm repo
helm repo add traefik https://traefik.github.io/charts
helm repo update
# Install in a dedicated namespace
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set ingressClass.enabled=true \
--set ingressClass.isDefaultClass=false \
--set ports.web.redirectTo.port=websecure \
--set ports.websecure.tls.enabled=true \
--set providers.kubernetesIngress.enabled=true \
--set providers.kubernetesCRD.enabled=true \
--set replicaCount=2 \
--set logs.level=INFO
Verify Installation
# Check pods and service
kubectl get pods -n traefik
kubectl get svc -n traefik
# Confirm the IngressClass exists
kubectl get ingressclass traefik -o yaml
Phase 3: Migrate Ingress Resources (With Examples)
Before (NGINX):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- my-app.tld
secretName: my-app-tls
rules:
- host: example.com
http:
paths:
- path: /app
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
After (Traefik):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
# Path rewrite via Middleware (see advanced section)
spec:
tls:
- hosts:
- my-app.tld
secretName: my-app-tls
rules:
- host: example.com
http:
paths:
- path: /app
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
The Zero-Modification Approach: Traefik’s NGINX Provider
Here’s the best part: you don’t need to modify existing Ingress resources immediately. Traefik v3.6.2+ includes an Ingress NGINX Provider that understands NGINX annotations and translates them automatically.
# Install Traefik with NGINX compatibility layer
helm upgrade --install traefik traefik/traefik \
--namespace traefik --create-namespace \
--set providers.kubernetesIngressNginx.enabled=true
Or using a values file:
# traefik-values.yaml
providers:
kubernetesIngressNginx:
enabled: true
helm upgrade --install traefik traefik/traefik \
--namespace traefik --create-namespace \
--values traefik-values.yaml
Your existing Ingress resources work unchanged:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://example.com"
spec:
ingressClassName: nginx # Traefik watches this class
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80
Annotation Migration Reference
| NGINX Annotation | Traefik Equivalent | Notes |
|---|---|---|
nginx.ingress.kubernetes.io/ssl-redirect: "true" |
traefik.ingress.kubernetes.io/router.entrypoints: websecure |
Or use NGINX provider |
nginx.ingress.kubernetes.io/rewrite-target: /path |
Middleware StripPrefix |
Requires Middleware CRD |
nginx.ingress.kubernetes.io/enable-cors: "true" |
Middleware Headers + CORS |
Chainable middleware |
nginx.ingress.kubernetes.io/cors-allow-origin: "*" |
Middleware Headers |
See CORS middleware docs |
nginx.ingress.kubernetes.io/proxy-body-size: "10m" |
serversTransport.maxBodySize |
Global setting |
nginx.ingress.kubernetes.io/rate-limit: "100" |
Middleware RateLimit |
Token bucket algorithm |
nginx.ingress.kubernetes.io/auth-url |
Middleware ForwardAuth |
Direct equivalent |
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" |
Service scheme: https |
In ServiceSpec |
Phase 4: Traffic Migration Strategies
Option A: DNS-Based Migration (Simple)
- Add Traefik to DNS - Add Traefik’s LoadBalancer IP to your DNS records alongside NGINX (round-robin)
- Monitor traffic - Watch logs on both controllers to verify traffic distribution
- Remove NGINX from DNS - Once validated, update DNS to point only to Traefik
- Wait for propagation - Allow 24-48 hours for DNS caches to expire
- Uninstall NGINX - Safe to remove after propagation window
Option B: Weighted Traffic Shift (Production-Ready)
Use an external load balancer (Cloudflare, AWS ALB, HAProxy) in front of both controllers:
Cloudflare / AWS ALB / HAProxy] -->|10% | B[NGINX] A -->|90% | C[Traefik]
Gradual weight shift:
| Stage | NGINX | Traefik | Duration |
|---|---|---|---|
| 1 | 100% | 0% | Baseline |
| 2 | 90% | 10% | 24 hours |
| 3 | 50% | 50% | 24 hours |
| 4 | 10% | 90% | 24 hours |
| 5 | 0% | 100% | Final |
Phase 5: Decommission NGINX
Step 1: Preserve the IngressClass
Before uninstalling, preserve the nginx IngressClass to avoid breaking existing resources:
helm upgrade ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx \
--reuse-values \
--set-json 'controller.ingressClassResource.annotations={"helm.sh/resource-policy": "keep"}'
Step 2: Delete Admission Webhooks
kubectl delete -A ValidatingWebhookConfiguration ingress-nginx-admission
kubectl delete -A MutatingWebhookConfiguration ingress-nginx-admission
Step 3: Uninstall NGINX
helm uninstall ingress-nginx --namespace ingress-nginx
kubectl delete namespace ingress-nginx
Step 4: Transfer LoadBalancer IP (Optional)
If you need to preserve your external IP, configure Traefik to claim it:
# traefik-values.yaml (AWS NLB example)
service:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-xxx,eipalloc-yyy"
Advanced: Traefik Middleware Examples
Rate Limiting
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: default
spec:
rateLimit:
average: 100
burst: 50
CORS Headers
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: cors-headers
namespace: default
spec:
headers:
accessControlAllowMethods:
- GET
- POST
- OPTIONS
accessControlAllowHeaders:
- "*"
accessControlAllowOriginList:
- "https://example.com"
accessControlMaxAge: 100
Path Rewrite (replacement for rewrite-target)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-app-prefix
namespace: default
spec:
stripPrefix:
prefixes:
- /app
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
traefik.ingress.kubernetes.io/router.middlewares: default-strip-app-prefix@kubernetescrd
spec:
# ... rest of ingress
Verification & Monitoring
Check Both Controllers
kubectl get pods -n ingress-nginx
kubectl get pods -n traefik
Get LoadBalancer IPs
kubectl get svc -n ingress-nginx ingress-nginx-controller
kubectl get svc -n traefik traefik
Test Traffic via Traefik
# Get Traefik's external IP
TRAEFIK_IP=$(kubectl get svc -n traefik traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
# Test with curl (bypass DNS)
curl --connect-to myapp.example.com:443:${TRAEFIK_IP} https://myapp.example.com
Check Traefik Logs for Ingress Discovery
kubectl logs -n traefik deployment/traefik | grep -i "ingress"
Backup Before Migration
# Export all Ingress resources
kubectl get ingress --all-namespaces -o yaml > ingress-backup.yaml
# Export NGINX ConfigMaps
kubectl get configmap --all-namespaces \
-l app.kubernetes.io/name=ingress-nginx -o yaml > nginx-configmaps.yaml
Key Differences: NGINX vs Traefik
| Aspect | NGINX Ingress | Traefik |
|---|---|---|
| Configuration | ConfigMaps, annotations | Helm values, CRDs, Gateway API |
| Service Discovery | Requires reload | Automatic (watches Kubernetes API) |
| TLS/Let’s Encrypt | HTTP challenge (needs public exposure) | DNS challenge supported |
| Metrics | Requires prometheus-exporter sidecar | Built-in Prometheus, Datadog, OTLP |
| Dashboard | External addon required | Built-in web UI (--api.dashboard=true) |
| TCP/UDP Routing | Limited (ConfigMap-based) | Native via IngressRouteTCP/UDP CRDs |
| Hot Reloading | No (requires pod restart) | Yes (zero-downtime config updates) |
Troubleshooting
Traefik Not Picking Up Ingress
# Verify IngressClass annotation
kubectl get ingress <name> -o jsonpath='{.spec.ingressClassName}'
# Check Traefik logs
kubectl logs -n traefik deployment/traefik --tail=100
# Verify RBAC
kubectl get clusterrole traefik -o yaml | grep -A5 ingress
TLS Certificate Issues
# Check certificate secret exists
kubectl get secret <tls-secret> -o yaml
# Verify Traefik TLSStore
kubectl get tlsstore -A
502 Bad Gateway
# Verify backend service endpoints
kubectl get endpoints <service-name>
# Check Traefik logs for upstream errors
kubectl logs -n traefik deployment/traefik | grep -i "upstream\|502"
Conclusion
The retirement of Ingress NGINX is a significant moment for the Kubernetes community, but it’s also an opportunity to modernize your ingress layer. Traefik offers:
✅ Zero-downtime migration with the NGINX compatibility provider
✅ No immediate code changes to existing Ingress resources
✅ Modern architecture with automatic service discovery
✅ Built-in features that previously required add-ons
Migration checklist:
- Inventory existing Ingress resources and annotations
- Deploy Traefik with NGINX provider enabled
- Test traffic routing through Traefik
- Implement gradual traffic shift (DNS or external LB)
- Monitor for errors during migration window
- Decommission NGINX after validation
- (Optional) Migrate to native Traefik CRDs for advanced features
Start your migration today—don’t wait until March 2026 when security updates end.