Skip to content

Quick Start

Description / nameInput element
Container Registry
Container Configuration Root Path
Host Network Interface
User ID
Group ID

Daemonless images are standard OCI images—the same format used by Docker and Podman. Because they follow open standards, you can run them with any OCI-compatible tool on FreeBSD.

The project officially supports two primary runtimes. Choose the one that matches your background and requirements:

Feature Podman AppJail
The Experience Familiar. Feels like Docker on Linux. Native. Built for the FreeBSD way.
Best For Quick migrations and compose.yaml fans. Production stability and deep system control.
Primary Tool podman / podman-compose appjail / appjail-director
Config Format Standard compose.yaml Director YAML + Makejail
Networking CNI-based (Bridge, Host) Native VNET, IP aliases, and virtual nets.
Privileges Root only (on FreeBSD) Unprivileged via doas delegation.

Which should I choose?

  • Choose Podman if you want to reuse existing Docker knowledge and Compose files. It is the path of least resistance for users coming from Linux.
  • Choose AppJail if you want a powerhouse FreeBSD-native orchestrator. It offers deeper integration with FreeBSD features and supports unprivileged management.

Both are first-class citizens in the Daemonless ecosystem. Pick whichever fits your workflow — or use both. Any other OCI-compatible tool with FreeBSD support should work as well.

Coexistence: Using Both Together

It is perfectly safe to follow both guides on the same host. They manage separate configurations and storage, and their naming conventions do not overlap.

Customize Your Guide

Scroll to Interactive Configuration at the bottom to set your PUID, PGID, and paths. All commands will update automatically.

Podman

Prerequisites

Root Privileges Required

Podman on FreeBSD currently requires root. Rootless mode is not yet supported. All commands in this guide must be run as root (or via sudo/doas).

Install Podman and container networking:

pkg install podman-suite sysutils/podman-compose

ocijail 0.5.0+ Required

ocijail version 0.5.0 or higher is required for .NET applications (Radarr/Sonarr) and PostgreSQL. This version natively supports the required jail parameters via OCI annotations.

Host Configuration

1. Enable Networking

Configure the kernel to allow packet filtering for local traffic and ensure fdescfs is mounted.

# Enable pf filtering for jails
sysctl net.pf.filter_local=1
echo 'net.pf.filter_local=1' >> /etc/sysctl.conf

# Mount fdescfs
mount -t fdescfs fdesc /dev/fd
echo 'fdesc /dev/fd fdescfs rw 0 0' >> /etc/fstab

2. Configure Firewall (pf.conf)

Add the following to /etc/pf.conf. Replace em0 if your external interface is different.

# Primary network interface
ext_if=em0

# Podman container networking
rdr-anchor "cni-rdr/*"
nat-anchor "cni-rdr/*"
table <cni-nat>
nat on $ext_if inet from <cni-nat> to any -> ($ext_if)
nat on $ext_if inet from 10.88.0.0/16 to any -> ($ext_if)

Reload the configuration:

pfctl -f /etc/pf.conf

3. Start Podman

sysrc podman_enable=YES
service podman start

Run Your First Container

We'll start with Tautulli, a lightweight Python app that doesn't require special permissions.

podman run -d --name tautulli \
  -p 8181:8181 \
  -e PUID=1000 -e PGID=1000 \
  -v /path/to/containers/tautulli:/config \
  ghcr.io/daemonless/tautulli:latest

Check the status:

podman ps
podman logs -f tautulli
Access the UI at: http://localhost:8181

.NET Applications

Applications like Radarr and Sonarr require the allow.mlock jail annotation to function correctly on FreeBSD.

podman run -d --name radarr \
  -p 7878:7878 \
  --annotation 'org.freebsd.jail.allow.mlock=true' \
  -e PUID=1000 -e PGID=1000 \
  -v /path/to/containers/radarr:/config \
  ghcr.io/daemonless/radarr:latest

Advanced Setup (Optional)

If you're using ZFS, configure Podman to use it for proper copy-on-write layering and snapshot support:

zfs create -o mountpoint=/var/db/containers/storage <pool>/podman
See ZFS Storage for storage.conf tuning.

To use container names as hostnames (e.g. postgres), the cni-dnsname plugin is required.

# Clone the ports overlay
git clone https://github.com/daemonless/freebsd-ports.git /usr/local/daemonless-ports

# Build and install
cd /usr/local/daemonless-ports/net/cni-dnsname
make install clean
See Networking Guide for details.

AppJail

Prerequisites

Warning

This quick start guide is intended exclusively for OCI container users. For more general information, see the AppJail Handbook, appjail-tutorial(7), director(1) and director-spec(5) man pages.

Warning

If you plan to use ZFS, set it up before running AppJail. See below for details.

Install AppJail and Director

pkg install -y appjail sysutils/py-director

Host Configuration

0. Configure trusted users (optional)

AppJail requires privileges to run, but it can be integrated with tools such as security/doas to run it as a user without root privileges. This is recommended when you are the only person using the computer and have privileges, or in cases where there are more than two sysadmins or developers on the same server with root access.

/usr/local/etc/doas.conf:

permit nopass keepenv :appjail as root cmd appjail

This rule also assumes that you have a group named appjail. If you don't, don't worry:

pw groupadd -n appjail

To add your user to the appjail group simply run the following:

pw groupmod -n appjail -m "$USER"

Where $USER is your user. For these changes to take effect, you must log back into the system if you are adding yourself.

Similarly, there is a variant for appjail-config named appjail-config-user. The instructions for using it are similar to the above:

permit nopass :appjail as root cmd appjail-config

To test the changes above, simply run the following as a non-root user:

appjail help
appjail-config-user help

See also: Trusted Users on AppJail Handbook.

1. Enable Networking

Tip

Not all network options require packet filtering (for example: aliasing, bridge-only, vnet-only, etc.), but it is particularly useful for Virtual Networks, a common network option in deployments.

AppJail does not require any configuration, as it uses the default settings, but we recommend that you at least configure the EXT_IF parameter to point to your external interface.

/usr/local/etc/appjail/appjail.conf:

EXT_IF=em0

NAT and port forwarding require IP forwarding, so let's set it up:

sysrc gateway_enable="YES"
sysctl net.inet.ip.forwarding=1

2. Configure Firewall (pf.conf)

AppJail uses anchors like other applications that use pf(4) as a backend. Just enable pf(4) in your rc.conf(5), put the anchors in the pf.conf(5) file and reload the rules.

# Enable pf(4):
sysrc pf_enable="YES"
sysrc pflog_enable="YES"
# Put the anchors in pf.conf(5):
cat << "EOF" >> /etc/pf.conf
nat-anchor "appjail-nat/jail/*"
nat-anchor "appjail-nat/network/*"
rdr-anchor "appjail-rdr/*"
EOF
# Reload the pf(4) rules:
service pf reload
# Or restart the rc(8) script if you don't have pf(4) started:
service pf restart
service pflog restart

See also: Packet Filter on AppJail Handbook.

3. Start AppJail

If you want to start your jails at startup, enable AppJail's rc(8) script:

sysrc appjail_enable=YES

.NET Applications

In an appjail-template(5) file, you can define any jail(8) parameter, including the one used by .NET applications.

Run Your First Container

Let's deploy a simple web application.

# mkdir -p -- "/path/to/containers/tautulli"
# appjail oci run \
    -d \
    -u root \
    -o overwrite=force \
    -o virtualnet=":<random> default" \
    -o nat \
    -o expose="8181:8181" \
    -o container="args:--pull" \
    -o ephemeral \
    -o fstab="/path/to/containers/tautulli /config" \
    -e PUID=1000 \
    -e PGID=1000 \
        ghcr.io/daemonless/tautulli:latest tautulli
...
[00:00:54] [ info  ] [tautulli] Detached: pid:97368, log:jails/tautulli/container/2026-03-22.log
# appjail jail list -j tautulli
STATUS  NAME      ALT_NAME  TYPE   VERSION       PORTS  NETWORK_IP4
UP      tautulli  -         thick  15.0-RELEASE  -      10.0.0.3
# appjail jail list -j tautulli name container_pid
NAME      CONTAINER_PID
tautulli  97368
# appjail logs tail jails/tautulli/container/2026-03-22.log -f
2026-03-22 04:49:07 - ERROR :: MainThread : Tautulli PlexTV :: PlexTV called, but no token provided.
2026-03-22 04:49:07 - ERROR :: MainThread : Tautulli PlexTV :: PlexTV called, but no token provided.
2026-03-22 04:49:08 - INFO :: MainThread : Tautulli WebStart :: Initializing Tautulli web server...
2026-03-22 04:49:08 - WARNING :: MainThread : Tautulli WebStart :: Web server authentication is disabled!
2026-03-22 04:49:08 - INFO :: MainThread : Tautulli WebStart :: Thread Pool Size: 10.
2026-03-22 04:49:08 - INFO :: MainThread : Tautulli WebStart :: Starting Tautulli web server on http://0.0.0.0:8181/
/app/tautulli/lib/cherrypy/process/servers.py:410: UserWarning: Unable to verify that the server is bound on 8181
  warnings.warn(msg)
2026-03-22 04:49:13 - INFO :: MainThread : [22/Mar/2026:04:49:13] ENGINE Serving on http://0.0.0.0:8181
2026-03-22 04:49:18 - INFO :: MainThread : Tautulli is ready!

Access the UI at http://10.0.0.3:8181

Notes:

  1. -d: The process will run in the background.
  2. -u root: We run s6 as root, but keep in mind that the process is already running as the bsd user inside the jail, which is also mapped based on the PUID and PGID environment variables. We recommend specifying the user explicitly. See this pr for more details.
  3. -o overwrite=force: Destroy the jail if it already exists, so that AppJail will recreate it instead of refusing to do so.
  4. -o virtualnet=":<random> default" -o nat -o expose="8181:8181": Network options. In this case, we chose to use Virtual Networks.
  5. -o container="args:--pull": Let's pull the image every time buildah(1) detects changes, so that AppJail always runs the jail using the latest image.
  6. -o ephemeral: Mark this jail as ephemeral, so that when it stops (or starts, in the event of a power outage on the computer), AppJail will destroy it.
  7. ghcr.io/daemonless/tautulli:latest tautulli: The image, tag, and the jail name. The tag is optional.

AppJail Director

Although you can use AppJail exclusively to deploy containers, it is recommended that you use AppJail Director. Environment variables set by appjail-oci(1) run will not be preserved after restarting the jail. You can use various appjail-oci(1) subcommands, such as set-user, set-env, etc., and then run the from subcommand, but this does not scale well when there are multiple containers. Another advantage is that Director defines its deployment file in YAML format declaratively, so it can be easily shared.

For example, the above deployment can easily be translated into a Director file:

appjail-director.yml:

options:
  - virtualnet: ':<random> default'
  - nat:
services:
  tautulli:
    name: tautulli
    options:
      - expose: '8181:8181'
      - container: 'boot args:--pull'
    oci:
      user: root
      environment:
        - PUID: 1000
        - PGID: 1000
    volumes:
      - config: /config
volumes:
  config:
    device: '/path/to/containers/tautulli'

Makejail:

ARG tag=latest

OPTION overwrite=force
OPTION from=ghcr.io/daemonless/tautulli:${tag}

.env:

DIRECTOR_PROJECT=tautulli

By default, director(1) uses Makejail (which is assumed to be in the same directory as the Director file) as its appjail-makejail(5) and executes it. Some options are defined in a appjail-makejail(5) file, while others are defined per service. The convention is to specify options that do not change frequently in a appjail-makejail(5) file and the rest per service in the Director file. You can also set parameters using ARG, as we did above to specify the image tag, whose default value is latest. Finally, we define a .env file with environment variables loaded by Director.

Console:

# appjail-director up
Starting Director (project:tautulli) ...
Creating tautulli (tautulli) ... Done.
 - Configuring the user (OCI) ... Done.
 - Configuring the environment (OCI):
   - PUID ... Done.
   - PGID ... Done.
Starting tautulli (tautulli) ... Done.
Finished: tautulli
# appjail-director info
tautulli:
 state: DONE
 last log: /root/.director/logs/2026-03-22_19h02m04s
 locked: false
 services:
  + tautulli (tautulli)
# ls /root/.director/logs/2026-03-22_19h02m04s/tautulli/
makejail.log        oci-environment.log oci-user.log        start.log
# tail -1 /root/.director/logs/2026-03-22_19h02m04s/tautulli/start.log
[00:00:05] [ info  ] [tautulli] Detached: pid:83091, log:jails/tautulli/container/2026-03-22.log

Advanced Setup (Optional)

To enable ZFS, simply add ENABLE_ZFS=1 to your appjail.conf(5) file. You may also need to configure ZPOOL, ZROOTFS, and ZOPTS if the default values do not suit your environment.

See also: ZFS on AppJail Handbook.

AppJail copies DEFAULT_RESOLV_CONF to the jail's resolv.conf(5) file, whose default value is /etc/resolv.conf. Since this file can be modified by many programs, it is recommended that you configure a custom resolv.conf(5) file in a more stable location, such as /usr/local/etc/appjail/resolv.conf.

/usr/local/etc/appjail/appjail.conf:

DEFAULT_RESOLV_CONF=/usr/local/etc/appjail/resolv.conf

AppJail can be integrated with a third-party DNS server, and we can configure that server to read a hosts(5) file modified by appjail-dns(8). Our first-class citizen is dns/dnsmasq, so let’s set it up. Before doing so, keep in mind that our resolv.conf(5) must point to an IP address where DNSMasq can receive packets, but the problem is that if we use a dynamic IP address, this can be problematic. To create a more deterministic environment, we’ll create an if_tap(4) interface and set a static IP address, so that our jails point to DNSMasq using that IP address.

sysrc cloned_interfaces="tap0"
sysrc ifconfig_tap0_name="ajdns"
sysrc ifconfig_ajdns="inet 172.0.0.1/32"
service netif cloneup
service netif start ajdns

/usr/local/etc/appjail/resolv.conf:

nameserver 172.0.0.1

Let's configure DNSMasq and appjail-dns rc script.

sysrc appjail_dns_enable="YES"
sysrc dnsmasq_enable="YES"
sysrc dnsmasq_conf="/usr/local/share/appjail/files/dnsmasq.conf"
touch /var/tmp/appjail-hosts
service dnsmasq start
service appjail-dns start
cp /usr/local/etc/appjail/resolv.conf /etc/resolv.conf
chflags schg /etc/resolv.conf

To test our configuration, simply use a jail's name as the hostname and try to resolve it using a tool like host(1).

# host tautulli
tautulli has address 10.0.0.3

If you've created a jail and then run host(1) before the appjail-dns rc script detects the changes, you may receive an NXDOMAIN error. If you don't want to wait, simply restart the appjail-dns rc script.

# service appjail-dns restart
Stopping appjail_dns.
Waiting for PIDS: 89362.
AppJail log file (DNS): /var/log/appjail.log
Starting appjail_dns.
# host tautulli
tautulli has address 10.0.0.3

Note

Please note that this only works with Virtual Networks.

See also: DNS on AppJail Handbook.


Interactive Configuration

Next Steps