Chapter 18. OCI Containers

18.1. Synopsis

The Open Container Initiative, commonly referred to as OCI, provides a vendor and OS-agnostic way to describe, distribute, and run containers. The OCI specifications provide these in a way that can be used on many different operating systems, including FreeBSD. The underlying virtualization technology is still FreeBSD jails, with the same feature set, but OCI tooling enables additional ways of working, and constructing, container-based workloads. 14.3-RELEASE and upwards, including snapshots, now include OCI-compatible images, and the Podman toolkit on FreeBSD is ready to use them, on both amd64 and arm64 architectures.

For FreeBSD users familiar with jails, there is a loose analogy:

  • FreeBSD’s base.txz tarball is an example of a container image.

  • the jail.conf file describes the desired container properties, or Containerfile.

  • use the jail .. command to run a container, given a filesystem path, with the podman suite of tools.

By importing this container stack, FreeBSD users both benefit from common tooling, but also enjoy wide support across public and private container registries, and container-specific tooling and services. In the aarch64 and amd64 download directories, you’ll see official OCI-format images. The naming may be a little confusing at first, but should make sense once you start using them.

The same images are also available through common public container registries, including Docker Hub, and Github Container Registry, but for the strongest chain of trust, you should fetch your image directly from Official FreeBSD Releases, and import them to your local system.

Each image comprises a subset of a standard FreeBSD base.txz release tarball, for various use cases, and the usual FreeBSD CHECKSUM.* files, that can be verified against the PGP-signed release announcement. This gives a very strong chain of provenance, directly verifiable from the FreeBSD release team.

CHECKSUM.SHA256
CHECKSUM.SHA512
FreeBSD-15.1-RELEASE-amd64-container-image-static.txz
FreeBSD-15.1-RELEASE-amd64-container-image-dynamic.txz
FreeBSD-15.1-RELEASE-amd64-container-image-runtime.txz
FreeBSD-15.1-RELEASE-amd64-container-image-notoolchain.txz
FreeBSD-15.1-RELEASE-amd64-container-image-toolchain.txz

18.2. Container Image Naming and Tagging

18.2.1. Introduction

FreeBSD’s official container images are built from base system packages, themselves built during the FreeBSD release process, and published to public registries. This guide explains how images are named and tagged, helping you choose the right image for your needs, and ensuring you understand the implications of each choice, as new images are published, and in some cases, mutable tags are amended. This naming convention aligns with the new base system package naming scheme, giving clear provenance between container images and the corresponding FreeBSD releases.

18.2.2. Available Image Types

Five types of FreeBSD container images are available, for amd64 and aarch64 architectures.

18.2.2.1. freebsd-static

The static image is intended as a base image, for a workload which is entirely statically linked. It contains no libraries, nor binaries, just the supporting files that most applications of this nature require.

  • public TLS certificates

  • minimal password file

  • minimal termcap

  • timezone database

Note that it has, by design, a lean footprint, to make a security compromise of the container less useful to the attacker. There’s no UNIX shell, no command-line tools, no dynamic libraries, nor package manager. It is the smallest image provided by the FreeBSD project.

18.2.2.2. freebsd-dynamic

The dynamic image uses the static image as a parent layer, and supports using shared libraries, including libc. Most FreeBSD software should run without issue with this image, with minor changes. It doesn’t have a shell, rc system, nor a package manager. These limitations are additional security, making it awkward for attackers in a compromised container to move laterally, or make use of tools that were never installed.

18.2.2.3. freebsd-runtime

Again, runtime builds on the preceding dynamic layer, and finally adds the minimum that a user would expect - a UNIX shell, rc system, and the package manager. It is the ideal base image for porting existing applications with a minimum of changes. Users will need to include additional FreeBSD base system libraries, as well as additional packages from the Ports tree. This is the closest to a typical FreeBSD system, including the pkg(8) tool, allowing users to install, or do, almost anything, similar to a non-containerised system.

18.2.2.4. freebsd-notoolchain

This base image contains almost all tools one would expect on a typical FreeBSD system, excluding those that are directly hardware-related, and thus not generally useful within a container, and the compiler and related toolchain, as it is quite large.

18.2.2.5. freebsd-toolchain

The Toolchain base image is the sum of all preceding images, including a full compiler and toolchain. It is generally possible to compile almost any software for FreeBSD in the same way as a normal non-jailed FreeBSD system. All images follow a consistent naming pattern, derived from the FreeBSD release they are based upon.

18.2.3. Image Tag Structure

18.2.3.1. Immutable Tags

Immutable tags never change and are ideal for production systems where you need stability and predictability. These are the most common tags used by the release process. They will not change, even after FreeBSD security patches, or errata notices. They are ideal for base images for software deployments where a high degree of reproducibility is expected, even as a trade-off against more pro-active security patching.

  • major.minor (e.g., 14.4): Points to a specific FreeBSD RELEASE version

  • major.minor.pX (e.g., 14.4p1): Points to a specific patch level of a RELEASE

  • major.snapYYYYMMDDHHMMSS (e.g., 16.snap20260325035941): a snapshot build with timestamp

18.2.3.2. Mutable Tags

Mutable tags are updated over time and are ideal for production, or CI/CD systems that always want the latest updates, but still wish to align with FreeBSD ABI compatibility.

  • major (e.g., 15) will always be the latest support RELEASE version along the 15.x stable ABI

  • major.snap (e.g., 15.snap) follows the latest developer snapshot along the 15.x stable ABI train, will often be in advance of official releases, and carries a risk of breakage, incompatibility, or other unanticipated issues. This is the most mutable tag, and should be used with caution. They are exactly the same as using FreeBSD’s STABLE or CURRENT snapshots, as they use those images as their source.

18.2.4. When to Use Each Tag Type

18.2.4.1. For Production

Use immutable tags to ensure your environment stays consistent:

  • freebsd-runtime:15.0 - pinned to FreeBSD 15.0-RELEASE

When security patches are released, you must explicitly rebuild or update with a new image tag.

18.2.4.2. For Continuous Integration and Development

Use mutable tags to automatically get the latest updates:

  • freebsd-runtime:15 - always the latest official RELEASE in the 15.x series

These additional mutable tags will follow the regular published snapshots of FreeBSD STABLE and CURRENT. Use these if you want to test against what subsequent future releases of FreeBSD may contain, and accept occasional breakage or unanticipated changes.

  • freebsd-runtime:14.snap - always the latest build from 14-STABLE branch

  • freebsd-runtime:15.snap - always the latest build from 15-STABLE branch

  • freebsd-runtime:16.snap - always the latest build from 16-CURRENT branch

18.3. Official Registry Locations

Architecture-independent images can be downloaded and directly imported into your own registry from the Official FreeBSD Releases site, or alternatively, pulled from the FreeBSD project’s Docker Hub, or GitHub Container Registry.

Both Alpine Linux and the Debian project publish tagged and mutable images, using similar approaches.

18.4. Introduction to Podman

The Podman suite comprises three main tools and a number of supporting ones:

  • podman is a daemonless container engine using docker-compatible commands

  • buildah generates OCI formatted container images

  • skopeo works with remote OCI image registries

18.5. Preparing the System for Podman Containers

If running commands over SSH, or in a similar remote session, use screen(1), tmux(1), or similar to avoid losing the connection while restarting the firewall.

If ZFS is not available, switch to UFS storage and skip the ZFS commands below:

# sed -I .bak -e 's/driver = "zfs"/driver = "vfs"/' \
    /usr/local/etc/containers/storage.conf

Otherwise, create the container storage dataset:

# zfs create -o mountpoint=/var/db/containers zroot/containers
# zfs snapshot zroot/containers@empty

The File Descriptor filesystem is required:

# mount -t fdescfs fdesc /dev/fd

Podman uses FreeBSD’s packet filter to forward container ports to the host network:

# test -c /dev/pf || kldload pf
# sysctl net.pf.filter_local=1

Amend /etc/sysctl.conf and /etc/fstab as appropriate, to make these changes permanent.

18.6. Installing Podman

Only the sysutils/podman-suite meta-package is required, but if the additional emulators/qemu-user-static package is installed, it becomes possible to build or test images for other architectures such as arm64 on an amd64 host.

# pkg install -r FreeBSD -y podman-suite emulators/qemu-user-static

Integrate changes from /usr/local/etc/containers/pf.conf.sample into /etc/pf.conf, setting egress macros appropriately, then restart the firewall:

# service pf restart

The packages install a number of template configuration files, none of which need to be edited immediately. Review and amend these as needed:

# pkg list buildah podman conmon \
    ocijail containers-common \
    containernetworking-plugins \
    | grep /etc/
/usr/local/etc/containers/containers.conf.sample
/usr/local/etc/containers/policy.json.sample
/usr/local/etc/containers/registries.conf.sample
/usr/local/etc/containers/storage.conf.sample
/usr/local/etc/containers/pf.conf.sample

18.7. Importing and Running Containers

At present, all Podman containers on FreeBSD must run as root, as FreeBSD jails require this.

With the necessary tools and firewall rules in place, the officially published images can be fetched from FreeBSD Releases, for aarch64 or amd64 as appropriate.

18.7.1. Importing FreeBSD OCI Images

It is simplest to pull images directly from a public container registry, but for the strongest chain of trust, download them from FreeBSD.org directly, and verify the checksums against the PGP-signed release announcement.

# export OCIBASE=https://download.freebsd.org/releases/OCI-IMAGES/15.1-RELEASE/amd64/Latest
# podman load -i=$OCIBASE/FreeBSD-15.1-RELEASE-amd64-container-image-static.txz
Getting image source signatures
Copying blob 44defc6cee2e done   |
Copying config 5921c62e76 done   |
Writing manifest to image destination
Loaded image: localhost/freebsd-static:15.1-RELEASE-amd64

# podman load -i=$OCIBASE/FreeBSD-15.1-RELEASE-amd64-container-image-dynamic.txz
Getting image source signatures
Copying blob 6054dffc3887 done   |
Copying config 33427d59f9 done   |
Writing manifest to image destination
Loaded image: localhost/freebsd-dynamic:15.1-RELEASE-amd64

# podman load -i=$OCIBASE/FreeBSD-15.1-RELEASE-amd64-container-image-runtime.txz
Getting image source signatures
Copying blob dcbbfcb598fe done   |
Copying config 90c4936754 done   |
Writing manifest to image destination
Loaded image: localhost/freebsd-runtime:15.1-RELEASE-amd64

# podman load -i=$OCIBASE/FreeBSD-15.1-RELEASE-amd64-container-image-notoolchain.txz
Getting image source signatures
Copying blob a16c94176cc3 done   |
Copying config 086572f35c done   |
Writing manifest to image destination
Loaded image: localhost/freebsd-notoolchain:15.1-RELEASE-amd64

# podman load -i=$OCIBASE/FreeBSD-15.1-RELEASE-amd64-container-image-toolchain.txz
Getting image source signatures
Copying blob b04531b7d93d done   |
Copying config b927c11b2e done   |
Writing manifest to image destination
Loaded image: localhost/freebsd-toolchain:15.1-RELEASE-amd64

18.7.2. Listing Images

# podman images
REPOSITORY                    TAG                   IMAGE ID      CREATED       SIZE
localhost/freebsd-toolchain   15.1-RELEASE-amd64  b927c11b2e8d  13 days ago   660 MB
localhost/freebsd-notoolchain 15.1-RELEASE-amd64  086572f35cc9  13 days ago   152 MB
localhost/freebsd-runtime     15.1-RELEASE-amd64  90c493675429  13 days ago   34.1 MB
localhost/freebsd-dynamic     15.1-RELEASE-amd64  33427d59f990  13 days ago   14.5 MB
localhost/freebsd-static      15.1-RELEASE-amd64  5921c62e76c1  13 days ago   2.92 MB

Note that the image IDs are reproducible each time, and the IMAGE_ID column matches the hashes reported when importing the images.

18.7.3. Listing Layers

It is possible to show the layers that comprise an image, demonstrating how the runtime image has three components, each one a layer in its own right:

# podman image tree ghcr.io/freebsd/freebsd-runtime:14.2
Image ID: c5f3e77557a9
Tags:     [ghcr.io/freebsd/freebsd-runtime:14.2]
Size:     35.07MB
Image Layers
├── ID: cd53fb07fb66 Size: 5.449MB Top Layer of: [ghcr.io/freebsd/freebsd-static:14.2]
├── ID: a01d37f7777b Size:  10.4MB Top Layer of: [ghcr.io/freebsd/freebsd-dynamic:14.2]
└── ID: 36b0c80ca1f7 Size: 19.21MB Top Layer of: [ghcr.io/freebsd/freebsd-runtime:14.2]

18.7.4. Using Public Registries

While the most secure provenance is downloading from Official FreeBSD Releases, there are two public container registries managed by the FreeBSD Release and Cluster Admin teams. The images from both registries are identical, but provide a simpler and more container-friendly workflow.

18.7.5. Fetching Containers

Images can be fetched from a public registry using podman pull:

# podman pull ghcr.io/freebsd/freebsd-notoolchain:15.1
Trying to pull ghcr.io/freebsd/freebsd-notoolchain:15.1...
Getting image source signatures
Copying blob 711cf0d3c0ef done   |
Copying config 086572f35c done   |
Writing manifest to image destination
086572f35cc90b1a1d2b7904b63e8a849071b4ba89b15492f6da47a802372aa2

18.7.6. Running a Container

Use podman run to start a container. The -it flags allocate an interactive terminal, and --rm removes the container when it exits. If no command is specified, the image’s default entrypoint is used:

# podman run -it --rm ghcr.io/freebsd/freebsd-notoolchain:15.1
# exit

A specific command can be given as well:

# podman run -it --rm ghcr.io/freebsd/freebsd-notoolchain:15.1 uname -a
FreeBSD c0038de70651 15.1-RELEASE FreeBSD 15.1-RELEASE releng/15.1-n283562-96841ea08dcf GENERIC amd64

As the container image does not include the kernel, freebsd-version(1) shows different results for the running kernel versus the installed userland. In this example, a 15.0-RELEASE container runs on a 15.1-RELEASE host:

# podman run -it --rm ghcr.io/freebsd/freebsd-notoolchain:15.0 /bin/sh
# freebsd-version -ru
15.1-RELEASE
15.0-RELEASE
# exit

18.8. Useful Variations and Tips

Almost all of the podman run ... parameters also apply to podman build. Refer to the podman-run(1) and podman-build(1) manual pages for details.

Use the --rm flag to have ephemeral containers clean themselves up afterwards.

Use podman images -a to show all downloaded images:

# podman images -a
REPOSITORY                       TAG            IMAGE ID      CREATED      SIZE
ghcr.io/freebsd/freebsd-runtime  15.1   90c493675429  2 weeks ago  34.1 MB

Use podman ps -a to see running containers:

# podman ps -a
CONTAINER ID  IMAGE                                          COMMAND  CREATED        STATUS        NAMES
3123623bef9b  ghcr.io/freebsd/freebsd-runtime:15.1   /bin/sh  4 minutes ago  Up 4 minutes  admiring_brattain

18.8.1. Environment Variables

Podman sets a number of default environment variables in each container:

# podman run -it --rm ghcr.io/freebsd/freebsd-notoolchain:15.1 /usr/bin/env
container=podman
TERM=xterm
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/root
HOSTNAME=60bae3343e6b

The HOSTNAME is set to the container ID by default. It can be overridden with --hostname, which is useful when the application inside the container needs a stable or meaningful hostname.

Arbitrary environment variables can be passed in with --env:

# podman run --env FOO=BAR -it --rm ghcr.io/freebsd/freebsd-runtime:15.1
# echo $FOO
BAR

18.8.2. Mounting Files and Volumes

Both files and volumes can be passed into containers using --volume. For example, the runtime image does not contain which(1):

# podman run -it --rm ghcr.io/freebsd/freebsd-runtime:15.1
# which
/bin/sh: which: not found

It can be mounted into the container from the host:

# podman run -it --rm --volume /usr/bin/which:/usr/bin/which \
    ghcr.io/freebsd/freebsd-runtime:15.1
# which which
/usr/bin/which

For a lean image such as freebsd-static, mount the existing pkg-static(8) from the host to bootstrap pkg(8) inside the container. Sharing the host’s package cache via --volume /var/cache/pkg avoids re-downloading packages that have already been fetched, which is particularly useful when building multiple containers. The --no-hosts flag prevents mounting /etc/hosts, which would otherwise interfere with package upgrades.

Sharing the package cache only works when the host and container run the same FreeBSD major version, as packages are version-specific.

# podman run -it --rm \
    --env ASSUME_ALWAYS_YES=true \
    --no-hosts \
    --volume /usr/local/sbin/pkg-static:/bin/pkg-static \
    --volume /var/cache/pkg \
    ghcr.io/freebsd/freebsd-static:15.1
# pkg-static bootstrap -r FreeBSD
Bootstrapping pkg from pkg+https://pkg.FreeBSD.org/FreeBSD:14:amd64/latest, please wait...
Verifying signature with trusted certificate pkg.freebsd.org.2013102301... done
[fd343e182fa5] Installing pkg-2.1.2...
[fd343e182fa5] Extracting pkg-2.1.2: 100%

Files and directories from the host can also be mounted into containers:

# podman run -it --rm \
    --volume /etc/fstab:/etc/fstab \
    --volume /tmp:/var/tmp \
    --volume /tmp \
    ghcr.io/freebsd/freebsd-runtime:15.1

18.8.3. Inspecting Image Contents

To list all files in any image, including minimal images with no shell or tools, export the container filesystem and list its contents:

# podman export $(podman create --rm ghcr.io/freebsd/freebsd-static:15.1 /) | tar tf -

The container images do not include pkg(8) itself, but pkg-static(8) can be mounted from the host to query the installed base system packages. Use pkg info -q to list package names, or pkg info -ql to show all files owned by packages:

# podman run --rm \
    -v /usr/local/sbin/pkg-static:/pkg \
    ghcr.io/freebsd/freebsd-runtime:15.1 /pkg info -q
FreeBSD-audit-lib-15.1
FreeBSD-bzip2-lib-15.1
FreeBSD-certctl-15.1
FreeBSD-clibs-15.1
FreeBSD-fetch-15.1
FreeBSD-kerberos-lib-15.1
FreeBSD-libarchive-15.1
FreeBSD-libcasper-15.1
FreeBSD-libexecinfo-15.1
FreeBSD-libucl-15.1
FreeBSD-mtree-15.1
FreeBSD-ncurses-lib-15.1
FreeBSD-openssl-15.1
FreeBSD-openssl-lib-15.1
FreeBSD-pam-lib-15.1
FreeBSD-pkg-bootstrap-15.1
FreeBSD-rc-15.1
FreeBSD-runtime-15.1
FreeBSD-xz-lib-15.1
FreeBSD-zlib-15.1
FreeBSD-zoneinfo-15.1
FreeBSD-zstd-lib-15.1

When the host and container run different FreeBSD major versions, pkg(8) detects the ABI by inspecting /bin/sh inside the container, and will warn about the mismatch:

# podman run -it --rm \
    --env ASSUME_ALWAYS_YES=true \
    --no-hosts \
    --volume /usr/local/sbin/pkg-static:/usr/sbin/pkg \
    --volume /var/cache/pkg \
    ghcr.io/freebsd/freebsd-runtime:14.4 /bin/sh
# pkg bootstrap
pkg: Warning: Major OS version upgrade detected.  Running "pkg bootstrap -f" recommended

For images without /bin/sh, such as freebsd-dynamic, pkg-static(8) cannot detect the system ABI at all.

18.9. Networking and Name Resolution

By default, the Podman suite of tools will use mount_nullfs(8) to mount /etc/resolv.conf and a modified /etc/hosts from the jail host, as well as a .containerenv file in /var/run.

This hosts file will conflict with base system package upgrades, so it is advised to skip this during podman build runs, but leave it enabled during production deployment:

# podman run --dns 1.2.3.4 ...
# podman build --no-hosts ...

The --dns flag specifies a custom DNS server instead of mounting /etc/resolv.conf from the host. The --no-hosts flag prevents mounting /etc/hosts from the host.

18.10. Building Custom Images

Custom images can be built directly from tarballs (for example those from poudriere(8), or official release tarballs), from existing tagged images, or bootstrapped from pkg(8).

18.10.1. Building from Tarballs

A custom image can be made from any FreeBSD root filesystem tarball using podman-import(1). The tarball can be an official release base.txz, one produced by poudriere-image(8), or your own custom tarball. The final tagged image can be pushed to registries, modified, and used for further image creation.

# podman import --os freebsd \
    --arch amd64 \
    --message 'Import FreeBSD 15.1-RELEASE base.txz' \
    https://download.freebsd.org/releases/amd64/15.1-RELEASE/base.txz
Downloading from "https://download.freebsd.org/releases/amd64/15.1-RELEASE/base.txz"
Getting image source signatures
Copying blob 3768988b151c done   |
Copying config 332bec83a4 done   |
Writing manifest to image destination
sha256:332bec83a428efa6c9f7d1f8b3fbd589c5613d938dca7da5125240c89f4f3dcd

# podman image tag 332bec83a428 localhost/freebsd-base:15.1

# podman image ls
REPOSITORY              TAG   IMAGE ID      CREATED        SIZE
localhost/freebsd-base  15.1  332bec83a428  5 minutes ago  715 MB

18.10.2. Building from Containerfiles

Images can be built using a Containerfile, similar to a Dockerfile. In most cases, the freebsd-notoolchain image is the most convenient base, as it includes nearly all standard system tools. For leaner containers, start from freebsd-static, freebsd-dynamic, or freebsd-runtime as appropriate, and add only what is needed:

# Containerfile
FROM ghcr.io/freebsd/freebsd-notoolchain:15.1
RUN env ASSUME_ALWAYS_YES=yes IGNORE_OS_VERSION=yes pkg bootstrap -r FreeBSD
RUN pkg update
RUN pkg upgrade -y
RUN pkg clean -y
ENTRYPOINT /bin/sh

Build the image with podman-build(1):

# podman build --no-hosts --squash \
    -t localhost/freebsd-base:15.1 \
    -f ./Containerfile

Further images can be layered on top:

# Containerfile.webserver
FROM localhost/freebsd-base:15.1
RUN pkg install -y lang/python3
RUN pkg clean -y
ENTRYPOINT /usr/local/bin/python3 -m http.server
# podman build --no-hosts \
    -t localhost/python-www:15.1 \
    -f ./Containerfile.webserver

Run the web server in the background with --detach, and use -p to map a host port to the container port. In this example, the container listens on its default port 8000, which is mapped to port 8888 on the host:

# c=$(podman run --detach --rm -p 8888:8000 localhost/python-www:15.1)

The port mapping uses pf(4) redirect rules. These rules redirect traffic arriving over the network, so the mapped port must be accessed from another host, or via the host’s network-facing IP address:

$ fetch -v http://172.16.2.10:8888/
resolving server address: 172.16.2.10:8888
requesting http://172.16.2.10:8888/
remote size / mtime: 784 / 0
fetch.out                                              784  B 3791 kBps    00s

To access the container directly from the host, use the container’s own IP on its listening port:

# podman inspect -f '{{.NetworkSettings.IPAddress}}' $c
10.88.0.91
# fetch -v http://10.88.0.91:8000/
resolving server address: 10.88.0.91:8000
requesting http://10.88.0.91:8000/
remote size / mtime: 784 / 0
fetch.out                                              784  B   25 MBps    00s

Port mappings via -p are handled by pf(4) redirect rules and only apply to traffic arriving over the network. Traffic originating from the jail host itself to localhost or 127.0.0.1 will not be redirected. From the host, use the container’s IP address directly instead.

18.11. Advanced Usage

18.11.1. Daemonising Podman

FreeBSD’s Podman implementation provides two rc(8) services:

  • The podman service runs at boot and ensures that all containers marked with restart-policy=always are started.

  • The podman_service service runs the Podman API service, providing a REST API and listening on /var/run/podman/podman.sock by default.

Enable them as follows:

# sysrc podman_enable=YES
# sysrc podman_service_enable=YES
# sysrc podman_service_flags='--time 0'
# service podman start
# service podman_service start

Containers that should restart automatically after a host reboot must be run with the --restart=always option:

# podman run --detach --restart=always my-image

18.11.2. Running Linux Containers

It is possible to run many Linux container images using FreeBSD’s Linux emulation:

# service linux onestart
# podman run --rm --os=linux docker.io/alpine cat /etc/os-release | head -1
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob 4abcf2066143 done   |
Copying config 05455a0888 done   |
Writing manifest to image destination
NAME="Alpine Linux"

Linux containers that depend on systemd, expect to be PID 1, or rely on Linux-specific kernel interfaces may not work correctly under FreeBSD’s Linux emulation.

18.11.3. Running a Local Registry

A registry is a web server for storing and sharing OCI images. The sysutils/docker-registry port provides a simple registry suitable for local development or a small organisation. Zot Registry is a more comprehensive modern alternative.

# pkg install -r FreeBSD sysutils/docker-registry
# zfs create zroot/var/db/registry
# chown -R www:www /var/db/registry
# sysrc docker_registry_enable=YES
# sysrc docker_registry_user=www
# sed -i '' -E -e 's,rootdirectory:.+,rootdirectory: /var/db/registry,' \
    /usr/local/etc/docker-registry/config.yml
# touch /usr/local/etc/docker-registry/htpasswd
# chown -R root:www /usr/local/etc/docker-registry/config.yml \
    /usr/local/etc/docker-registry/htpasswd
# chmod 0640 /usr/local/etc/docker-registry/config.yml \
    /usr/local/etc/docker-registry/htpasswd

A sample configuration for /usr/local/etc/docker-registry/config.yml:

version: 0.1
log:
  fields:
    service: registry
storage:
  cache:
    blobdescriptor: inmemory
  filesystem:
    rootdirectory: /var/db/registry
http:
  addr: :5000
  secret: <random-secret>
  headers:
    X-Content-Type-Options: [nosniff]
auth:
  htpasswd:
    realm: basic-realm
    path: /usr/local/etc/docker-registry/htpasswd
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

Start the registry and verify it is running:

# service registry start
# curl -u admin:passwd http://localhost:5000/v2/

It is recommended to serve the registry over TLS, behind a reverse proxy such as nginx or haproxy.

18.11.4. Synchronising Registries

It is possible to synchronise official FreeBSD images into a local registry using skopeo. The following example replicates all FreeBSD-related images from Docker Hub to a local registry:

# sync.yml
docker.io:
  images:
    freebsd/freebsd-static: []
    freebsd/freebsd-dynamic: []
    freebsd/freebsd-runtime: []
$ skopeo sync --src yaml --dest docker --all sync.yml cr.example.net/freebsd
INFO[0000] Processing repo                               repo=freebsd/freebsd-static
INFO[0000] Querying registry for image tags              repo=freebsd/freebsd-static
...
INFO[0771] Synced 42 images from 3 sources

So long as the credentials in /root/.config/containers/auth.json are up to date, root access is not required to sync images.

18.11.5. Working with Container Registries

Registries generally require authentication for push operations, and optionally for pull. Credentials are saved in /root/.config/containers/auth.json by default after login:

# podman login ghcr.io -u username
Password:
Login Succeeded!

Images can then be pushed to a registry:

# podman push localhost/freebsd-runtime:15.1 \
    docker://ghcr.io/example/freebsd-runtime:15.1
Getting image source signatures
Copying blob bbacfdfbe140 done   |
Copying config 786f2592a8 done   |
Writing manifest to image destination

Public images can be pulled without authentication:

# podman pull ghcr.io/freebsd/freebsd-runtime:15.1

18.12. Building Container Images from Sources

The FreeBSD release tooling provides an oci-release target to build the necessary images for podman load to use. It requires building world, kernel, and base system packages first, so this can take a while.

The following example builds arm64 images:

# cd /usr/src \
    && make TARGET_ARCH=aarch64 TARGET=arm64 -s -j32 buildworld \
    && make TARGET_ARCH=aarch64 TARGET=arm64 KERNCONF=GENERIC -s -j32 buildkernel \
    && make TARGET_ARCH=aarch64 TARGET=arm64 KERNCONF=GENERIC -s -j32 packages \
    && cd ./release \
    && make TARGET_ARCH=aarch64 TARGET=arm64 -j32 -s clean \
    && make KERNCONF=GENERIC TARGET_ARCH=aarch64 TARGET=arm64 WITH_OCIIMAGES=yes \
      -s oci-release

And similarly for amd64:

# cd /usr/src \
    && make TARGET_ARCH=amd64 TARGET=amd64 -s -j32 buildworld \
    && make TARGET_ARCH=amd64 TARGET=amd64 KERNCONF=GENERIC -s -j32 buildkernel \
    && make TARGET_ARCH=amd64 TARGET=amd64 KERNCONF=GENERIC -s -j32 packages \
    && cd ./release \
    && make TARGET_ARCH=amd64 TARGET=amd64 -s -j32 clean \
    && make KERNCONF=GENERIC TARGET_ARCH=amd64 TARGET=amd64 \
      WITH_OCIIMAGES=yes -s oci-release

18.13. Cross-Architecture Testing

The emulators/qemu-user-static package allows testing container images built for other architectures. After installing the package, enable and start the service to register image activators for all emulated architectures:

# pkg install emulators/qemu-user-static
# service qemu_user_static enable
# service qemu_user_static start

Then run an arm64 image on an amd64 host by mounting the appropriate QEMU binary:

# podman run -it --rm --arch=arm64 \
    -v /usr/local/bin/qemu-aarch64-static:/usr/local/bin/qemu-aarch64-static \
    ghcr.io/freebsd/freebsd-runtime:14.snap uname -a
FreeBSD 73d4049d10b2 14.2-RELEASE-p1 FreeBSD 14.2-RELEASE-p1 GENERIC arm64

18.14. Troubleshooting

18.14.1. Package Upgrades and Mounted Files

Podman mounts /etc/resolv.conf and /etc/hosts into containers using mount_nullfs(8). These mounts can cause errors during pkg(8) operations with messages such as Cross-device link:

pkg: Fail to rename /etc/.pkgtemp.hosts.VeIDrHM0sKN5 -> /etc/hosts:Cross-device link

To avoid this, use --no-hosts and --dns flags:

# podman run --no-hosts --dns 1.1.1.1 -it --rm \
    ghcr.io/freebsd/freebsd-runtime:15.1 /bin/sh

18.14.2. Checking Networking

Start with the firewall rules from /usr/local/etc/containers/pf.conf.sample. Test connectivity step by step from within a container:

# podman run --no-hosts --dns 1.1.1.1 \
    --env ASSUME_ALWAYS_YES=1 \
    -it --rm \
    ghcr.io/freebsd/freebsd-runtime:15.1 \
    /bin/sh

# route show 1.1.1.1
   route to: 1.1.1.1
destination: default
    gateway: 10.88.0.1
  interface: eth0

# ping -c 3 10.88.0.1
PING 10.88.0.1 (10.88.0.1): 56 data bytes
64 bytes from 10.88.0.1: icmp_seq=0 ttl=64 time=0.083 ms
...

# ping -c 3 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: icmp_seq=0 ttl=60 time=0.920 ms
...

# fetch -v http://1.1.1.1/
...

# pkg -dd bootstrap -r FreeBSD

If ping(8) fails, check the firewall rules for blocked ICMP. If fetch(1) fails, check the firewall rules for blocked TCP on ports 80 and 443. If pkg(8) fails but ICMP and TCP work, this may be a fragmentation or MTU problem. Add set reassemble yes no-df to /etc/pf.conf and restart the firewall.

For debugging blocked packets, enable log on all block rules in /etc/pf.conf and use tcpdump(1) on the pflog(4) device:

# service pflog onerestart
# tcpdump -vvveni pflog0 icmp or net 10.88.0.0/16

18.14.3. MTU Consistency

Ensure MTU values are consistent across the primary network interface, the container bridge, and the container itself. Mismatched MTU values can cause packet fragmentation and connectivity failures.

In the following example, vtnet0 has MTU 9000, but the bridge and containers have MTU 1500. In general, align the internal layout to match the external network:

# ifconfig vtnet0 mtu 9000
# ifconfig cni-podman0 mtu 9000

Inside the container:

# ifconfig eth0 mtu 9000

The bridges and epairs used by Podman are ephemeral and will need to be configured for each bridge creation and each container. In some cloud environments, adding set reassemble yes no-df to /etc/pf.conf may also be required.


Last modified on: July 11, 2026 by Dave Cottlehuber