6.9 KiB
| project | type | status | path | tags | created | updated | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| traefik-deployment | project-plan | active | Tech/Projects |
|
2026-05-06 | 2026-05-06 |
Traefik Deployment — Homelab Ingress
Goal
Replace ad-hoc host:port access with consistent named endpoints for ~8
services across three surfaces (public, private, east-west). Build the full
ingress stack — Traefik, Knot DNS, wildcard TLS — and migrate MCP behind it
as the first real service.
Locked Architecture
Topology
- Traefik VM on PVE (own VM, runs Docker + tailscaled + cloudflared)
- Knot primary in LXC on PVE
- Knot secondary in Docker on NAS (Ubuntu bare metal)
- Pi-hole + Unbound stays as recursive frontend + ad-blocker
- Cross-VLAN firewall locks PVE↔NAS to explicit flows (Knot AXFR, Traefik→NAS services)
DNS
- Knot authoritative for
herbylab.devinternal - Active/passive replication via native AXFR + NOTIFY
- Pi-hole conditional forwards
herbylab.devqueries to Knot - Split-horizon: same name resolves to Traefik VM internally, Cloudflare externally
- Zone file in git, Ansible deploys to primary
TLS
- Wildcard
*.herbylab.devvia Traefik + Cloudflare DNS-01 - One cert covers public and private surfaces
- Tailscale certs dropped from the plan
Naming
- Capability-based:
.herbylab.devregardless of host - No
lab.infix, nodev.prefix — domain is by function, not location
Routing surfaces
- Public: Cloudflare → cloudflared (on Traefik VM) → Traefik → service
- Private: Tailscale → Traefik VM tailnet IP → service (DNS via Pi-hole → Knot)
- East-west: direct, never through Traefik
Visual
┌─────────────────────┐
│ Cloudflare Edge │
│ *.herbylab.dev │
└──────────┬──────────┘
│ tunnel
tailnet ────────────────────┼──────────────── tailscale0 │ ┌──────────▼──────────┐ │ Traefik VM (PVE) │ │ - traefik │ │ - cloudflared │ │ - tailscaled │ └──────────┬──────────┘ │ ┌────────────────┼────────────────┐ │ │ │ ┌───▼──┐ ┌───▼──┐ ┌───▼──┐ │ MCP │ │whoami│ │ ... │ └──────┘ └──────┘ └──────┘
Build Phases
Phase 1 — Foundations
No services routed yet. Each step produces a verifiable artifact.
- 1.1 Cloudflare API token
- Scope:
herbylab.devzone, DNS edit permissions only - Verify with
curlagainst CF API - Store in password manager
- Scope:
- 1.2 Traefik VM on PVE
- Fresh Debian or Ubuntu, 1 vCPU, 1GB RAM, on PVE VLAN
- Install Docker, Tailscale;
tailscale up - Verify: pingable from laptop over tailnet
- Snapshot
- 1.3 Knot primary in LXC on PVE
- Stand up LXC, install Knot
- Load minimal zone file with one test record
- Verify:
dig test.herbylab.dev @returns expected answer
- 1.4 Knot secondary in Docker on NAS
- Configure as AXFR slave
- Open cross-VLAN firewall rule for AXFR
- Verify:
dig test.herbylab.dev @matches primary
- 1.5 Pi-hole conditional forwarding
- Configure Pi-hole:
herbylab.dev→ Knot - Verify from a normal client:
dig test.herbylab.devreturns Knot's answer
- Configure Pi-hole:
- 1.6 Wildcard cert via Traefik + Cloudflare DNS-01
- Minimal Traefik config, ACME with DNS-01 challenge
- Request
*.herbylab.dev - Verify: cert in
acme.json, expiry ~90 days out - No services routed yet
Phase 1 checkpoint: working DNS chain, working wildcard cert, Traefik VM with no routes. Production unchanged. Safe abort point.
Phase 2 — Canary (whoami)
- 2.1 Deploy
whoamion Traefik VM- Docker compose, labels for both public and private surfaces
- 2.2 Add DNS records
- Knot zone:
whoami.herbylab.dev→ Traefik VM tailnet IP - Cloudflare:
whoami.herbylab.dev→ cloudflared tunnel
- Knot zone:
- 2.3 Four-position verification
- Laptop on tailnet →
https://whoami.herbylab.devworks, real cert - Laptop off tailnet (LTE hotspot) → same URL works via cloudflared
digfrom inside LAN returns Traefik VM IPdigfrom outside returns Cloudflare IP
- Laptop on tailnet →
- 2.4 Decision: keep
whoamias permanent diagnostic, or tear down
Phase 3 — MCP Migration
Parallel-path safety net. Existing cloudflared → MCP stays running until
the new path is verified.
- 3.1 Add Traefik labels to MCP container
- Add internal DNS record in Knot
- Existing public path untouched
- 3.2 Verify private surface first
- From tailnet:
https://mcp.herbylab.dev→ Traefik → MCP works
- From tailnet:
- 3.3 Cut public traffic over
- Flip Cloudflare DNS / tunnel config to land
mcp.herbylab.devon Traefik - Verify Claude can still call MCP
- Rollback path: flip DNS back
- Flip Cloudflare DNS / tunnel config to land
- 3.4 Decommission old direct cloudflared route
- After a few days of stability
Open Items (Build-Time Decisions)
- Inventory current
*.herbylab.devrecords to migrate from Pi-hole local DNS into Knot zone file - TTL strategy — start at 300 during buildout, raise to 3600 once stable
- Second Pi-hole instance for redundancy (parallel work, not blocking)
- Pi-hole sync method — gravity-sync vs Teleporter
- Ansible role structure for Knot primary deployment
Key Principles
- Capability-based naming. Domain reflects function, not host. Service can move hosts without renaming.
- Surfaces compose, they don't overlap. Cloudflare = public auth + ACME validation. Tailscale = device naming on tailnet. Traefik = service routing once a request lands. Internal DNS = telling LAN clients where to send the packet.
- East-west stays direct. Service-to-service inside Docker/LAN never traverses Traefik.
- Build outside-in for foundations, inside-out for services. Cert and DNS first; canary before production.
- Parallel paths for migration. Never cut over working infrastructure without a rollback flip.
Reference
Prior session notes: Tech/Sessions/homelab-dns-decision.md (Knot
architecture lock-in, alternatives considered, why split-horizon with
public domain).
...sent from Jenny & Travis