Jan Tuomi

A home server journey, part 5: Ansible
(2025-12-26) [note]

This is the fifth episode in a series where I set up a FreeBSD home server, explaining all the steps, problems and solutions along the way. This time we're provisioning the server with Ansible.

Check out episodes 1, 2, 3, 4.

Automation

Ansible is a tool for running a bunch of Python scripts, or "tasks", in a kind of declarative way. It's commonly used for provisioning servers.

My ansible configuration is publicly available on my GitHub. I update it regularly.
🌐 jantuomi/ansible-freebsd-home-server

I have only one server, and I'm planning to have it for a long time. Why would I need automated provisioning? It seems like an extra step when I could just provision everything manually. There is no need to synchronise multiple hosts or anything like that.

I view this kind of automation as executable documentation, or readable instructions for both the computer and the human. If I define the server in terms of these Ansible tasks, I can (in theory) reconstruct what's been done with the server just by reading the Ansible playbook.

To be reliable, this necessitates that everything is defined in Ansible. Which happens to be very difficult to actually do.

Project structure

There are many ways to structure an Ansible project. I picked one that I consider to be simple:

This is my first non-trivial Ansible project, so I wouldn't be surprised if I have some anti-patterns in there.

The playbooks are designed to be fully idempotent: I can run the playbook again after the first run and expect the system to be in the same state. This is very important, since I want to be able to fearlessly run any playbook at any time and expect a success, or at least a helpful failure that doesn't compromise basic operation of the host.

The only host in the Ansible inventory is my server. I run the playbook like this from my development laptop:

$ ansible-playbook -i inventory playbook.yml

If I want to only update the ingress jail, I run:

$ ansible-playbook -i inventory playbook.yml -t jail_ingress

Separation of responsibilities

Let's borrow and tweak the standard cloud infrastructure stack diagram that shows what parts of the stack are handled by the cloud provider and what parts are handled by the customer.

My current approach is to use Ansible for the "core" parts of server administration, installing packages, defining service jails, etc:

A stack model depicting the software layers in my server

Some notes about this setup:

Why not Chef, Salt, etc?

Ansible is very simple. It's pretty much a framework to write idempotent shell scripts without having to write shell scripts. Or Python scripts, whichever way you want to look at it.

It's agentless. I don't have to run a service on the FreeBSD host to use Ansible. It uses SSH as transport and runs the tasks with Python, both of which are software that I expect to be present on any *nix server.

A snippet of Ansible playbook installing PostgreSQL in the "postgres" jail

How about containers?

I'd image that most new server setups in 2025 are designed with container workloads in mind. This one is too, kinda.

I isolate most services with FreeBSD jails, which are similar to OCI containers. Processes are isolated from other jails and the host automatically. Virtual networking (vnet) isolates the network. ZFS snapshots and clones provide thin jail filesystems that have a small storage footprint. Jails share the host kernel.

Many OCI container concepts translate well to jails, but jails are somewhat more persistent. Conventionally, jail filesystems are not ephemeral and survive jail restarts.

Only ssh, bash, vim and some other packages are installed on the host directly. Everything else is 🔐 incarcerated 🔐.

A list of running jails on the server

FreeBSD OCI container support

FreeBSD was recently welcomed into the prestigious club of platforms that can run OCI containers. This was achieved by using jails as the container runtime and podman as container manager.

This pretty much means that we can use Dockerfiles/Containerfiles to define our jails. Existing Containerfiles cannot be used directly, since they are built on Linux base images, but they can be used as a starting point for building new, FreeBSD-based images.

While this sounds very cool and promising, I want to let the implementation mature a bit before building everything on it.

I tried installing the podman-suite package and following the instructions, and promptly broke all of my networking, so yeah 😑

I'm sure OCI containers will be a valid and respectable approach to containerization on FreeBSD, after the kinks have been ironed out.

To be continued

In the next part(s) we will be looking at the specifics of the jail setup, e.g. how to create jails using jail.conf.d scripts.