--- title: "How to Configure Kubernetes Ingress with Traefik and TLS" description: "Install the Traefik Ingress controller with Helm, route traffic by host and path to your Services, and issue free Let's Encrypt certificates with cert-manager." category: "Docker Kubernetes" language: en source: https://cubepath.com/docs/docker-kubernetes/ingress-configuration-in-kubernetes updated: 2026-09-25 --- # How to Configure Kubernetes Ingress with Traefik and TLS An Ingress is a Kubernetes object that describes how external HTTP and HTTPS traffic reaches Services inside the cluster: which hostname and path go to which Service, and which TLS certificate to use. The Ingress itself does nothing until an **Ingress controller** reads it and configures a reverse proxy. In this tutorial you will install Traefik as the Ingress controller, route two applications by path and by hostname, and secure them with automatic Let's Encrypt certificates from cert-manager. > Note: The community `ingress-nginx` controller that many older tutorials use was retired in March 2026 and no longer receives security fixes. The Ingress resources in this guide are standard `networking.k8s.io/v1` objects, so they work with any maintained controller; only the annotations in Step 7 are Traefik specific. ## Prerequisites To follow this tutorial you need: - A Kubernetes cluster (1.30 or later) with at least one worker node, for example one built on CubePath VPS instances with [kubeadm on Ubuntu 24.04](/docs/docker-kubernetes/kubernetes-installation-with-kubeadm). - `kubectl` configured with cluster admin access. - A domain name with DNS A records pointing to a node that will receive traffic. This guide uses `app.your_domain` and `api.your_domain`. - Ports 80 and 443 open on that node. With UFW: `sudo ufw allow 80,443/tcp`. ## Step 1 - Installing Helm Helm is the package manager for Kubernetes, and it is the supported way to install both Traefik and cert-manager. On Ubuntu 24.04, install it from the Snap Store on the machine where you run `kubectl`: ```bash sudo snap install helm --classic helm version ``` ## Step 2 - Installing the Traefik Ingress controller Add the official Traefik chart repository: ```bash helm repo add traefik https://traefik.github.io/charts helm repo update ``` How Traefik receives traffic depends on your cluster: - **The cluster supports `LoadBalancer` Services** (a managed Kubernetes service, or MetalLB on bare metal): the chart defaults are enough, and the Service gets an external IP. - **A self-built kubeadm cluster without a load balancer**: run Traefik as a DaemonSet that binds ports 80 and 443 directly on each node through `hostPort`. This is the setup used below. Create a values file: ```bash nano traefik-values.yaml ``` ```yaml deployment: kind: DaemonSet service: type: ClusterIP ports: web: hostPort: 80 websecure: hostPort: 443 ``` Install the chart into its own namespace: ```bash helm install traefik traefik/traefik \ --namespace traefik --create-namespace \ -f traefik-values.yaml ``` If your cluster does support `LoadBalancer` Services, run the same command without `-f traefik-values.yaml`. Check that one Traefik Pod runs on each worker node: ```bash kubectl get pods -n traefik -o wide ``` ```output NAME READY STATUS RESTARTS AGE IP NODE traefik-4k8zq 1/1 Running 0 40s 10.244.1.9 worker-1 traefik-x7m2d 1/1 Running 0 40s 10.244.2.6 worker-2 ``` The chart also creates an `IngressClass` named `traefik` and marks it as the default. Ingress objects refer to it with `ingressClassName`: ```bash kubectl get ingressclass ``` ```output NAME CONTROLLER PARAMETERS AGE traefik traefik.io/ingress-controller 1m ``` A request to a node now reaches Traefik, which answers `404` because no routes exist yet: ```bash curl -I http://your_node_ip ``` ```output HTTP/1.1 404 Not Found ``` ## Step 3 - Deploying two sample applications You need something to route to. Deploy two small echo servers, each with its own `ClusterIP` Service: ```bash nano apps.yaml ``` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: app-a spec: replicas: 2 selector: matchLabels: app: app-a template: metadata: labels: app: app-a spec: containers: - name: echo image: hashicorp/http-echo:1.0 args: ["-text=Hello from app A"] ports: - containerPort: 5678 --- apiVersion: v1 kind: Service metadata: name: app-a spec: selector: app: app-a ports: - port: 80 targetPort: 5678 --- apiVersion: apps/v1 kind: Deployment metadata: name: app-b spec: replicas: 2 selector: matchLabels: app: app-b template: metadata: labels: app: app-b spec: containers: - name: echo image: hashicorp/http-echo:1.0 args: ["-text=Hello from app B"] ports: - containerPort: 5678 --- apiVersion: v1 kind: Service metadata: name: app-b spec: selector: app: app-b ports: - port: 80 targetPort: 5678 ``` Apply it and wait for both Deployments: ```bash kubectl apply -f apps.yaml kubectl rollout status deployment/app-a kubectl rollout status deployment/app-b ``` ## Step 4 - Routing by path A path-based Ingress sends different URL prefixes on the same hostname to different Services. Create it: ```bash nano ingress-paths.yaml ``` ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-paths spec: ingressClassName: traefik rules: - host: app.your_domain http: paths: - path: /a pathType: Prefix backend: service: name: app-a port: number: 80 - path: /b pathType: Prefix backend: service: name: app-b port: number: 80 ``` `pathType` controls how the path matches: | pathType | Matches `/a` | Matches `/a/x` | Matches `/abc` | |----------|--------------|----------------|----------------| | `Exact` | Yes | No | No | | `Prefix` | Yes | Yes | No | `Prefix` matches whole path segments, so `/a` does not match `/abc`. Apply the Ingress and check that it was accepted: ```bash kubectl apply -f ingress-paths.yaml kubectl get ingress demo-paths ``` Test both paths. The `Host` header lets you test before DNS has propagated: ```bash curl -H "Host: app.your_domain" http://your_node_ip/a curl -H "Host: app.your_domain" http://your_node_ip/b ``` ```output Hello from app A Hello from app B ``` ## Step 5 - Routing by hostname Host-based routing gives each application its own hostname, which is the usual choice for separate services. Replace the previous Ingress with one rule per host: ```bash kubectl delete -f ingress-paths.yaml nano ingress-hosts.yaml ``` ```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo spec: ingressClassName: traefik rules: - host: app.your_domain http: paths: - path: / pathType: Prefix backend: service: name: app-a port: number: 80 - host: api.your_domain http: paths: - path: / pathType: Prefix backend: service: name: app-b port: number: 80 ``` Apply and test it: ```bash kubectl apply -f ingress-hosts.yaml curl http://app.your_domain curl http://api.your_domain ``` ```output Hello from app A Hello from app B ``` If these commands fail but the `Host` header test works, your DNS records are not pointing to the node yet. Check them with `dig +short app.your_domain`. ## Step 6 - Adding automatic HTTPS with cert-manager cert-manager requests certificates from Let's Encrypt, stores them as Kubernetes Secrets and renews them before they expire. Install it from its official OCI chart, including its CustomResourceDefinitions: ```bash helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \ --namespace cert-manager --create-namespace \ --set crds.enabled=true ``` Wait for its three Pods (`cert-manager`, `cert-manager-cainjector` and `cert-manager-webhook`) to be running: ```bash kubectl get pods -n cert-manager ``` Create a `ClusterIssuer` that uses the Let's Encrypt production API and solves the HTTP-01 challenge through Traefik. Replace `your_email` with an address you control: ```bash nano cluster-issuer.yaml ``` ```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: your_email privateKeySecretRef: name: letsencrypt-prod-account-key solvers: - http01: ingress: ingressClassName: traefik ``` Apply it and check that the ACME account was registered: ```bash kubectl apply -f cluster-issuer.yaml kubectl get clusterissuer letsencrypt-prod ``` ```output NAME READY AGE letsencrypt-prod True 10s ``` > Tip: While testing, use the staging server `https://acme-staging-v02.api.letsencrypt.org/directory` in a second issuer. Its certificates are not trusted by browsers, but its rate limits are much higher. Now request certificates for the Ingress. Add the `cert-manager.io/cluster-issuer` annotation and a `tls` section to `ingress-hosts.yaml`. The `metadata` and `spec` blocks become: ```yaml metadata: name: demo annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: traefik tls: - hosts: - app.your_domain - api.your_domain secretName: demo-tls rules: # keep the two rules from Step 5 unchanged ``` Apply it. cert-manager sees the annotation, creates a `Certificate` object and completes the challenge: ```bash kubectl apply -f ingress-hosts.yaml kubectl get certificate ``` Issuance usually takes less than a minute: ```output NAME READY SECRET AGE demo-tls True demo-tls 45s ``` Verify the certificate from outside: ```bash curl -v https://app.your_domain 2>&1 | grep -E 'subject:|issuer:' ``` ```output * subject: CN=app.your_domain * issuer: C=US; O=Let's Encrypt; CN=R12 ``` ## Step 7 - Redirecting HTTP to HTTPS Traefik configures features such as redirects through `Middleware` objects, which you attach to an Ingress with an annotation. Create a middleware that redirects to HTTPS: ```bash nano redirect-https.yaml ``` ```yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: redirect-https namespace: default spec: redirectScheme: scheme: https permanent: true ``` ```bash kubectl apply -f redirect-https.yaml ``` Reference it from the Ingress. The value format is `-@kubernetescrd`: ```bash kubectl annotate ingress demo traefik.ingress.kubernetes.io/router.middlewares=default-redirect-https@kubernetescrd ``` Add the same annotation to `ingress-hosts.yaml` so the next `kubectl apply` keeps it. Then check the redirect: ```bash curl -I http://app.your_domain ``` ```output HTTP/1.1 308 Permanent Redirect Location: https://app.your_domain/ ``` Certificate renewals keep working, because cert-manager publishes its challenge on a separate, more specific route. ## Troubleshooting **Traefik returns 404 for your hostname.** Check `kubectl describe ingress demo`: the `ingressClassName` must be `traefik`, the host must match the request exactly, and every backend must list endpoints. `kubectl logs -n traefik -l app.kubernetes.io/name=traefik` shows configuration errors. **`502 Bad Gateway` or `Service Unavailable`.** Traefik found the route but not a ready Pod. Check the Service selector and the Pods with `kubectl get endpointslices -l kubernetes.io/service-name=app-a`. **The certificate stays `READY False`.** Follow the chain with `kubectl describe certificate demo-tls`, then `kubectl get challenges` and `kubectl describe challenge `. The usual causes are a DNS record that does not point to the node, or port 80 blocked by the firewall, so Let's Encrypt cannot reach the challenge URL. **Connections to ports 80 or 443 time out.** Confirm that a Traefik Pod runs on the node you are testing and that UFW allows `80,443/tcp` on it. ## Conclusion You installed Traefik as the Ingress controller, routed traffic to two Services by path and by hostname, and secured them with Let's Encrypt certificates that cert-manager renews automatically. Every new application now needs only a `ClusterIP` Service and an Ingress with the `cert-manager.io/cluster-issuer` annotation. As next steps, add more Traefik middlewares (basic authentication, rate limiting, security headers), look at the Gateway API, which Traefik also supports, for more advanced routing, and give your applications [persistent storage](/docs/docker-kubernetes/persistent-volumes-in-kubernetes) if they need it.