System overview

«PROJECT» is a self-hosted service stack on one Hetzner dedicated server running Proxmox VE. Services run in LXC containers plus one QEMU VM (the isolated devvm — inventory). Everything is reached over Tailscale — there are no public service endpoints.

The three layers

1. Host

Hetzner dedicated baremetal («PUBLIC_IP») running Proxmox VE 9.x as node «HOSTNAME». See inventory for hardware and topology for the host network config. The host itself is joined to the tailnet — that’s the management path; the Proxmox UI is never exposed publicly.

2. Internal network

Guests live on 10.0.1.0/24 behind SDN bridge vnet0 (gateway 10.0.1.1), reaching the internet via SNAT out vmbr0 to «PUBLIC_IP». Inbound access is not NAT’d — there is no port forwarding at all.

3. Access path

All service access enters over Tailscale:

  • HTTP services → the caddy LXC (on the tailnet) terminates TLS and reverse-proxies to internal 10.0.1.x addresses.
  • git-over-SSH → the gitea LXC is itself tailnet-joined.
  • DNS → «MYDOMAIN» resolves via split-horizon: tailnet clients send those queries to technitiumdns at «TECHNITIUM_TAILNET_IP».
https://wastebin.«MYDOMAIN» — request flow

1. DNS   client split-horizon → technitium @«TECHNITIUM_TAILNET_IP»
         answers «CADDY_TAILNET_IP»
2. TLS   caddy :443 — real Let's Encrypt cert via Cloudflare DNS-01
3. proxy caddy → http://10.0.1.22:8088 (plain HTTP on the LAN side)

Invariants

These hold everywhere in the system — docs and changes should preserve them:

  • No public service exposure. No public A/AAAA records for «MYDOMAIN» services, no port forwards, no public Proxmox UI — enforced twice: the Hetzner Robot firewall drops new inbound connections at the provider edge (topology), and the host has no NAT/port-forwards anyway.
  • Single HTTP ingress. Caddy is the only reverse proxy; every HTTP service gets a «MYDOMAIN» subdomain pointing at «CADDY_TAILNET_IP».
  • Real TLS certs despite zero public records — Let’s Encrypt via Cloudflare DNS-01 challenges.
  • Small tailnet footprint. Only machines that need to be on the tailnet are joined (host, caddy, technitiumdns, gitea). No subnet routing.
  • Helper scripts by default. LXCs come from Proxmox community helper-scripts unless there’s a reason not to.
  • SSO where supported. Pocket-ID (backed by LLDAP) is the identity provider.

Layer-by-layer detail

AreaDoc
Host hardware + guest tableinventory
Bridges, subnet, NATtopology
DNS zones, records, split-horizondns
Tailnet membership + sharing modeltailscale
Storage pools + Storage Boxstorage
Every service + how it’s reachedcatalog
Auth stackpocket-id, lldap

Design history

Authentik → replaced by Pocket-ID. Rootless-podman service VMs → experimented with, not adopted. See decisions.