split-horizon dns
this project uses one caddy deployment and two dns answers for the same app hostnames.
public path
public cloudflare dns uses a single wildcard record that points app hostnames at the edge vps public ipv4:
*.k3x.dev A <edge-vps-public-ip>
that means new app hostnames such as vault.k3x.dev, git.k3x.dev, or immich.k3x.dev do not require new cloudflare records. dns matches the wildcard and returns the edge vps ip. caddy decides whether the hostname is actually served.
important details:
- wildcard covers one label:
app.k3x.dev, notfoo.app.k3x.dev - exact records override the wildcard
- apex
k3x.devneeds its own record if it should serve anything - cloudflare does not create per-app records; the wildcard is the record
traffic flow:
internet client
-> cloudflare wildcard dns returns edge vps public ipv4
-> edge vps public ipv4 :443
-> haproxy tcp passthrough
-> lab node tailscale ip :443
-> caddy hostNetwork pod
local / tailscale path
local dns, router dns, blocky, pihole, dnsmasq, or tailscale split dns should mirror the wildcard pattern and point the same app hostnames directly at the lab node tailscale ip:
*.k3x.dev A <lab-node-tailscale-ip>
traffic flow:
local or tailscale client
-> lab node tailscale ip :443
-> caddy hostNetwork pod
why one caddy
one caddy deployment avoids duplicate configs, duplicate cert mounts, and public/internal drift. tls certs are valid for the hostname regardless of whether the client reached caddy through the vps or directly through tailscale.
caddy exposure model
caddy uses hostNetwork: true and hostPort: 443, so it binds the lab node’s :443 directly. this is intentionally single-node-specific and keeps haproxy and split dns simple.
make sure nothing else on the node binds :443.
external-dns note
external-dns is intentionally not part of v1. the public target is always the edge vps, not a changing kubernetes load balancer. a single wildcard record removes the need to create cloudflare records per app while avoiding another controller with dns write access.
revisit external-dns only if hostnames become too dynamic or the architecture changes to expose kubernetes services directly.