Running an OCI Container on FreeBSD with Podman and ocijail
- 8 minsPost 0 laid out the question: can FreeBSD’s own primitives handle container orchestration? Before getting anywhere near that, I needed a container running. This post covers the first working setup with Podman and ocijail on FreeBSD.
If you don’t have a FreeBSD box yet, start with the headless VM setup guide.
Install the Tooling
The install itself is short:
sudo pkg install -y ocijail podman buildah
The packages install:
- ocijail 0.4.0: the OCI-compatible runtime that creates jails.
- Podman 5.7.1: container lifecycle management.
- Buildah 1.42.2: OCI image builder.
Plus: conmon (container monitor), containernetworking-plugins (CNI for FreeBSD, uses pf), containers-common (shared config including storage.conf and registries.conf).
Both Podman and Buildah are marked “experimental, for evaluation and testing purposes” on FreeBSD. That label matches the current state of the tooling.
Before the First Container
The pkg install output already points to the pieces that need to be configured. These are the ones that mattered on a fresh system.
1. ZFS Dataset for Container Storage
sudo zfs create -o mountpoint=/var/db/containers zroot/containers
Podman’s storage driver on FreeBSD defaults to ZFS (configured in /usr/local/etc/containers/storage.conf). Each image layer becomes a separate ZFS dataset. When you pull an image, Podman creates ZFS clones for each layer.
2. fdescfs for conmon
conmon (the container monitor process) needs /dev/fd to properly support restart policies:
sudo mount -t fdescfs fdesc /dev/fd
echo "fdesc /dev/fd fdescfs rw 0 0" | sudo tee -a /etc/fstab
Without this, containers work but --restart=always won’t.
3. pf Firewall for Container NAT
Container networking on FreeBSD uses pf for NAT. The containernetworking-plugins package ships a sample config:
sudo cp /usr/local/etc/containers/pf.conf.sample /etc/pf.conf
Edit /etc/pf.conf and change the interface name from ix0 to your actual interface (on a VM, probably vtnet0):
v4egress_if = "vtnet0"
v6egress_if = "vtnet0"
Enable and start pf:
sudo sysrc pf_enable=YES
sudo service pf start
When a container starts, its IP gets added to the <cni-nat> pf table automatically. The NAT rules translate container traffic through the host’s egress interface. You don’t need to configure individual rules per container.
4. IP Forwarding
The FreeBSD cloud image already has this enabled. If you’re on a manual install, check:
sysctl net.inet.ip.forwarding
If it says 0:
sudo sysctl net.inet.ip.forwarding=1
sudo sysrc gateway_enable=YES
Hello World
sudo podman run --rm quay.io/dougrabson/hello
!... Hello Podman World ...!
.--"--.
/ - - \
/ (O) (O) \
~~~| -=(,Y,)=- |
.---. /` \ |~~
~/ o o \~~~~.----. ~~
| =(X)= |~ / (O (O) \
~~~~~~~ ~| =(Y_)=- |
~~~~ ~~~| U |~~
Project: https://github.com/containers/podman
Website: https://podman.io
That image comes from Doug Rabson’s registry (he’s the ocijail author). Podman pulled it, created a jail through ocijail, ran the hello binary, and removed the jail again on exit.
Everything runs as root. Rootless Podman is not available on FreeBSD yet: it’s a known gap that the Foundation has documented.
FreeBSD OCI Images
FreeBSD ships official OCI images on Docker Hub. The tag naming is easy to get wrong:
| Image | Tag | Size | What’s in it |
|---|---|---|---|
freebsd/freebsd-static | 15.0 | ~5 MB | Statically linked binaries only |
freebsd/freebsd-dynamic | 15.0 | ~16 MB | Dynamic libraries |
freebsd/freebsd-runtime | 15.0 | 34 MB | Minimal runtime |
freebsd/freebsd-notoolchain | 15.0 | ~280 MB | Full userland minus compiler |
freebsd/freebsd-toolchain | 15.0 | ~800 MB | Full userland + compiler |
The tag is 15.0, not 15.0-RELEASE. If you use 15.0-RELEASE, you get a cryptic “manifest unknown” error.
Let’s run a real FreeBSD container:
$ sudo podman run --rm docker.io/freebsd/freebsd-runtime:15.0 freebsd-version
15.0-RELEASE
And check the kernel from inside:
$ sudo podman run --rm docker.io/freebsd/freebsd-runtime:15.0 uname -a
FreeBSD 8c79045701db 15.0-RELEASE FreeBSD 15.0-RELEASE releng/15.0-n280995-7aedc8de6446 GENERIC amd64
That is a FreeBSD 15.0 userspace running inside a jail created through the OCI stack.
Looking at the Result
Run a container in the background:
$ sudo podman run -d --name test-jail docker.io/freebsd/freebsd-runtime:15.0 sleep 300
Now check it from Podman and from the host:
$ sudo podman ps
CONTAINER ID IMAGE COMMAND NAMES
498077d3948c docker.io/freebsd/freebsd-runtime:15.0 sleep 300 test-jail
$ sudo jls
JID IP Address Hostname Path
5 498077d3948c /var/db/containers/storage/zfs/graph/e005dd...
The Podman container shows up directly in jls. The hostname matches the container ID, and the path points into the ZFS-backed container storage. If you run zfs list -r zroot/containers, you’ll see datasets for the image layers as well.
The networking:
$ sudo podman exec test-jail ifconfig eth0
eth0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP>
ether 58:9c:fc:10:df:14
inet 10.88.0.6 netmask 0xffff0000 broadcast 10.88.255.255
groups: epair
Each container gets its own VNET network stack with an epair interface. eth0 inside the container maps to an epair on the host side. pf handles NAT from the container subnet (10.88.0.0/16) to the outside.
Running a Real Service: Nginx in a Jail
The freebsd-runtime image is too minimal for real testing because it does not include pkg or basic DNS tools. For anything practical, freebsd-notoolchain is easier to work with:
sudo podman run -d --name nginx-jail \
docker.io/freebsd/freebsd-notoolchain:15.0 \
sh -c 'ASSUME_ALWAYS_YES=yes pkg install -y nginx && \
echo "FreeBSD jail-based container serving via OCI" \
> /usr/local/www/nginx/index.html && \
nginx -g "daemon off;"'
Wait about 15 seconds for pkg to install nginx, then:
$ sudo jls
JID IP Address Hostname Path
7 f9a6ce316139 /var/db/containers/storage/zfs/graph/6058e5...
$ NGINX_IP=$(sudo podman inspect nginx-jail --format '{{.NetworkSettings.IPAddress}}')
$ fetch -qo- http://$NGINX_IP
FreeBSD jail-based container serving via OCI
At that point nginx is running inside a jail created by Podman and ocijail, with storage on ZFS and networking handled through pf.
Stack Layout
The stack looks like this:
Podman CLI
└── ocijail (OCI runtime)
└── jail(2) system call
├── Isolation: jail with separate root filesystem
├── Storage: ZFS dataset (zroot/containers/...)
├── Network: VNET + epair interface
│ └── pf NAT (10.88.0.0/16 → vtnet0)
└── Monitor: conmon (restart policy, logging)
Podman speaks OCI. ocijail translates OCI operations into jail operations. The jail gets storage from ZFS, network connectivity through VNET and pf, and conmon handles monitoring and restart behavior.
The stack here is Podman, ocijail, and the FreeBSD kernel, without a Docker daemon or containerd in the middle.
Watch Out
More gotchas, on top of the VM setup ones:
The
runtimeimage is too minimal for real work. Nopkg, nodrill, nohost, nogetent. DNS lookups fail silently because the resolver infrastructure is incomplete. Usefreebsd-notoolchainfor testing: it’s 280 MB but has a full userland.Image tags are
15.0, not15.0-RELEASE. Every FreeBSD user will try15.0-RELEASEfirst (because that’s whatfreebsd-versionprints). The error message (“manifest unknown”) doesn’t tell you it’s a tag problem. Save yourself 5 minutes: the tag matches the release number without the-RELEASEsuffix.Both Podman and Buildah are experimental. The pkg install messages say it explicitly: “should be used for evaluation and testing purposes only.” Take that at face value and expect rough edges.
What’s Next
At this point there is a working container with network access and a simple service on top of it. What is still missing is everything beyond a single host and a single container. The next posts tackle those parts:
- Networking: Container networking on FreeBSD - IP connectivity, port forwarding, pods, and why DNS service discovery doesn’t work out of the box with CNI.
- Native tools: Bastille, Pot, and the Nomad stack - FreeBSD-native jail tools that solve the networking gaps without CNI. Bastille for single-node, Pot+Nomad+Consul for orchestration.
Next up is container-to-container networking.
Sources and references:
- ocijail on GitHub
- Podman on FreeBSD
- FreeBSD OCI images
- containernetworking-plugins for FreeBSD
- Podman testing on FreeBSD
- containers-common ZFS storage
Keep the Lab Running
This series runs on real hardware and real hours of debugging. If it saved you from trial-and-error, consider keeping the test nodes running.
Most readers scroll past. Less than 3% of readers contribute to keeping independent technical content free and accessible.