Reference
One card per part of the build. Each keeps the vendor's steps apart from what this build changes, and lists where it breaks against the rest.
Metal and OS
Part 1- Hardware note: Pinova P2 M.2 slots
The two M.2 slots in this mini PC are not the same, and the case opens with a latch the manual doesn't mention.
- Debian 13, headless, on two disks
A minimal Debian install with no desktop, on the OS disk only, leaving the data disk untouched for later.
- Base hardening
Updates, baseline tools, key-only SSH, a UTF-8 locale, and automatic security updates on a fresh Debian box.
- Data volume
The second disk as one ext4 partition, mounted at /srv by UUID, holding stacks, container data and backups.
Reachable from anywhere, visible to no one
Part 2- Tailscale on a server
Tailscale for a stable address on any network, with MagicDNS on, key expiry off, and Tailscale SSH left out on purpose.
- UFW, tailnet only
A firewall that answers only on the Tailscale interface, with one narrow exception for containers reaching the front door.
- mosh and tmux
A connection that survives wifi changes and laptop sleep, and a shell that survives the connection.
Containers and a front door
Part 3- Docker Engine on Debian
Docker Engine and the Compose plugin on a headless Debian 13 box, with storage on a separate data volume and DNS that works behind Tailscale.
- Caddy with Cloudflare DNS-01
One HTTPS front door with real certificates, for private service names that only lead somewhere from inside the tailnet.
Source control and CI
Part 4- Forgejo
A private git host in one container, behind Caddy for the web and on its own port for git over SSH.
- Forgejo Actions runner with dind
A CI runner whose job containers reach Forgejo at the same address the runner does, from inside a nested Docker daemon.
Watching it and keeping it
Part 5- Dockge
A web UI for the compose stacks under /srv/stacks, working through the Docker socket.
- Beszel
Resource graphs for the box and its containers, from a hub and an agent that both run on host networking.
- Uptime Kuma
Up and down checks for every service, probing 127.0.0.1 from host networking, with alerts that actually arrive.
- Homepage
One landing page with a tile per service and live container status, configured from files you own.
- restic to SFTP and R2
Encrypted daily backups to a machine at home and to object storage off-site, run as root by a systemd timer, with a tested restore.
- msmtp and failure alerts
One outbound mail account for Forgejo, Uptime Kuma, and an email whenever a systemd job fails.
Every card uses the same stand-ins. Swap in your own.
devbox.example.ts.net- The box on your tailnet. example.ts.net is the sample tailnet in Tailscale's docs.
100.x.y.z- The box's tailnet address, from tailscale ip -4.
tailnet.example- A domain you own, on public DNS, for the private service names. Caddy needs a real zone for its certificates.
git.dev.tailnet.example- A service name under the *.dev wildcard record, which points at 100.x.y.z.
youruser- Your login on the box.
you@tailnet.example- Your mail address. Alerts go here.
backupuser@backup-host.local- The home machine that holds the local backup copy.