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.

Homelab Kubernetes, Guide 3: Traefik

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-pool is 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.