Until recently, my public-facing services were spread across two very different places. Mastodon ran on its own dedicated Linux VM, while everything else — this website, a handful of tools I use for myself, and a few smaller things — lived on Uberspace. Two places to patch, two places to reason about when something broke, two mental models to keep in my head. That split had been bothering me for a while, and this summer I finally did something about it: everything now runs on a single FreeBSD host, with each service isolated in its own jail managed by BastilleBSD.

Why move at all

Uberspace is a solid shared-hosting product, and I don’t want to badmouth it. But a few things kept adding friction. New PHP releases took far longer to show up there than I would have liked, which meant some of my tools sat on outdated versions for longer than felt comfortable. And Uberspace still builds around Apache as the primary webserver, rather than something like nginx or Caddy that I’d generally reach for today.

Neither of those is a dealbreaker on its own. But they were the visible tip of a longer list of small annoyances that had built up over time. One that stuck with me: Uberspace puts more weight on consistently addressing all users with female-gendered language than on staying technically current. In my opinion, that’s time misallocated.

Combine that with running a second, entirely separate VM just for Mastodon, and it was clear I was maintaining more infrastructure, in more places, than I actually wanted to.

Why FreeBSD and jails

Once I decided to consolidate, the question was what to consolidate onto. I didn’t want a pile of Docker containers on a generic Linux box, and I didn’t want a stack of full VMs either — both felt heavier than the problem called for. What I actually needed was strong isolation between services that all happen to live on the same physical machine, without the overhead of virtualizing an entire kernel per service.

FreeBSD jails are a good fit for exactly that. They’ve been part of the OS for decades, isolation is a first-class concept rather than something bolted on, and the surrounding pieces — the network stack, ZFS underneath for storage — are mature and boring in the best way. Boring is what I want from infrastructure I depend on every day.

ZFS in particular, and boot environments built on top of it, matter a lot to me here. I can snapshot a known-good state of the machine and zfs send it elsewhere for a consistent backup — one that can be restored instantly if something goes wrong, rather than a slow file-by-file restore.

This also wasn’t my first time trusting FreeBSD with something important. My home network already runs on OPNsense, which is FreeBSD under the hood, handling VLAN segmentation, RADIUS authentication for WiFi, and a VPN service that works around the CGNAT my ISP, Deutsche Glasfaser, forces on me. It had already earned my trust before this migration — extending that to my public-facing services felt like the natural next step rather than a leap of faith.

BastilleBSD as the glue

Raw jails are managed through jail.conf and a handful of low-level tools, which is fine but not much fun to operate day to day. BastilleBSD sits on top of that: it gives you templates to bootstrap new jails from, and straightforward commands for the lifecycle operations you actually perform — create, start, stop, snapshot, destroy. It’s the layer that turns “jails” from a kernel feature into something I can actually operate without re-deriving the same jail.conf boilerplate every time.

That’s what made jails practical to use for this many services at once, rather than just one.

On top of plain Bastille, I wrote a small set of helper scripts that automate recreating a jail from scratch and then applying the right Bastille template to install the actual software — so bringing a service back to a known state is a single command rather than a sequence I’d have to remember. Under the hood, Bastille mounts the base image into each jail with nullfs, so jails sharing a release only need one copy of the base system on disk. Right now every jail starts from a plain 15.1-RELEASE template; at some point I’d like to build my own base template that already bundles the nginx+php-fpm stack, so services don’t each have to install it themselves on top.

The services, jailed

Each service now gets its own jail:

  • This website — the Hugo site you’re reading right now.
  • Mastodon — moved off its dedicated Linux VM into a jail of its own.
  • Matomo — the analytics installation for this site.
  • An internal mail relay — forwards outbound mail to Google Workspace.
  • Part-DB — where I track my electronics component inventory.
  • An internal tool for managing multichannel social postings — still fairly new, and probably worth its own post once it’s matured a bit.

Nothing here shares a jail with anything else. If one service needs attention — an update, a restart, a config change — it doesn’t touch the others.

Where this leaves things

One host, one set of jails, one mental model instead of two. Updates happen per service instead of being constrained by what a shared-hosting provider decides to support. The Mastodon VM is gone, folded into the same host as everything else.

One side effect I didn’t expect going in: overall RAM usage went down. FreeBSD seems to run with a noticeably lower base memory footprint than Linux, even with ZFS in the mix — jails add far less overhead per service than separate VMs did.

There’s more to say about the social-posting tool mentioned above — that’ll be its own post once it’s in better shape.