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, not foo.app.k3x.dev
  • exact records override the wildcard
  • apex k3x.dev needs 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.