Skip to content
Light Dark

Running an OCI Container on FreeBSD with Podman and ocijail

- 8 mins

Post 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:

ImageTagSizeWhat’s in it
freebsd/freebsd-static15.0~5 MBStatically linked binaries only
freebsd/freebsd-dynamic15.0~16 MBDynamic libraries
freebsd/freebsd-runtime15.034 MBMinimal runtime
freebsd/freebsd-notoolchain15.0~280 MBFull userland minus compiler
freebsd/freebsd-toolchain15.0~800 MBFull 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:

  1. The runtime image is too minimal for real work. No pkg, no drill, no host, no getent. DNS lookups fail silently because the resolver infrastructure is incomplete. Use freebsd-notoolchain for testing: it’s 280 MB but has a full userland.

  2. Image tags are 15.0, not 15.0-RELEASE. Every FreeBSD user will try 15.0-RELEASE first (because that’s what freebsd-version prints). 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 -RELEASE suffix.

  3. 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:

Antenore Gatta

Antenore Gatta

A proud and busy Hacker, Father and Kyndrol

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.

Post comment

Markdown is allowed, HTML is not. All comments are moderated.