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.
NoteThe community
ingress-nginxcontroller that many older tutorials use was retired in March 2026 and no longer receives security fixes. The Ingress resources in this guide are standardnetworking.k8s.io/v1objects, 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.
kubectlconfigured with cluster admin access.- A domain name with DNS A records pointing to a node that will receive traffic. This guide uses
app.your_domainandapi.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:
sudo snap install helm --classic
helm version
Step 2 - Installing the Traefik Ingress controller
Add the official Traefik chart repository:
helm repo add traefik https://traefik.github.io/charts
helm repo update
How Traefik receives traffic depends on your cluster:
- The cluster supports
LoadBalancerServices (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:
nano traefik-values.yaml
deployment:
kind: DaemonSet
service:
type: ClusterIP
ports:
web:
hostPort: 80
websecure:
hostPort: 443
Install the chart into its own namespace:
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:
kubectl get pods -n traefik -o wide
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:
kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
traefik traefik.io/ingress-controller <none> 1m
A request to a node now reaches Traefik, which answers 404 because no routes exist yet:
curl -I http://your_node_ip
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:
nano apps.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:
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:
nano ingress-paths.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:
kubectl apply -f ingress-paths.yaml
kubectl get ingress demo-paths
Test both paths. The Host header lets you test before DNS has propagated:
curl -H "Host: app.your_domain" http://your_node_ip/a
curl -H "Host: app.your_domain" http://your_node_ip/b
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:
kubectl delete -f ingress-paths.yaml
nano ingress-hosts.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:
kubectl apply -f ingress-hosts.yaml
curl http://app.your_domain
curl http://api.your_domain
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:
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:
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:
nano cluster-issuer.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:
kubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer letsencrypt-prod
NAME READY AGE
letsencrypt-prod True 10s
TipWhile testing, use the staging server
https://acme-staging-v02.api.letsencrypt.org/directoryin 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:
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:
kubectl apply -f ingress-hosts.yaml
kubectl get certificate
Issuance usually takes less than a minute:
NAME READY SECRET AGE
demo-tls True demo-tls 45s
Verify the certificate from outside:
curl -v https://app.your_domain 2>&1 | grep -E 'subject:|issuer:'
* 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:
nano redirect-https.yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: default
spec:
redirectScheme:
scheme: https
permanent: true
kubectl apply -f redirect-https.yaml
Reference it from the Ingress. The value format is <namespace>-<name>@kubernetescrd:
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:
curl -I http://app.your_domain
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 <name>. 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 if they need it.
