Homelab Kubernetes, Guide 3: Traefik
Learn how to deploy Traefik on K3s with automatic HTTPS using Let’s Encrypt and Cloudflare DNS challenges. This guide covers IngressRoutes, TLS, wildcard DNS, and exposing services by hostname.
This is part of a series on building a self-hosted Kubernetes homelab. Start with the series intro if you haven't already.
You have a cluster and MetalLB has an IP pool configured, but no services you deploy will be reachable by hostname yet. Traefik is what makes that happen. In these examples it sits at 192.168.1.91, receives every incoming request, looks at the hostname, and routes it to the correct service in the cluster. It also handles TLS: it gets a certificate from Let's Encrypt for each hostname and renews it automatically.
What Traefik Does
Traefik is a reverse proxy. Where a regular proxy is every computer/servcie come from many sources and go out one, a reverse proxy has many request come to one point and it distributes them properly. Every service in this series gets a subdomain like pihole.example.com or nextcloud.example.com. Traefik receives all requests to your domain and routes them to the right pod based on the hostname.
TLS (web encryption) termination happens at Traefik. Services inside the cluster communicate over plain HTTP. Traefik handles the HTTPS layer and holds the certificates.
Traefik gets certificates from Let's Encrypt using the Cloudflare DNS challenge. Instead of Let's Encrypt making an HTTP request to verify domain ownership, it checks for a specific DNS TXT record. Traefik creates that record via the Cloudflare API, Let's Encrypt verifies it, and the cert is issued. This process works without opening any ports on your router.
Prerequisites
- MetalLB is installed and
traefik-poolis configured at 192.168.1.91 - Your domain is managed in Cloudflare
- A Cloudflare API token with Zone: DNS: Edit permissions for your domain
To create the token: log in to the Cloudflare dashboard, go to Profile > API Tokens, click Create Token, use the Edit zone DNS template, and scope it to your domain. Copy the token before closing the page.
Create the Cloudflare DNS Records
Before installing Traefik, you need two DNS records in Cloudflare. These are what make your subdomains resolve to Traefik for devices on your LAN.
In the Cloudflare dashboard, go to your domain's DNS settings and add:
| Type | Name | Content | Proxy status |
|---|---|---|---|
| A | * |
192.168.1.91 |
DNS only (grey cloud) |
| A | @ |
192.168.1.91 |
DNS only (grey cloud) |
The * record is the wildcard. It tells DNS that any subdomain of your domain resolves to 192.168.1.91. The @ record covers the root domain itself.
Both records must be grey cloud (DNS only), not orange cloud (proxied). Here's why. When a record is orange cloud, Cloudflare intercepts the request and forwards it to the target IP from Cloudflare's servers. Your server's IP is 192.168.1.91, a private LAN address that is completely unreachable from the internet. Every request would fail. Grey cloud means Cloudflare simply returns the IP in the DNS response, and your browser connects directly. That works fine for devices already on your LAN.
Once Pi-hole is installed in Guide 5, it takes over internal DNS for your network and overrides these records locally. The Cloudflare records then serve as a fallback for any device that doesn't use Pi-hole.
Store the Cloudflare API Token
Traefik needs this token to create and remove DNS records during certificate issuance. Store it as a Kubernetes secret in the network namespace:
kubectl create secret generic cloudflare-api-token \
--from-literal=CF_DNS_API_TOKEN=your-token-here \
--namespace network
Create the Helm Values File
Create the appdata directory:
mkdir -p /data/appdata/traefik
Create /data/appdata/traefik/values.yaml. Replace [email protected] with your actual email:
globalArguments:
- "--global.sendanonymoususage=false"
additionalArguments:
- "--certificatesresolvers.letsencrypt.acme.email=your-email@domain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,8.8.8.8:53"
deployment:
replicas: 1
ports:
web:
redirectTo:
port: websecure
websecure:
tls:
enabled: true
service:
type: LoadBalancer
annotations:
metallb.io/address-pool: traefik-pool
ingressRoute:
dashboard:
enabled: false
providers:
kubernetesCRD:
enabled: true
allowCrossNamespace: true
additionalVolumeMounts:
- name: acme-data
mountPath: /data
additionalVolumes:
- name: acme-data
hostPath:
path: /data/appdata/traefik
type: DirectoryOrCreate
env:
- name: CF_DNS_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: CF_DNS_API_TOKEN
Here's what the key settings do:
| Setting | Reason |
|---|---|
acme.storage=/data/acme.json |
Where Traefik stores certificates. Mounted from /data/appdata/traefik on the host so they survive pod restarts. |
dnschallenge.provider=cloudflare |
Tells Traefik to use the Cloudflare API to create DNS challenge records. |
redirectTo: websecure |
Any HTTP request on port 80 is automatically redirected to HTTPS. |
allowCrossNamespace: true |
Allows IngressRoutes in any namespace to route traffic. Without this, only the network namespace works. |
dashboard.enabled: false |
Disables the auto-created dashboard IngressRoute so you can create your own behind TLS. |
Install Traefik
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik \
--namespace network \
--values /data/appdata/traefik/values.yaml
Verify Traefik Got Its IP
kubectl get svc -n network
The traefik service should show EXTERNAL-IP as 192.168.1.91. If it shows <pending>, check that MetalLB is running and the traefik-pool is configured correctly.
Check the pod is running:
kubectl get pods -n network
How IngressRoutes Work
An IngressRoute is a Traefik-specific Kubernetes resource. It maps a hostname to a service. Every service in this series gets one. Here's the structure:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: example
namespace: example-namespace
spec:
entryPoints:
- websecure
routes:
- match: Host(`service.example.com`)
kind: Rule
services:
- name: service-name
port: 80
tls:
certResolver: letsencrypt
The certResolver: letsencrypt line triggers automatic certificate issuance. The first time Traefik sees this IngressRoute, it requests a cert from Let's Encrypt using the DNS challenge. Subsequent requests use the cached cert and renewal is automatic.
Expose the Traefik Dashboard
Create /data/manifests/traefik-dashboard.yaml. Replace example.com with your domain:
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: traefik-dashboard
namespace: network
spec:
entryPoints:
- websecure
routes:
- match: Host(`traefik.example.com`)
kind: Rule
services:
- name: api@internal
kind: TraefikService
tls:
certResolver: letsencrypt
Apply it:
kubectl apply -f /data/manifests/traefik-dashboard.yaml
After 1-2 minutes for the DNS challenge to complete, https://traefik.example.com will show the Traefik dashboard with a valid certificate.
Common Problems
The first certificate request takes 1-2 minutes. Let's Encrypt has to verify the DNS TXT record that Traefik creates via the Cloudflare API. After the first cert is issued and cached, subsequent IngressRoutes get their certs quickly.
Let's Encrypt rate limits apply. If you create and delete IngressRoutes repeatedly during testing, you can hit the hourly issuance limit. While testing, add this to additionalArguments to use the Let's Encrypt staging server (which issues untrusted certs but has no rate limits):
--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
Remove it when you're ready for production certs.
allowCrossNamespace: true is required. Every service in this series has its IngressRoute in its own namespace. Without this setting, Traefik ignores IngressRoutes outside the traefik namespace.
Summary
Traefik is running at 192.168.1.91. Any IngressRoute you create with certResolver: letsencrypt will automatically get a TLS certificate from Let's Encrypt. HTTP traffic is redirected to HTTPS automatically.
In Guide 4, you'll set up Cloudflare Tunnel so your services are accessible from outside your home network without opening any ports on your router.