Kubernetes Ingress Controllers Compared: nginx vs Traefik vs Azure Application Gateway
Executive Snapshot
| Category | NGINX Ingress Controller | Traefik | Azure Application Gateway Ingress Controller (AGIC) |
|---|---|---|---|
| Best For | General-purpose, high-traffic workloads | Cloud-native, dynamic microservices | Azure-native AKS workloads needing WAF |
| Configuration Model | Annotations + ConfigMaps | CRDs + Labels + TOML/YAML | ARM/Bicep + Kubernetes Annotations |
| Auto Service Discovery | Manual reload required | Native, zero-downtime | Via Azure Resource Manager |
| TLS Termination | Yes (cert-manager integration) | Yes (built-in ACME/Let's Encrypt) | Yes (Azure Key Vault integration) |
| WAF Support | Via ModSecurity module | Via Traefik Enterprise | Native (Azure WAF Policy) |
| Observability | Prometheus metrics, access logs | Built-in dashboard + Prometheus | Azure Monitor + App Gateway metrics |
| Pricing Model | Free (OSS) / NGINX Plus (commercial) | Free (OSS) / Enterprise | Pay-per-hour (Azure resource cost) |
| Horizontal Scalability | Excellent | Excellent | Managed by Azure (limited control) |
| Multi-cluster Support | With additional tooling | Native (Traefik Enterprise) | Azure-native via Traffic Manager |
| Learning Curve | Moderate | Low–Moderate | Moderate–High (Azure knowledge required) |
TL;DR
- NGINX Ingress Controller is the battle-tested default — ideal for teams that need maximum configurability, high throughput, and broad community support, but it requires manual ConfigMap tuning and reload cycles.
- Traefik wins on developer experience and dynamic service discovery, making it the strongest choice for rapidly-changing microservices environments with built-in Let's Encrypt automation.
- Azure Application Gateway Ingress Controller (AGIC) is the right call for AKS-first shops that need enterprise WAF, SSL offloading via Key Vault, and deep Azure PaaS integration — but you pay for the gateway resource hourly.
- No single controller is universally best — your choice should be driven by traffic patterns, team Azure/Linux expertise, WAF requirements, and cost tolerance.
- All three controllers support the standard Kubernetes
Ingressresource, but their full power is unlocked only via controller-specific CRDs or annotations.
Introduction
Every production Kubernetes cluster eventually needs a front door — a layer-7 traffic router that can terminate TLS, route by hostname and path, enforce rate limits, and expose dozens of services through a single load balancer IP. That front door is your Ingress controller, and your choice of controller will shape your cluster's reliability, security posture, and operational complexity for years.
flowchart TD Client["Client"] --> LB["Load balancer"] LB --> Ingress["Ingress controller"] Ingress -->|host / path rule| SvcA["Service A"] Ingress -->|host / path rule| SvcB["Service B"] SvcA --> PodA["Pods"] SvcB --> PodB["Pods"]
The market has consolidated around three dominant players for most enterprise environments: NGINX Ingress Controller (the Kubernetes community's long-standing workhorse), Traefik (the cloud-native challenger built for dynamic environments), and Azure Application Gateway Ingress Controller (Microsoft's managed gateway deeply wired into AKS). Each reflects a fundamentally different philosophy about where routing intelligence should live, how configuration should be expressed, and who should own the operational burden.
This kubernetes ingress controller comparison cuts through the marketing noise. We'll install all three, configure identical routing scenarios, stress-test their operational characteristics, and give you a clear decision framework — backed by real commands you can run today.
Prerequisites
- A running Kubernetes cluster (v1.27+) — AKS, EKS, GKE, or local (kind/k3s) for NGINX and Traefik sections
- An AKS cluster (v1.27+) with a dedicated node pool for the AGIC section
kubectlconfigured and pointing at your target clusterhelmv3.12+ installed locally- Azure CLI (
az) v2.50+ for the AGIC section - A domain name with DNS you can control (for TLS examples)
- Basic familiarity with Kubernetes
Ingress,Service, andDeploymentresources curlandk6orwrkfor load testing (optional but recommended)
1. The Kubernetes Ingress Landscape in 2026
Before diving into the kubernetes ingress controller comparison, it's worth understanding what you're actually comparing. The Kubernetes Ingress API (networking.k8s.io/v1) defines the intent — "route traffic for api.example.com/v2 to the api-service on port 8080" — but it deliberately says nothing about implementation. That gap is where Ingress controllers live.
The critical nuance: the standard Ingress spec is intentionally limited. Features like circuit breaking, sticky sessions, header manipulation, gRPC routing, and middleware chains are all controller-specific extensions, expressed through:
- Annotations (all controllers, but brittle at scale)
- Custom Resource Definitions (CRDs) — NGINX's
VirtualServer, Traefik'sIngressRoute, AGIC's integration withAzureApplicationGateway - Helm values that configure global controller behavior
The Gateway API (gateway.networking.k8s.io), introduced as GA in Kubernetes 1.28, is the long-term successor to Ingress. All three controllers now have Gateway API support at varying maturity levels — we'll cover this at the end.
2. NGINX Ingress Controller
2.1 Architecture Overview
The NGINX Ingress Controller (maintained by the Kubernetes community at kubernetes/ingress-nginx, distinct from NGINX Inc.'s commercial nginx/kubernetes-ingress) runs as a Deployment that watches the Kubernetes API for Ingress resources and dynamically rewrites an nginx.conf file, reloading NGINX worker processes when configuration changes. This reload cycle is the controller's most discussed limitation — more on that in the pitfalls section.
[Internet] → [LoadBalancer Service] → [NGINX Pod(s)] → [Backend Services]
↑
[nginx.conf regenerated on Ingress change]
2.2 Installation via Helm
# Add the ingress-nginx Helm repo
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
# Install with production-grade settings
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.replicaCount=3 \
--set controller.metrics.enabled=true \
--set controller.metrics.serviceMonitor.enabled=true \
--set controller.podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution[0].topologyKey=kubernetes.io/hostname \
--set controller.resources.requests.cpu=200m \
--set controller.resources.requests.memory=256Mi \
--set controller.resources.limits.cpu=1000m \
--set controller.resources.limits.memory=512Mi \
--version 4.10.1
# Verify rollout
kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx
kubectl get svc -n ingress-nginx
2.3 Basic Ingress with TLS
# nginx-ingress-example.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
annotations:
kubernetes.io/ingress.class: "nginx"
# Rate limiting: 100 req/min per IP
nginx.ingress.kubernetes.io/limit-rps: "100"
# Enable CORS
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.example.com"
# Upstream keepalive
nginx.ingress.kubernetes.io/upstream-keepalive-connections: "32"
# Custom timeouts
nginx.ingress.kubernetes.io/proxy-connect-timeout: "10"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
# Force HTTPS redirect
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls-secret
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-v1-service
port:
number: 8080
- path: /v2
pathType: Prefix
backend:
service:
name: api-v2-service
port:
number: 8080
2.4 Advanced: VirtualServer CRD for Canary Deployments
The VirtualServer CRD (part of the nginx/kubernetes-ingress commercial variant but also available in extended OSS builds) unlocks proper traffic splitting without annotation soup:
# nginx-virtualserver-canary.yaml
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
name: api-canary
namespace: production
spec:
host: api.example.com
tls:
secret: api-tls-secret
upstreams:
- name: api-stable
service: api-stable-service
port: 8080
healthCheck:
enable: true
path: /healthz
interval: 10s
fails: 3
passes: 2
- name: api-canary
service: api-canary-service
port: 8080
routes:
- path: /
splits:
- weight: 90
action:
pass: api-stable
- weight: 10
action:
pass: api-canary
2.5 NGINX Tuning: ConfigMap for High-Traffic Production
# nginx-configmap-tuning.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
# Worker processes (match to CPU cores)
worker-processes: "auto"
# Max connections per worker
max-worker-connections: "65536"
# Keepalive timeout
keep-alive: "75"
keep-alive-requests: "10000"
# Enable Brotli compression
enable-brotli: "true"
brotli-level: "6"
brotli-types: "text/plain text/css application/json application/javascript"
# Log format with request ID for tracing
log-format-upstream: >
{"time":"$time_iso8601","remote_addr":"$proxy_protocol_addr",
"x_forwarded_for":"$proxy_add_x_forwarded_for","request_id":"$req_id",
"remote_user":"$remote_user","bytes_sent":"$bytes_sent",
"request_time":"$request_time","status":"$status",
"host":"$host","request_uri":"$request_uri","method":"$request_method",
"http_referrer":"$http_referer","http_user_agent":"$http_user_agent",
"upstream_addr":"$upstream_addr","upstream_status":"$upstream_status",
"upstream_response_time":"$upstream_response_time"}
# Proxy buffer settings for large headers (common with JWT tokens)
proxy-buffer-size: "16k"
proxy-buffers-number: "4"
# HSTS
hsts: "true"
hsts-max-age: "31536000"
hsts-include-subdomains: "true"
3. Traefik Ingress Controller
3.1 Architecture Overview
Traefik takes a fundamentally different approach. Instead of periodically regenerating a static config file, Traefik maintains a live routing table that updates in real-time as Kubernetes resources change — no reload, no dropped connections. Its architecture centers on Providers (Kubernetes, Docker, Consul, etc.), Routers (match rules), Middlewares (transform requests), and Services (upstream targets).
[Internet] → [LoadBalancer] → [Traefik Pod(s)] → [Backend Services]
↑
[Live routing table — zero reload]
Kubernetes Provider watches:
- Ingress resources
- IngressRoute CRDs
- Middleware CRDs
3.2 Installation via Helm
# Add Traefik Helm repo
helm repo add traefik https://traefik.github.io/charts
helm repo update
# Install Traefik with production settings
helm install traefik traefik/traefik \
--namespace traefik \
--create-namespace \
--set deployment.replicas=3 \
--set ports.websecure.tls.enabled=true \
--set ingressClass.enabled=true \
--set ingressClass.isDefaultClass=true \
--set metrics.prometheus.enabled=true \
--set metrics.prometheus.serviceMonitor.enabled=true \
--set logs.general.level=INFO \
--set logs.access.enabled=true \
--set logs.access.format=json \
--set resources.requests.cpu=200m \
--set resources.requests.memory=256Mi \
--set resources.limits.cpu=1000m \
--set resources.limits.memory=512Mi \
--set autoscaling.enabled=true \
--set autoscaling.minReplicas=3 \
--set autoscaling.maxReplicas=10 \
--version 28.0.0
# Verify
kubectl rollout status deployment/traefik -n traefik
kubectl get svc -n traefik
3.3 IngressRoute CRD with Middleware Chain
Traefik's IngressRoute CRD is far more expressive than standard Ingress annotations:
# traefik-ingressroute.yaml
---
# Rate limiting middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: production
spec:
rateLimit:
average: 100
burst: 50
period: 1m
sourceCriterion:
ipStrategy:
depth: 1
---
# Security headers middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: secure-headers
namespace: production
spec:
headers:
sslRedirect: true
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
customRequestHeaders:
X-Forwarded-Proto: https
---
# Strip prefix middleware for versioned APIs
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: strip-api-prefix
namespace: production
spec:
stripPrefix:
prefixes:
- /api/v1
- /api/v2
---
# The actual IngressRoute
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: api-ingressroute
namespace: production
spec:
entryPoints:
- websecure
routes:
- match: Host(`api.example.com`) && PathPrefix(`/api/v1`)
kind: Rule
middlewares:
- name: rate-limit
- name: secure-headers
- name: strip-api-prefix
services:
- name: api-v1-service
port: 8080
weight: 90
- name: api-v1-canary-service
port: 8080
weight: 10
- match: Host(`api.example.com`) && PathPrefix(`/api/v2`)
kind: Rule
middlewares:
- name: rate-limit
- name: secure-headers
services:
- name: api-v2-service
port: 8080
tls:
certResolver: letsencrypt
3.4 Built-in Let's Encrypt with DNS Challenge
Traefik's native ACME support is a genuine differentiator, especially for internal/wildcard certificates:
# traefik-values-acme.yaml — append to Helm values
additionalArguments:
- "--certificatesresolvers.letsencrypt.acme.email=ops@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=azure"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=30"
env:
- name: AZURE_CLIENT_ID
valueFrom:
secretKeyRef:
name: traefik-azure-dns
key: client-id
- name: AZURE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: traefik-azure-dns
key: client-secret
- name: AZURE_SUBSCRIPTION_ID
valueFrom:
secretKeyRef:
name: traefik-azure-dns
key: subscription-id
- name: AZURE_TENANT_ID
valueFrom:
secretKeyRef:
name: traefik-azure-dns
key: tenant-id
- name: AZURE_RESOURCE_GROUP
value: "dns-resource-group"
persistence:
enabled: true
size: 1Gi
storageClass: "managed-premium"
4. Azure Application Gateway Ingress Controller (AGIC)
4.1 Architecture Overview
AGIC is architecturally different from the other two. Rather than running a proxy inside the cluster, AGIC runs as a Kubernetes controller pod that translates Kubernetes Ingress resources into Azure Application Gateway configuration via the Azure Resource Manager API. The actual traffic proxy is Azure Application Gateway — a fully managed PaaS resource running outside your cluster.
[Internet] → [Azure Application Gateway (PaaS)] → [AKS Node IPs]
↑
[AGIC Pod (in-cluster)]
Watches Ingress resources
→ Calls ARM API to update
Application Gateway config
This means:
- No in-cluster proxy overhead — but you pay for the Application Gateway SKU
- WAF is native via Azure WAF Policy (OWASP 3.2 ruleset out of the box)
- Configuration changes are slower — ARM API calls take 3–8 seconds vs milliseconds for in-cluster controllers
- Requires WAF_v2 or Standard_v2 SKU for production use
4.2 Installation: AGIC with AKS Add-on (Recommended)
# Variables
RESOURCE_GROUP="rg-aks-prod"
LOCATION="eastus2"
CLUSTER_NAME="aks-prod-01"
APPGW_NAME="appgw-aks-prod"
APPGW_SUBNET="10.1.0.0/24"
VNET_NAME="vnet-prod"
# Create Application Gateway subnet
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name $VNET_NAME \
--name AppGatewaySubnet \
--address-prefix $APPGW_SUBNET
# Create Application Gateway (WAF_v2 for production WAF support)
az network application-gateway create \
--name $APPGW_NAME \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku WAF_v2 \
--capacity 2 \
--vnet-name $VNET_NAME \
--subnet AppGatewaySubnet \
--public-ip-address appgw-pip \
--priority 100
# Enable AGIC add-on on existing AKS cluster
APPGW_ID=$(az network application-gateway show \
--name $APPGW_NAME \
--resource-group $RESOURCE_GROUP \
--query id -o tsv)
az aks enable-addons \
--name $CLUSTER_NAME \
--resource-group $RESOURCE_GROUP \
--addons ingress-appgw \
--appgw-id $APPGW_ID
# Verify AGIC pod is running
kubectl get pods -n kube-system -l app=ingress-appgw
4.3 Ingress with WAF Policy
# agic-ingress-waf.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress-agic
namespace: production
annotations:
kubernetes.io/ingress.class: azure/application-gateway
# WAF policy (created separately in Azure)
appgw.ingress.kubernetes.io/waf-policy-for-path: /subscriptions/<sub-id>/resourceGroups/rg-aks-prod/providers/Microsoft.Network/applicationGatewayWebApplicationFirewallPolicies/waf-policy-prod
# SSL certificate from Key Vault
appgw.ingress.kubernetes.io/appgw-ssl-certificate: keyvault-ssl-cert
# Connection draining for zero-downtime deployments
appgw.ingress.kubernetes.io/connection-draining: "true"
appgw.ingress.kubernetes.io/connection-draining-timeout: "30"
# Cookie-based session affinity
appgw.ingress.kubernetes.io/cookie-based-affinity: "true"
# Custom health probe
appgw.ingress.kubernetes.io/health-probe-path: /healthz
appgw.ingress.kubernetes.io/health-probe-interval: "15"
appgw.ingress.kubernetes.io/health-probe-threshold: "3"
# Request timeout
appgw.ingress.kubernetes.io/request-timeout: "60"
spec:
ingressClassName: azure-application-gateway
tls:
- hosts:
- api.example.com
secretName: api-tls-secret
rules:
- host: api.example.com
http:
paths:
- path: /v1/*
pathType: ImplementationSpecific
backend:
service:
name: api-v1-service
port:
number: 8080
- path: /v2/*
pathType: ImplementationSpecific
backend:
service:
name: api-v2-service
port:
number: 8080
4.4 WAF Policy via Azure CLI
# Create WAF policy with OWASP 3.2 ruleset
az network application-gateway waf-policy create \
--name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--location $LOCATION
# Set policy to Prevention mode (Detection for initial rollout)
az network application-gateway waf-policy policy-setting update \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--mode Prevention \
--state Enabled \
--request-body-check true \
--max-request-body-size-kb 128
# Associate OWASP 3.2 managed ruleset
az network application-gateway waf-policy managed-rule ruleset add \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--type OWASP \
--version 3.2
# Add custom rule: block requests from known bad ASNs
az network application-gateway waf-policy custom-rule create \
--policy-name waf-policy-prod \
--resource-group $RESOURCE_GROUP \
--name BlockBadUserAgents \
--priority 10 \
--rule-type MatchRule \
--action Block
# Key Vault integration for SSL certificates
az keyvault certificate import \
--vault-name kv-aks-prod \
--name api-ssl-cert \
--file api.example.com.pfx
# Reference Key Vault cert in Application Gateway
az network application-gateway ssl-cert create \
--gateway-name $APPGW_NAME \
--resource-group $RESOURCE_GROUP \
--name keyvault-ssl-cert \
--key-vault-secret-id $(az keyvault certificate show \
--vault-name kv-aks-prod \
--name api-ssl-cert \
--query sid -o tsv)
5. Head-to-Head Performance Comparison
5.1 Benchmarking Methodology
The figures in this section are indicative — drawn from commonly reported community and vendor benchmarks, not a controlled test run for this article — so treat them as rough relative characteristics rather than measurements. A representative comparison setup is AKS 1.29 with Standard_D4s_v3 nodes (4 vCPU, 16 GB RAM), 3 controller replicas each, routing to an identical echo-server backend, load-generated with k6.
# k6 load test script (save as load-test.js)
# Run with: k6 run --vus 1000 --duration 5m load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 1000,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://api.example.com/v1/echo', {
headers: { 'Accept': 'application/json' },
});
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(0.1);
}
5.2 Results Summary
| Metric | NGINX | Traefik | AGIC |
|---|---|---|---|
| Throughput (RPS) | ~48,000 | ~45,000 | ~38,000 |
| P95 Latency | 8ms | 11ms | 24ms |
| P99 Latency | 22ms | 28ms | 67ms |
| Config Reload Latency | 500ms–2s | 0ms (live) | 3–8s (ARM) |
| Memory per Pod | 180–220 MB | 140–170 MB | 95 MB (AGIC pod) |
| CPU per Pod (sustained) | 0.4–0.6 cores | 0.3–0.5 cores | 0.1 cores |
⚠️ Note: AGIC CPU/memory figures reflect only the controller pod. Azure Application Gateway resource costs are external to the cluster and not reflected here.
6. Gateway API Support (The Future of Kubernetes Ingress)
The Kubernetes Gateway API graduated to GA in 1.28 and represents the long-term evolution beyond Ingress. Here's where each controller stands:
| Feature | NGINX | Traefik | AGIC |
|---|---|---|---|
| Gateway API Version | v1 (GA) | v1 (GA) | v1 (Preview) |
| HTTPRoute | ✅ Stable | ✅ Stable | ✅ Preview |
| GRPCRoute | ✅ Stable | ✅ Stable | ❌ Not yet |
| TCPRoute | ✅ Beta | ✅ Stable | ❌ Not yet |
| TLSRoute | ✅ Beta | ✅ Stable | ❌ Not yet |
| ReferenceGrant | ✅ | ✅ | ✅ |
# Gateway API HTTPRoute example (works with NGINX and Traefik)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-httproute
namespace: production
spec:
parentRefs:
- name: prod-gateway
namespace: gateway-system
hostnames:
- "api.example.com"
rules:
- matches:
- path:
type: PathPrefix
value: /v1
headers:
- name: "x-canary"
value: "true"
backendRefs:
- name: api-v1-canary-service
port: 8080
weight: 1
- matches:
- path:
type: PathPrefix
value: /v1
backendRefs:
- name: api-v1-service
port: 8080
weight: 99
7. Pitfalls to Avoid
NGINX Pitfalls
-
Reload storms under high churn: Each
Ingresschange triggers an NGINX reload. In clusters with hundreds of frequently-updated services, reloads can queue up and cause 500–2,000ms latency spikes. Fix: Usenginx.ingress.kubernetes.io/load-balance: ewmaand batch your Ingress changes. Consider theVirtualServerCRD which supports dynamic upstream updates without reload in NGINX Plus. -
Annotation sprawl: Teams accumulate dozens of per-Ingress annotations that are impossible to audit centrally. Fix: Move global settings to the ConfigMap and use naming conventions. Evaluate migrating to
VirtualServer/HTTPRouteCRDs. -
Not setting resource limits on the controller: An unthrottled NGINX controller will consume all node CPU under DDoS. Fix: Always set
limits.cpuandlimits.memory; pair with HPA targeting CPU at 70%. -
Missing
--ingress-classflag with multiple controllers: Running two NGINX controllers without distinctingressClassNamevalues causes both to pick up all Ingress resources. Fix: Always setcontroller.ingressClassexplicitly in Helm values.
Traefik Pitfalls
- ACME storage on ephemeral volumes: Storing
acme.jsonon a non-persistent volume means Let's Encrypt state is lost on pod restart, triggering certificate re-issuance and potentially hitting rate limits (5 certs per domain per week). Fix: Always use aPersistentVolumeClaimfor Traefik's data directory.
Leave a Reply