Home Infrastructure as Debian Packages
I have half a dozen Linux machines at home: a primary server, a cloud VM, three desktops, two laptops, and the occasional test VM. For a long time, configuration was a mixture of manual steps, shell scripts, and remembered knowledge. That worked fine until it did not, when setting up a new machine meant either repeating all those steps from memory or reverse-engineering what the others had installed.
The fix I landed on was to stop treating home infrastructure as something you configure and start treating it as something you deploy. Every configuration decision is now a Debian package. New machine? sudo apt install dean-network-client. Done.
The package model Link to heading
Each package owns exactly one concern: Tailscale VPN configuration, the NFS home directory mount, Syncthing sync for laptops, SMTP relay so every machine can send email, SSH key provisioning, preventing workstations from suspending during active SSH sessions. Concerns stay separate. A package that configures Tailscale does not also touch the NFS mounts.
Most packages use config-package-dev, which handles the common case of modifying a file owned by another package. It tracks what was there before, applies the change, and knows how to undo it on removal. For anything more complex, a postinst script handles the setup and postrm handles the teardown.
Metapackages pull the right set of concerns together for each machine type:
| Package | Installs on |
|---|---|
dean-network-common |
Everything: Tailscale, SSH keys, SMTP relay, update notifications, centralised user accounts |
dean-network-client |
Desktops: NFS home mount, Chrome policy |
dean-network-laptop |
Laptops: Syncthing sync, autofs, SSHFS |
dean-network-server |
Servers: NFS exports, APT repo serving, backup config |
A new machine gets dean-network-client or dean-network-laptop. The metapackage pulls in dean-network-common and everything else that belongs on that machine type. An apt upgrade across the fleet picks up any change I have packaged since the last run.
All packages use the same versioning scheme: YY.M.B, with the build number sequential within the month. The version goes up whenever anything changes. There is no question of whether a change is minor or major. The number exists so apt upgrade knows something changed, not to communicate its significance.
Every configuration decision is now a Debian package.
Two PPAs, one bootstrap Link to heading
There are two package repositories: a bootstrap PPA hosted on a cloud VM, and the main config PPA hosted on the primary server. The bootstrap PPA exists because the main PPA is only reachable over Tailscale, and Tailscale itself needs to be installed before a machine can reach it. The five bootstrap packages handle that chicken-and-egg problem: Tailscale setup and the APT source configuration for the main PPA.
The bootstrap PPA is served publicly over HTTPS with basic auth. Getting a new machine onto the network is a single curl command that runs the bootstrap script, which configures APT authentication, installs the GPG key, adds the source, and runs apt install for the appropriate metapackage.
curl -fsSL "https://USER:${BOOTSTRAP_PASSWORD}@bootstrap.example.com/deploy.sh" | sudo -E bash
After that, the machine is Tailscale-connected, has the main PPA configured, and can install everything else.
What this solves Link to heading
The main benefit is not provisioning speed, even though that is faster. It is that the state of every machine is auditable and reproducible. I can look at which packages are installed on a machine and know exactly what was configured and when. I can look at the changelog for a package and know what changed and why. If something is broken, I can remove the package and the change is undone cleanly.
The state of every machine is auditable and reproducible.
It also forces decisions to be made once. Previously, if I wanted to change the SMTP relay configuration, I would edit it on each machine separately, probably introducing small inconsistencies along the way. Now I change the package, bump the version, publish it, and the next apt upgrade propagates it everywhere.
The third thing it gives is documentation. Writing a proper control file and changelog entry is a small overhead, but it means every change has at least a sentence of context explaining what it does. That documentation is often for my future self six months later wondering why something is configured a particular way.
Tradeoffs Link to heading
There are two genuine limitations. The first is overhead: changing one line in a config file means editing the package source, bumping the version, building, publishing, and running apt upgrade. The build and publish cycle is scripted, so in practice it is not many steps. But there are more steps than editing a file in place, and that cost is real. The tradeoff is worth it for configuration that should be consistent across machines and tracked over time. For something genuinely ephemeral or experimental, packaging it would be the wrong choice.
The second is that some things resist packaging well. Configuration that is inherently per-machine (hostnames, network interface names, machine-specific service parameters) lives in debconf or in separate files outside the packages. The packages handle the common case; the per-machine exceptions are handled separately.
The result Link to heading
Running apt list --installed | grep dean-network on any machine gives a complete picture of what the network has decided that machine should be. Running apt upgrade keeps it current. Adding a new machine takes a few minutes rather than an afternoon.
It is more infrastructure than a home network strictly needs. But it is the kind of infrastructure that pays for itself the first time you need to add a machine, recover from a failure, or remember why something was configured a particular way.