7.6 KiB
7.6 KiB
Solution Architecture
Overview
The wylab.me infrastructure resolves the Traefik + Technitium + Docker circular dependency through four interacting components. The architecture separates the DNS resolution path for internal services (via Technitium at 172.17.0.1) from the ACME certificate DNS path (via Cloudflare 1.1.1.1 directly), eliminating both the 8-second latency and the circular dependency.
Component Graph
INTERNET
│
Let's Encrypt
ACME endpoint
│
│ (DNS via 1.1.1.1 — bypasses Technitium)
│
┌──────────────────────────────────────────────┐
│ UNRAID HOST (192.168.1.50) │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ HOST NETWORK NAMESPACE │ │
│ │ │ │
│ │ Technitium DNS │ │
│ │ Binds: 0.0.0.0:53 (UDP/TCP) │ │
│ │ → Reachable at 172.17.0.1:53 │ │
│ │ → Reachable at 192.168.1.50:53 │ │
│ │ │ │
│ │ iptables DNAT rules (for host-net │ │
│ │ containers needing bridge services) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ docker0: 172.17.0.1/16 │
│ │ │
│ ┌───────────┴────────────────────────────┐ │
│ │ BRIDGE NETWORK │ │
│ │ │ │
│ │ Traefik Container (bridge mode) │ │
│ │ IP: 172.17.0.x │ │
│ │ Port 80, 443 mapped │ │
│ │ → DNS: 172.17.0.1 (daemon.json) │ │
│ │ → ACME resolvers: ["1.1.1.1:53"] │ │
│ │ → Backend: http://172.17.0.1:PORT │ │
│ │ │ │
│ │ Other Service Containers (bridge mode) │ │
│ │ IPs: 172.17.0.x │ │
│ │ → DNS: 172.17.0.1 (daemon.json) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ /etc/docker/daemon.json: │
│ {"dns": ["172.17.0.1"]} │
│ │
│ /boot/config/go: │
│ (persists daemon.json + iptables on boot) │
└──────────────────────────────────────────────┘
Components
Traefik Reverse Proxy
- Purpose: Terminates TLS, routes HTTP/HTTPS traffic to backend containers, manages Let's Encrypt certificates
- Network mode: Bridge (default Docker network)
- Inputs: Inbound HTTP (port 80) and HTTPS (port 443) from LAN/internet; Docker label-based service discovery; Traefik dynamic config files
- Outputs: Proxied requests to backend containers; ACME certificate requests to Let's Encrypt; certificate storage in acme.json
- Key design choices:
- Runs in bridge mode (not host mode) to use Docker's internal service discovery and label-based routing
- ACME resolver configured with
resolvers = ["1.1.1.1:53"]to bypass Technitium for certificate DNS - Backend URLs for host-networked services use
172.17.0.1:PORT(not127.0.0.1:PORTor192.168.1.50:PORT)
Technitium DNS Server
- Purpose: Internal DNS resolver for
wylab.medomain; forwards public DNS queries upstream - Network mode: Host (shares host network namespace)
- Inputs: DNS queries on UDP/TCP port 53 from all network interfaces
- Outputs: DNS responses; upstream forwarding for non-local domains
- Key design choices:
- Host network mode required so it binds to
172.17.0.1(the docker0 gateway), making it reachable from bridge containers - Management UI (
dns.wylab.me) is served through Traefik — creates the documented circular dependency for ACME, mitigated by Traefik's explicit ACME DNS bypass - Also reachable from LAN at
192.168.1.50:53for non-Docker clients
- Host network mode required so it binds to
Docker Daemon (daemon.json)
- Purpose: Provides DNS configuration to all bridge-networked containers at creation time
- Location:
/etc/docker/daemon.jsonon Unraid host - Inputs: none (static configuration)
- Outputs: Injects
nameserver 172.17.0.1into/etc/resolv.confof all new bridge containers - Key design choices:
- Single field
{"dns": ["172.17.0.1"]}— minimal change, system-wide effect - Requires Docker daemon restart; applies to all subsequently created containers
- Must be persisted in
/boot/config/go(Unraid wipes/etc/on reboot)
- Single field
iptables DNAT Rules
- Purpose: Enables host-networked containers (which don't have bridge gateway) to reach bridge-networked services if needed; routes inbound traffic correctly
- Location: Applied at host level; persisted via
/boot/config/go - Inputs: Packets from host-networked containers destined for bridge container IPs
- Outputs: Rewritten destination IPs for correct routing
- Key design choices:
- Required only for host-networked containers that need to reach bridge containers (inverse of the main DNS problem)
- Must be re-applied on each boot via
/boot/config/go
/boot/config/go (Persistence Layer)
- Purpose: Ensures all runtime configuration survives Unraid reboots
- Location:
/boot/config/goon the Unraid flash drive - Inputs: none (startup script)
- Outputs: Writes daemon.json, applies iptables rules, restarts Docker daemon if needed
- Key design choices:
- This is the canonical Unraid persistence mechanism — no alternative survives reboots
- Script must be idempotent (safe to run multiple times in case of partial failures)
- Order matters: daemon.json must be written before Docker daemon starts (or before daemon restart)
Interaction Flows
Flow 1: Normal DNS resolution (bridge container → internal service)
Container → /etc/resolv.conf (nameserver 172.17.0.1) → Technitium at 172.17.0.1:53 → Internal DNS response (~2ms)
Flow 2: ACME certificate renewal (Traefik → Let's Encrypt)
Traefik ACME module → resolvers=["1.1.1.1:53"] → Cloudflare DNS → Resolves acme-v02.api.letsencrypt.org → Let's Encrypt HTTPS endpoint → Certificate
Note: Technitium is NOT in this path. The circular dependency is broken.
Flow 3: Host-networked container needing bridge service (via iptables DNAT)
Host-net container → iptables DNAT rule → Rewritten to bridge container IP → Bridge service
Flow 4: Boot sequence
Unraid starts → /boot/config/go executes → daemon.json written → iptables rules applied → Docker daemon (re)started → Containers start with correct DNS