Traefik Ingress
Traefik is the ingress controller for this platform. It ships as the default ingress controller with K3s and handles all inbound HTTPS traffic — routing requests to the correct Kubernetes service based on hostname, terminating TLS, and providing automatic certificate management.
Why Traefik?
Section titled “Why Traefik?”Traefik was not explicitly chosen — it comes bundled with K3s. However, it is a natural fit for this platform:
- Zero configuration. K3s installs and configures Traefik automatically. No Helm chart, no custom values, no additional manifests are needed for the controller itself.
- CRD-native routing. Traefik’s
IngressRouteCRD provides more expressive routing than the standard KubernetesIngressresource, including native support for host matching, path rules, middleware chains, and TLS configuration. - Automatic TLS. Traefik integrates with Let’s Encrypt out of the box for automatic certificate provisioning and renewal. In this platform, Cloudflare handles TLS at the edge, so Traefik terminates TLS between Cloudflare and the cluster.
- Low resource footprint. Traefik runs as a single pod on node1, consuming minimal CPU and memory — consistent with the platform’s cost-effective approach.
Traffic Flow
Section titled “Traffic Flow”graph TD
user["User Browser"]
cf["Cloudflare<br/>(CDN + DDoS protection)"]
nsg["Azure NSG<br/>(ports 80, 443)"]
traefik["Traefik<br/>(K3s Ingress Controller)"]
subgraph cluster["K3s Cluster"]
traefik
subgraph sites["Site Services"]
kr["kevinryan-io:80"]
brand["brand-kevinryan-io:80"]
docs["docs-kevinryan-io:80"]
ai["aiimmigrants-com:80"]
spec["specmcp-ai:80"]
sdd["sddbook-com:80"]
de["distributedequity-org:80"]
end
subgraph services["Platform Services"]
umami["umami:80"]
grafana["grafana:80"]
end
end
user -->|HTTPS| cf
cf -->|HTTPS| nsg
nsg --> traefik
traefik -->|Host routing| sites & services
- The user’s browser connects to Cloudflare over HTTPS
- Cloudflare proxies the request to the Azure public IP (node1) over HTTPS
- The NSG allows traffic on ports 80 and 443
- Traefik receives the request, matches the
Hostheader against its IngressRoute rules, and forwards to the correct ClusterIP service - The service routes to the nginx pod on port 8080
IngressRoute Configuration
Section titled “IngressRoute Configuration”Every site and service uses a Traefik IngressRoute CRD (traefik.io/v1alpha1). All follow the same pattern:
apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: <site-name> namespace: <site-name>spec: entryPoints: - websecure routes: - match: Host(`<domain>`) || Host(`www.<domain>`) kind: Rule services: - name: <site-name> port: 80 tls: {}Key Elements
Section titled “Key Elements”| Field | Value | Purpose |
|---|---|---|
entryPoints | websecure | Listen on the HTTPS port (443) only |
match | Host(...) | Route based on the HTTP Host header |
services.port | 80 | Forward to the ClusterIP service on port 80 |
tls: {} | Empty object | Enable TLS termination with default certificate |
Route Inventory
Section titled “Route Inventory”All nine IngressRoutes in the cluster:
| IngressRoute | Namespace | Host Match | Service |
|---|---|---|---|
kevinryan-io | kevinryan-io | kevinryan.io || www.kevinryan.io | kevinryan-io:80 |
brand-kevinryan-io | brand-kevinryan-io | brand.kevinryan.io | brand-kevinryan-io:80 |
docs-kevinryan-io | docs-kevinryan-io | docs.kevinryan.io | docs-kevinryan-io:80 |
aiimmigrants-com | aiimmigrants-com | aiimmigrants.com || www.aiimmigrants.com | aiimmigrants-com:80 |
specmcp-ai | specmcp-ai | specmcp.ai || www.specmcp.ai | specmcp-ai:80 |
sddbook-com | sddbook-com | sddbook.com || www.sddbook.com | sddbook-com:80 |
distributedequity-org | distributedequity-org | distributedequity.org || www.distributedequity.org | distributedequity-org:80 |
umami | umami | analytics.kevinryan.io | umami:80 |
grafana | observability | monitoring.kevinryan.io | grafana:80 |
Host Matching Patterns
Section titled “Host Matching Patterns”Sites use two patterns:
- Apex + www — most sites match both the bare domain and
wwwsubdomain:Host('example.com') || Host('www.example.com'). This ensures users reach the site regardless of whether they includewww. - Subdomain only — subdomains like
brand.kevinryan.io,docs.kevinryan.io,analytics.kevinryan.io, andmonitoring.kevinryan.iomatch a single hostname sincewwwsubdomains of subdomains are not used.
TLS Termination
Section titled “TLS Termination”Every IngressRoute specifies tls: {} with an empty configuration object. This delegates certificate management to Traefik’s default TLS resolver.
Cloudflare + Traefik TLS Model
Section titled “Cloudflare + Traefik TLS Model”The TLS chain in this platform has two layers:
graph LR
user["User"] -->|"TLS (Cloudflare cert)"| cf["Cloudflare Edge"]
cf -->|"TLS (Traefik cert)"| traefik["Traefik"]
traefik -->|"HTTP (plain)"| svc["ClusterIP Service"]
| Segment | TLS | Certificate |
|---|---|---|
| User → Cloudflare | Yes | Cloudflare-issued (managed automatically) |
| Cloudflare → Traefik | Yes | Traefik default certificate (self-signed or Let’s Encrypt) |
| Traefik → Service | No | Plain HTTP within the cluster network |
Cloudflare’s SSL/TLS mode is set to “Full”, meaning Cloudflare connects to the origin (Traefik) over HTTPS but does not require a valid CA-signed certificate. This allows Traefik to use its default self-signed certificate for the Cloudflare-to-origin segment while Cloudflare handles the trusted certificate presented to end users.
Namespace Isolation
Section titled “Namespace Isolation”Each IngressRoute lives in the same namespace as the service it routes to. This is a deliberate design choice:
- No cross-namespace routing. Each site’s ingress configuration is self-contained within its namespace, managed by the same Flux Kustomization as the site’s deployment and service.
- Independent lifecycle. Adding, removing, or modifying a site’s routing does not touch any shared configuration. The IngressRoute is just another manifest in
k8s/<site-name>/. - Least privilege. Flux’s
prune: truesetting means removing a site’s directory fromk8s/automatically removes its IngressRoute, service, deployment, and namespace — a clean teardown with no orphaned routes.
Entry Points
Section titled “Entry Points”K3s configures Traefik with two default entry points:
| Entry Point | Port | Protocol | Used By |
|---|---|---|---|
web | 80 | HTTP | Not used (no HTTP-only routes defined) |
websecure | 443 | HTTPS | All IngressRoutes |
All IngressRoutes in this platform use websecure exclusively. HTTP traffic on port 80 is handled by Traefik’s default HTTP-to-HTTPS redirect, ensuring all requests are upgraded to TLS.
Service Mesh
Section titled “Service Mesh”Traefik routes to standard Kubernetes ClusterIP services. The service-to-pod mapping is straightforward:
apiVersion: v1kind: Servicemetadata: name: <site-name> namespace: <site-name>spec: selector: app: <site-name> ports: - port: 80 targetPort: 8080Every service maps port 80 (what Traefik connects to) to port 8080 (what the nginx container listens on). The only exception is Umami, which maps port 80 to port 3000 (its Node.js server), and Grafana, which also exposes port 80.
Adding a Route for a New Site
Section titled “Adding a Route for a New Site”To add Traefik routing for a new site:
- Create
k8s/<site-name>/ingress.yaml:
apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: <site-name> namespace: <site-name>spec: entryPoints: - websecure routes: - match: Host(`<domain>`) || Host(`www.<domain>`) kind: Rule services: - name: <site-name> port: 80 tls: {}-
Ensure the corresponding
service.yamlexists in the same directory with a selector matching the deployment’s pod labels. -
Add DNS records in Terraform (Cloudflare module) pointing the domain to the cluster’s public IP.
-
Merge to
main. Flux applies the IngressRoute and Traefik begins routing traffic to the new service within the reconciliation interval.
No Traefik restart or reconfiguration is needed — it watches for IngressRoute CRDs and updates its routing table dynamically.