Modern, open-source management platform for FreeBSD managing Virtual Machines (Bhyve), Jails, and ZFS storage.
!!! tip "What is Sylve?"
Sylve is a modern, open-source management platform for FreeBSD: Bhyve VMs, jails, ZFS, and networking behind one web UI. It manages the host from inside its own jail, so it needs host access a normal container never gets -- kernel modules, devfs entries like /dev/pf and /dev/vmm, ZFS delegation, and allow.vmm. Both deploy methods below wire that up for you.
Both tags ship Sylve's prebuilt native-FreeBSD binary from a GitHub release; they differ only in which release they track.
| Tag | Description | Best For |
|---|---|---|
latest |
The latest tagged release (AlchemillaHQ/Sylve). | Most deployments. |
nightly |
The rolling tip release, tracking upstream master. |
Early access to unreleased fixes. |
Requirements:
- FreeBSD 15+ and
root - A ZFS pool (a
sylvedataset gets delegated to the jail) - AppJail path:
appjail5.4+,sysutils/py-director-- Podman path:podman,ocijail,podman-compose
!!! warning "Experimental" Both the Podman and AppJail deployments of Sylve are experimental on our end.
=== ":appjail-appjail: AppJail Director"
!!! warning "`load-kld` hook is required"
[load-kld](https://github.com/DtxdF/AppJail/blob/main/share/examples/appjail/hooks/pre.d/load-kld.sh) hook is required for the kernel-module handling this deployment relies on.
1. Be sure to set `HOOKSDIR` in your `appjail.conf(5)` file.
```console
$ grep '^HOOKSDIR' /usr/local/etc/appjail/appjail.conf
HOOKSDIR=/usr/local/etc/appjail/hooks
```
2. Create the hooks directory if it does not exist.
```console
$ mkdir -p /usr/local/etc/appjail/hooks/pre.d/
```
3. Install the `load-kld` hook and set the execute bit.
```console
$ fetch -o /usr/local/etc/appjail/hooks/pre.d/load-kld.sh https://github.com/DtxdF/AppJail/blob/main/share/examples/appjail/hooks/pre.d/load-kld.sh
$ chmod +x /usr/local/etc/appjail/hooks/pre.d/load-kld.sh
```
**1.** Prepare the host (one time). Kernel modules are loaded automatically at jail start via `load-kld` labels; only the boot-only `kern.racct` tunable touches `loader.conf`:
```bash
pkg install -y appjail sysutils/py-director
[ "$(sysctl -n kern.racct.enable)" = "1" ] || echo 'kern.racct.enable="1"' >> /boot/loader.conf
# reboot if `sysctl -n kern.racct.enable` is still 0 (Sylve requires racct)
sysrc appjail_enable=YES
```
**2.** Save as `.env`:
``` { data-zip-bundle="sylve-appjail" data-zip-filename=".env" }
# Host path for Sylve's data
SYLVE_DATA_LOCATION=/var/appjail-volumes/sylve/data
DIRECTOR_PROJECT=sylve
```
**3.** Save as `appjail-director.yml`:
```yaml { data-zip-bundle="sylve-appjail" data-zip-filename="appjail-director.yml" }
options:
# Equivalent to 'network_host: host'.
# Use it only if you don't have problems with Sylve managing your pf(4).
#- alias:
#- ip4_inherit:
#
# The jail will use its own network stack without touching with the host's;
# in addition, AppJail will configure an IPv4 address that you can use to
# communicate with Sylve via the jail's IPv4 address or hostname (if you
# have enabled DNS in AppJail).
- virtualnet: ':<random> default'
- nat:
services:
sylve:
name: sylve
options:
- from: ghcr.io/daemonless/sylve:nightly
- template: !ENV '${PWD}/sylve-template.conf'
- container: 'args:--pull'
- label: 'load-kld:1'
- label: 'load-kld.load-vmm:vmm'
- label: 'load-kld.load-if-bridge:if_bridge'
- label: 'load-kld.load-cryptodev:cryptodev'
- label: 'load-kld.load-if-epair:if_epair'
- label: 'load-kld.load-nullfs:nullfs'
- label: 'load-kld.load-netlink:netlink'
- label: 'load-kld.load-nlsysevent:nlsysevent'
- label: 'load-kld.load-nmdm:nmdm'
- label: 'load-kld.load-pf:pf'
- label: 'load-kld.load-pflog:pflog'
- label: 'load-kld.load-if-wg:if_wg'
- label: 'load-kld.load-linux:linux'
- label: 'load-kld.load-linux.module:linuxelf'
- label: 'load-kld.load-linux64:linux64'
- label: 'load-kld.load-linux64.module:linux64elf'
- label: 'load-kld.load-pty:pty'
- label: 'load-kld.load-linprocfs:linprocfs'
- label: 'load-kld.load-linsysfs:linsysfs'
- label: 'load-kld.load-ctl:ctl'
- label: 'load-kld.load-iscsi:iscsi'
- device: 'include $devfsrules_hide_all'
- device: 'include $devfsrules_unhide_basic'
- device: 'include $devfsrules_unhide_login'
- device: 'include $devfsrules_jail'
- device: 'include $devfsrules_jail_vnet'
- device: 'path zfs unhide'
- device: 'path zvol unhide'
- device: "path 'zvol/*' unhide"
- device: "path 'zvol/*/*' unhide"
- device: "path 'zvol/*/*/*' unhide"
- device: "path 'zvol/*/*/*/*' unhide"
- device: "path 'zvol/*/*/*/*/*' unhide"
- device: 'path shm unhide'
- device: 'path pf unhide'
- device: 'path pflog unhide'
- device: 'path bpf unhide'
- device: "path 'bpf*' unhide"
- device: 'path vmm unhide'
- device: "path 'vmm/*' unhide"
- device: 'path vmm.io unhide'
- device: "path 'vmm.io/*' unhide"
- device: 'path vmmctl unhide'
- device: "path 'nmdm*' unhide"
- device: "path 'tap*' unhide"
- device: 'path mem unhide'
- device: 'path kmem unhide'
- device: 'path pci unhide'
- device: 'path io unhide'
- device: 'path cam unhide'
- device: "path 'cam/ctl' unhide"
- device: "path 'da*' unhide"
- device: "path 'ada*' unhide"
- device: "path 'nda*' unhide"
- device: "path 'pass*' unhide"
- device: "path 'xpt*' unhide"
- device: "path 'nvme*' unhide"
- device: 'path iscsi unhide'
volumes:
- sylve-data: /var/db/sylve
volumes:
sylve-data:
device: !ENV '${SYLVE_DATA_LOCATION}'
```
**4.** Save as `sylve-template.conf`:
``` { data-zip-bundle="sylve-appjail" data-zip-filename="sylve-template.conf" }
# EDIT: the ZFS dataset created during host prep
${dataset}: zroot/sylve
# Number of child jails allowed to be created by this jail.
${children_max}: 100
exec.start: "/bin/sh /etc/rc"
exec.stop: "/bin/sh /etc/rc.shutdown jail"
mount.devfs
persist
allow.vmm
allow.vmm_ppt
stop.timeout: 30
allow.chflags
allow.raw_sockets
allow.routing
allow.nfsd
allow.unprivileged_proc_debug
enforce_statfs: 1
allow.mount
allow.mount.devfs
allow.mount.fdescfs
allow.mount.linprocfs
allow.mount.linsysfs
allow.mount.tmpfs
allow.mount.zfs
zfs.mount_snapshot
children.max: ${children_max}
allow.socket_af
allow.sysvipc
allow.reserved_ports
allow.set_hostname
allow.suser
exec.created+: "zfs create -p -o jailed=on ${dataset}"
exec.created+: "zfs jail ${name} ${dataset}"
exec.created+: "jexec -l ${name} zfs list -t fs -Hro name ${dataset} | xargs -L 1 jexec -l ${name} zfs mount"
exec.prestop+: "jexec -l ${name} zfs list -t fs -Hro name ${dataset} | tail -r | xargs -L 1 jexec -l ${name} zfs umount"
exec.clean
```
**5.** Save as `Makejail`:
``` { data-zip-bundle="sylve-appjail" data-zip-filename="Makejail" }
OPTION container=boot
OPTION overwrite=force
```
**6.** Create the data directory and deploy:
```bash
appjail-director up
```
=== ":material-tune: Podman Compose"
**1.** Prepare the host (one time). Generate the setup script, review it, then run it -- it loads the kernel modules, adds a devfs ruleset, creates the ZFS dataset, and installs the OCI `createRuntime` hook (`ocijail` can't express devfs rulesets or ZFS delegation through annotations). Each step asks before running; already-configured steps are skipped:
```bash
podman run --rm ghcr.io/daemonless/sylve:latest host-setup > sylve-setup.sh
less sylve-setup.sh # review exactly what it will change
sh sylve-setup.sh # or: sh sylve-setup.sh -y
```
If it reports that `kern.racct` needs a reboot, reboot before continuing (Sylve requires it).
**2.** Create the deployment files. `init` writes `compose.yaml` and `.env` into the current directory (it refuses to overwrite existing files without `--force`):
```bash
podman run --rm -v "$PWD:/out" ghcr.io/daemonless/sylve:latest init
vi .env
```
**3.** Start:
```bash
mkdir -p "$SYLVE_DATA_LOCATION" # FreeBSD won't auto-create a bind-mount source
podman compose up -d
```
Access Sylve at: https://your-host:8181 (first login: admin / admin)
| Variable | Default | Description |
|---|---|---|
SYLVE_HOSTNAME |
-- | The jail's hostname (node-identity check). Podman: hostname: in compose.yaml. |
SYLVE_DATA_LOCATION |
-- | Host path for Sylve's data (/var/db/sylve in the jail). |
SYLVE_DATASET |
zroot/sylve |
ZFS dataset delegated to Sylve. AppJail: ${dataset} in sylve-template.conf. |
TZ |
System default | Timezone. |
| Port | Service | Description |
|---|---|---|
8181 |
sylve | Web UI (HTTPS) |
!!! note "Network Mode"
The Podman path shares the host network (network_mode: host) so Sylve manages the host's interfaces, firewall, and VMs directly. The AppJail path defaults to its own vnet + NAT to avoid clobbering the host's pf(4) rules -- uncomment alias/ip4_inherit in appjail-director.yml for host networking instead.
If requests fail with selected_node_not_found, Sylve's EnsureCorrectHost check is comparing its configured hostname against the one you're browsing to -- set the jail's hostname to the name you reach it at.
A nullfs /dev (-v /dev:/dev) makes Bhyve guest-memory mmap fail with ENXIO (Unable to setup memory (6)) -- VM creation succeeds but the guest never boots. Both deploy methods mount a real devfs via a ruleset; don't override it.
sh sylve-setup.sh --undo removes Sylve's OCI hook and devfs ruleset. It never touches your ZFS dataset (it prints the zfs destroy command if you want it gone).
| Registry | ghcr.io/daemonless/sylve |
| Upstream | https://github.com/AlchemillaHQ/Sylve |
| Website | https://sylve.io |