ZFS boot environments + jails will do 90% of what you're using Docker for, and the other 10% is the part you don't actually need
Published by RodHat

I was wrong about containers for about six years. I said, loudly, to anyone in earshot, that jails
already solved this and Docker was reinventing chroot with a marketing budget. That was half right
and half stubborn — the OCI image format and the ecosystem around it (registries, orchestration,
the whole docker build → docker push → k8s pull pipeline) solved a real distribution problem
that jails never addressed. I’ll give ground on that.
What I won’t give ground on: for a huge fraction of workloads — a single service, on one box or a handful of boxes, where you’re not shipping images to a fleet of strangers — jails on ZFS give you tighter isolation with less machinery, and most people running Docker for that use case are paying a complexity tax for a distribution problem they don’t actually have.
The setup
Every jail gets its own ZFS dataset. That’s the whole trick, and it’s why this pairs so well with ZFS specifically instead of UFS:
zfs create zroot/jails
zfs create zroot/jails/myapp
Base the jail off a clean userland snapshot instead of re-extracting a tarball every time:
zfs snapshot zroot/jails/base@clean
zfs clone zroot/jails/base@clean zroot/jails/myapp
zfs clone gives you a copy-on-write jail root that costs you essentially zero disk until it
diverges from the base. That’s your “image layer” story, and it’s arguably cleaner than OCI layers
because it’s the actual filesystem doing the work, not a union-mount abstraction on top of it.
The jail config
/etc/jail.conf:
myapp {
host.hostname = "myapp.internal";
path = "/jails/myapp";
ip4.addr = "10.0.0.12";
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
mount.devfs;
allow.raw_sockets = 0;
enforce_statfs = 2;
}
enforce_statfs = 2 is the line most people skip and shouldn’t — it hides the host’s actual
filesystem structure from the jail entirely, so df inside the jail shows only what’s mounted for
it. That’s a real isolation boundary at the kernel level, not a namespace convention userland
processes are trusting each other to respect.
The part that actually beats containers: boot environments as instant rollback
This is the trick that made me stop being smug about jails in the abstract and start actually recommending the workflow. Before you push a change into a jail, snapshot it:
zfs snapshot zroot/jails/myapp@pre-deploy-2026-07-23
Deploy breaks something? You’re not debugging a broken container image or rebuilding from a Dockerfile you hope is still reproducible:
zfs rollback zroot/jails/myapp@pre-deploy-2026-07-23
service jail restart myapp
That’s an atomic, filesystem-level rollback of the entire jail root, in the time it takes ZFS to flip a pointer. No image rebuild, no registry round-trip, no “well it worked in the previous image tag, let me go find it.”
Where I’ll actually concede the point
If you need to ship the same artifact to a hundred heterogeneous machines you don’t control, or you need the orchestration ecosystem — service mesh, autoscaling, the whole Kubernetes universe — jails alone don’t give you that story, and pretending otherwise is exactly the stubbornness I copped to above. Use the right tool. I just want the “jails are obsolete” crowd to sit with the fact that for a single-tenant service on hardware you own, this workflow is faster, lighter, and gives you a rollback primitive most container setups have to bolt on separately.
If you’re the one who has to own this decision instead of just having opinions about it in a Slack thread, The Practice of System and Network Administration is still the best book I know for reasoning about operational tradeoffs like this one without reaching for whatever’s trendy.