Skip to content

Networking

Two separate overlay networks do two separate jobs here — easy to conflate, worth keeping distinct:

  • Tailscale connects the three hosts to each other.
  • Cloudflare (Tunnel, Access, DNS, Pages) connects the outside world to Lana — nothing else.

All three hosts share one tailnet:

Tailscale name Host Tailscale IP
vps1 Lana 100.115.215.23
krieger Krieger 100.90.162.24
pam Pam 100.109.83.48

vps1 (Lana) also offers itself as a Tailscale exit node. This is what lets Lana’s wake-proxy containers reach Krieger’s Ollama/ComfyUI over a private, encrypted path with no home-network port forwarding — see Krieger.

There are a couple of other tailnet members (a phone, this Mac) not relevant to the server-side stack.

Nothing on Lana has a port directly exposed to the internet. Public traffic reaches Docker-hosted apps through one path only:

Browser → Cloudflare (DNS + Access) → Cloudflare Tunnel → cloudflared on Lana → app container

DNS. The spelvin.dev zone lives in Cloudflare. Every app hostname (git.spelvin.dev, roadmap.spelvin.dev, etc.) is a proxied CNAME to <tunnel-id>.cfargotunnel.com — never an A record pointing at Lana’s real IP.

Tunnel. One named Cloudflare Tunnel (confusingly named flowviz in the dashboard, after whichever app got tunneled first — it now carries every migrated app, not just FlowViz) runs as a cloudflared container on Lana, outbound-only. Its ingress config is a flat list of hostname → internal service:port rules, e.g. git.spelvin.dev → http://gitea:3000, terminated by a catch-all 404. This config lives in Cloudflare’s dashboard/API, not as a file in vps-infra.

Access. Sits in front of the Tunnel (and, separately, in front of Cloudflare Pages sites when needed) as the actual login gate. Two patterns are in use:

  • self_hosted Access Application — gates a hostname outright; nothing reaches the app without passing Access’s login page first. Used for apps with no native SSO of their own (FlowViz, Windmill).
  • saas Access Application — Access acts as an OIDC identity provider that the app itself talks to, so the app has its own login page but delegates identity to Access rather than maintaining its own user database. Used for apps with native OIDC support (Gitea, Outline, LiteLLM, Open WebUI, Leantime).

Every Access policy on personal-use apps allows exactly one identity (Sign in with Apple via private relay) — consistent across FlowViz, LiteLLM, Windmill, and the rest of the single-user apps.

Pages. spelvin.dev and nothingwilts.com bypass all of the above entirely — they’re static Astro sites deployed straight to Cloudflare Pages by a Gitea Actions workflow (wrangler pages deploy) in each site’s own repo, with a custom domain attached via the same API token. No container, no Tunnel entry, no origin on Lana at all.

The recipe, in order:

  1. Deploy the container on Lana (vps-infra/<app>/docker-compose.yml), on the shared web Docker network so cloudflared can reach it by container name.
  2. Add a DNS CNAME: <hostname>.spelvin.dev → <tunnel-id>.cfargotunnel.com, proxied.
  3. Add an ingress rule to the Tunnel config: <hostname> → http://<container>:<port>.
  4. Create a self_hosted Access Application for the hostname, with the standard single-identity allow policy (or a saas one instead, if the app supports native OIDC and you’d rather avoid a double login).

Static sites skip steps 2–4 in favor of the Pages custom-domain flow, plus an Access Application on the Pages hostname if it needs gating too.