Skip to content

ephemeral

Jesús Daniel Colmenares Oviedo edited this page Aug 8, 2025 · 3 revisions

Deploying virtual machines with ephemeral jails

Overlord will take care not to destroy the jail with your valuable virtual machine in vmjail deployments. However, this approach does not honor "The Ephemeral Concept" of AppJail, which has many advantages that cannot be ignored, particularly when updating or upgrading the jail and its contents.

metadata.yml:

kind: metadata
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - vm-only
metadata:
  ts_auth_key: !ENV '${TS_AUTH_KEY}'
  resolv.conf: |
    nameserver 172.0.0.1
  timezone: 'America/Caracas'
  loader.conf: |
    nvme_load="YES"
    if_bridge_load="YES"
    bridgestp_load="YES"
    if_wg_load="YES"
    kern.racct.enable=1
  ssh_key: !ENV '${SSH_KEY}'
  sshd_config: |
    # Ports
    Port 22

    # Authentication
    PubkeyAuthentication yes
    AuthenticationMethods publickey
    PermitRootLogin prohibit-password
    PrintMotd no

    # Forwarding
    X11Forwarding no
    AllowAgentForwarding yes

    # Connection checks
    ClientAliveCountMax 3
    ClientAliveInterval 15

    # Compression
    Compression no

    # Limits
    LoginGraceTime 40

    # Public keys
    AuthorizedKeysFile      /etc/ssh/authorized_keys

    # SFTP
    Subsystem sftp internal-sftp
  sysctl.conf: |
    # A bit of hardening
    security.bsd.see_other_uids=0
    security.bsd.see_other_gids=0
    security.bsd.see_jail_proc=0
    kern.randompid=1

    # Allow packet filtering in if_bridge(4)
    net.link.bridge.pfil_member=1
    net.link.bridge.pfil_bridge=1
  pkg.conf: |
    FreeBSD: {
      url: "pkg+http://pkg.FreeBSD.org/${ABI}/latest",
      mirror_type: "srv",
      signature_type: "fingerprints",
      fingerprints: "/usr/share/keys/pkg",
      enabled: yes
    }
  appVM.appConfig: |
    <%page args="appvm_use_pkgbase='0',poweroff='0'"/>

    kind: 'vmJail'
    vmName: ${appName}
    makejail: 'gh+DtxdF/vm-makejail'
    overwrite: true
    % if poweroff != "0":
    poweroff: true
    % endif
    datastore: '/var/appjail-vm/${appName}/data'
    options:
      - fstab: '/var/appjail-vm/${appName}/data vm-data <volumefs>'
    template:
      loader: 'bhyveload'
      cpu: '${ncpu}'
      memory: '${memory}'
      network0_type: 'virtio-net'
      network0_switch: 'public'
      wired_memory: 'YES'
    diskLayout:
      driver: 'nvme'
      size: '${disk}'
      from:
        % if appvm_use_pkgbase == "0":
        type: 'components'
        components:
          - base.txz
          - kernel.txz
        osArch: amd64
        osVersion: '${fbsdver}'
        % else:
        type: 'appjailImage'
        entrypoint: gh+AppJail-makejails/pkgbase
        imageName: pkgbase
        imageArch: amd64
        imageTag: '${fbsdver}'
        % endif
      disk:
        scheme: 'gpt'
        partitions:
          - type: 'freebsd-boot'
            size: '512k'
            alignment: '1m'
          - type: 'freebsd-swap'
            size: '${swap}'
            alignment: '1m'
          - type: 'freebsd-ufs'
            alignment: '1m'
            format:
              flags: '-U'
        bootcode:
          bootcode: '/boot/pmbr'
          partcode: '/boot/gptboot'
          index: 1
      fstab:
        - device: '/dev/nda0p3'
          mountpoint: '/'
          type: 'ufs'
          options: 'rw,sync'
          dump: 1
          pass: 1
        - device: '/dev/nda0p2'
          mountpoint: 'none'
          type: 'swap'
          options: 'sw'
          dump: 0
          pass: 0
    script-environment:
    - HOSTNAME: '${hostname}'
    script: |
      <%text filter="n">
        set -xe
        set -o pipefail

        . "/metadata/environment"

        sysrc -f /mnt/etc/rc.conf ifconfig_vtnet0="inet 192.168.8.2/24"
        sysrc -f /mnt/etc/rc.conf defaultrouter="192.168.8.1"
        sysrc -f /mnt/etc/rc.conf fsck_y_enable="YES"
        sysrc -f /mnt/etc/rc.conf clear_tmp_enable="YES"
        sysrc -f /mnt/etc/rc.conf dumpdev="NO"
        sysrc -f /mnt/etc/rc.conf moused_nondefault_enable="NO"
        sysrc -f /mnt/etc/rc.conf hostname="${HOSTNAME}"

        if [ -f "/metadata/resolv.conf" ]; then
          cp -a /metadata/resolv.conf /mnt/etc/resolv.conf
        fi

        if [ -f "/metadata/loader.conf" ]; then
          cp /metadata/loader.conf /mnt/boot/loader.conf
        fi

        if [ -f "/metadata/zerotier_network" ]; then
          pkg -c /mnt install -y zerotier

          zerotier_network=`head -1 -- "/metadata/zerotier_network"`

          cat << EOF > /mnt/etc/rc.local
          while :; do
            if ! /usr/local/bin/zerotier-cli join ${zerotier_network}; then
              sleep 1
              continue
            fi

            break
          done

          rm -f /etc/rc.local
        EOF

          sysrc -f /mnt/etc/rc.conf zerotier_enable="YES"
        elif [ -f "/metadata/ts_auth_key" ]; then
          pkg -c /mnt install -y tailscale

          ts_auth_key=`head -1 -- "/metadata/ts_auth_key"`

          echo "/usr/local/bin/tailscale up --accept-dns=false --auth-key=\"${ts_auth_key}\" && rm -f /etc/rc.local" > /mnt/etc/rc.local

          sysrc -f /mnt/etc/rc.conf tailscaled_enable="YES"
        fi

        if [ -f "/metadata/timezone" ]; then
          timezone=`head -1 -- "/metadata/timezone"`

          ln -fs "/usr/share/zoneinfo/${timezone}" /mnt/etc/localtime
        fi

        if [ -f "/metadata/sshd_config" ]; then
          sysrc -f /mnt/etc/rc.conf sshd_enable="YES"
          cp /metadata/sshd_config /mnt/etc/ssh/sshd_config
        fi

        if [ -f "/metadata/ssh_key" ]; then
          cp /metadata/ssh_key /mnt/etc/ssh/authorized_keys
        fi

        if [ -f "/metadata/sysctl.conf" ]; then
          cp /metadata/sysctl.conf /mnt/etc/sysctl.conf
        fi

        if [ -f "/metadata/pkg.conf" ]; then
          mkdir -p /mnt/usr/local/etc/pkg/repos
          cp /metadata/pkg.conf /mnt/usr/local/etc/pkg/repos/Latest.conf
        fi
      </%text>
    metadata:
      - resolv.conf
      - loader.conf
      - timezone
      - sshd_config
      - ssh_key
      - sysctl.conf
      - pkg.conf
      - ts_auth_key

app.yml:

kind: appConfig
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
appName: 'myvm'
appFrom: 'appVM.appConfig'
appConfig:
  ncpu: '2'
  memory: '2G'
  disk: '30G'
  swap: '2G'
  fbsdver: '15.snap-full'
  hostname: 'myvm'
  appvm_use_pkgbase: '1'

Note: I'm using an appConfig deployment here because it is easier to customize with different types of installations.

In this article, the most relevant parameters are:

    ...
    overwrite: true
    ...
    datastore: '/var/appjail-vm/${appName}/data'
    ...
    options:
      - fstab: '/var/appjail-vm/${appName}/data vm-data <volumefs>'

When overwrite is set to true, Overlord will destroy your jail if you reapply the deployment, which is what we want to do. datastore, when specified, is used by Overlord to create that directory and the .img and .iso subdirectories (which are not so relevant here, but you will see later that they become important in other types of installations such as iso or img). And last but not least, there is the entry in fstab(5), which becomes the default data store for vm-bhyve and where the virtual machine data will be stored.

console:

$ overlord apply -f metadata.yml
$ overlord apply -f app.yml
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      state: UNFINISHED
      last_log: 2025-08-07_15h34m33s
      locked: True
      services:
        - {'name': 'vm', 'status': 66, 'jail': 'myvm'}
      up:
        operation: RUNNING
        last_update: 7.19 seconds
        job_id: 117
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      state: DONE
      last_log: 2025-08-07_15h34m33s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'myvm'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 1 minute and 32.7 seconds
        job_id: 117
        restarted: False
$ overlord get-info -f app.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      virtual-machines:
          operation: RUNNING
          last_update: 1 minute and 36.41 seconds
          job_id: 117
$ overlord get-info -f app.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      virtual-machines:
          operation: COMPLETED
          output: |
            md0 created
             md0p1 added
             md0p2 added
             md0p3 added
             /dev/md0p3: 28670.0MB (58716160 sectors) block size 32768, fragment size 4096
                using 46 cylinder groups of 625.22MB, 20007 blks, 80128 inodes.
                with soft updates
             super-block backups (for fsck_ffs -b #) at:
              192, 1280640, 2561088, 3841536, 5121984, 6402432, 7682880, 8963328, 10243776,
              11524224, 12804672, 14085120, 15365568, 16646016, 17926464, 19206912,
              20487360, 21767808, 23048256, 24328704, 25609152, 26889600, 28170048,
              29450496, 30730944, 32011392, 33291840, 34572288, 35852736, 37133184,
              38413632, 39694080, 40974528, 42254976, 43535424, 44815872, 46096320,
              47376768, 48657216, 49937664, 51218112, 52498560, 53779008, 55059456,
              56339904, 57620352
             bootcode written to md0
             partcode written to md0p1
             ifconfig_vtnet0:  -> inet 192.168.8.2/24
             defaultrouter: NO -> 192.168.8.1
             fsck_y_enable: NO -> YES
             clear_tmp_enable: NO -> YES
             dumpdev: NO -> NO
             moused_nondefault_enable: YES -> NO
             hostname:  -> myvm
             [myvm.appjail] Installing pkg-2.2.1...
             [myvm.appjail] Extracting pkg-2.2.1: .......... done
             Updating FreeBSD repository catalogue...
             [myvm.appjail] Fetching data.pkg: .......... done
             Processing entries:
             Processing entries............. done
             FreeBSD repository update completed. 36062 packages processed.
             Updating FreeBSD-kmods repository catalogue...
             [myvm.appjail] Fetching meta.conf: . done
             [myvm.appjail] Fetching data.pkg: .. done
             Processing entries: .......... done
             FreeBSD-kmods repository update completed. 199 packages processed.
             Updating FreeBSD-base repository catalogue...
             [myvm.appjail] Fetching data.pkg: ... done
             Processing entries: .......... done
             FreeBSD-base repository update completed. 561 packages processed.
             All repositories are up to date.
             The following 2 package(s) will be affected (of 0 checked):

             New packages to be INSTALLED:
                ca_root_nss: 3.108 [FreeBSD]
                tailscale: 1.84.2_1 [FreeBSD]

             Number of packages to be installed: 2

             The process will require 36 MiB more space.
             12 MiB to be downloaded.
             [myvm.appjail] [1/2] Fetching tailscale-1.84.2_1.pkg: .......... done
             [myvm.appjail] [2/2] Fetching ca_root_nss-3.108.pkg: ........ done
             Checking integrity... done (0 conflicting)
             [myvm.appjail] [1/2] Installing ca_root_nss-3.108...
             [myvm.appjail] [1/2] Extracting ca_root_nss-3.108: ....... done
             [myvm.appjail] [2/2] Installing tailscale-1.84.2_1...
             [myvm.appjail] [2/2] Extracting tailscale-1.84.2_1: ...... done
             =====
             Message from ca_root_nss-3.108:

             --
             FreeBSD does not, and can not warrant that the certification authorities
             whose certificates are included in this package have in any way been
             audited for trustworthiness or RFC 3647 compliance.

             Assessment and verification of trust is the complete responsibility of
             the system administrator.

             This package installs symlinks to support root certificate discovery
             for software that either uses other cryptographic libraries than
             OpenSSL, or use OpenSSL but do not follow recommended practice.

             If you prefer to do this manually, replace the following symlinks with
             either an empty file or your site-local certificate bundle.

               * /etc/ssl/cert.pem
               * /usr/local/etc/ssl/cert.pem
               * /usr/local/openssl/cert.pem
             tailscaled_enable:  -> YES
             sshd_enable: NO -> YES
             vm_list:  -> myvm
             Starting myvm
               * found guest in /vm/myvm
               * booting...
            [00:00:00] [ debug ] Cloning https://github.com/AppJail-makejails/pkgbase as /usr/local/appjail/cache/tmp/.appjail/appjail.MKDRFh8cuH ...
             [00:00:00] [ info  ] [pkgbase] pkgbase (arch:amd64, tag:15.snap-full): already up to date.
             + set -o pipefail
             + . /metadata/environment
             + export 'HOSTNAME=myvm'
             + sysrc -f /mnt/etc/rc.conf 'ifconfig_vtnet0=inet 192.168.8.2/24'
             + sysrc -f /mnt/etc/rc.conf 'defaultrouter=192.168.8.1'
             + sysrc -f /mnt/etc/rc.conf 'fsck_y_enable=YES'
             + sysrc -f /mnt/etc/rc.conf 'clear_tmp_enable=YES'
             + sysrc -f /mnt/etc/rc.conf 'dumpdev=NO'
             + sysrc -f /mnt/etc/rc.conf 'moused_nondefault_enable=NO'
             + sysrc -f /mnt/etc/rc.conf 'hostname=myvm'
             + [ -f /metadata/resolv.conf ]
             + cp -a /metadata/resolv.conf /mnt/etc/resolv.conf
             + [ -f /metadata/loader.conf ]
             + cp /metadata/loader.conf /mnt/boot/loader.conf
             + [ -f /metadata/zerotier_network ]
             + [ -f /metadata/ts_auth_key ]
             + pkg -c /mnt install -y tailscale
             pkg: Warning: Major OS version upgrade detected.  Running "pkg bootstrap -f" recommended
             + head -1 -- /metadata/ts_auth_key
             + ts_auth_key=[REDACTED]
             + echo '/usr/local/bin/tailscale up --accept-dns=false --auth-key="[REDACTED]" && rm -f /etc/rc.local'
             + sysrc -f /mnt/etc/rc.conf 'tailscaled_enable=YES'
             + [ -f /metadata/timezone ]
             + head -1 -- /metadata/timezone
             + timezone=America/Caracas
             + ln -fs /usr/share/zoneinfo/America/Caracas /mnt/etc/localtime
             + [ -f /metadata/sshd_config ]
             + sysrc -f /mnt/etc/rc.conf 'sshd_enable=YES'
             + cp /metadata/sshd_config /mnt/etc/ssh/sshd_config
             + [ -f /metadata/ssh_key ]
             + cp /metadata/ssh_key /mnt/etc/ssh/authorized_keys
             + [ -f /metadata/sysctl.conf ]
             + cp /metadata/sysctl.conf /mnt/etc/sysctl.conf
             + [ -f /metadata/pkg.conf ]
             + mkdir -p /mnt/usr/local/etc/pkg/repos
             + cp /metadata/pkg.conf /mnt/usr/local/etc/pkg/repos/Latest.conf
          last_update: 21.26 seconds
          job_id: 117

We can log into our jail and install a package. After doing so, let's reapply the deployment to see what happens.

$ ssh root@100.78.139.110
The authenticity of host '100.78.139.110 (100.78.139.110)' can't be established.
ED25519 key fingerprint is SHA256:+0KHV4m5X42UZgZ2N+qtJDwm4EFl0yhvYo6VRenZlxc.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '100.78.139.110' (ED25519) to the list of known hosts.
root@myvm:~ # pkg install -y fastfetch
Bootstrapping pkg from pkg+http://pkg.FreeBSD.org/FreeBSD:15:amd64/latest, please wait...
Verifying signature with trusted certificate pkg.freebsd.org.2013102301... done
Installing pkg-2.2.1...
Newer FreeBSD version for package pkg:
To ignore this error set IGNORE_OSVERSION=yes
- package: 1500054
- running userland: 1500053
Ignore the mismatch and continue? [y/N]: y
Extracting pkg-2.2.1: 100%
Updating FreeBSD repository catalogue...
pkg: Repository FreeBSD has a wrong packagesite, need to re-create database
Fetching meta.conf: 100%    179 B   0.2kB/s    00:01
Fetching data.pkg: 100%   10 MiB 230.3kB/s    00:46
Processing entries:   0%
Processing entries: 100%
FreeBSD repository update completed. 36062 packages processed.
Updating FreeBSD-kmods repository catalogue...
FreeBSD-kmods repository is up to date.
Updating FreeBSD-base repository catalogue...
FreeBSD-base repository is up to date.
All repositories are up to date.
Updating database digests format: 100%
The following 5 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        FreeBSD-bluetooth: 15.snap20250721070021 [FreeBSD-base]
        FreeBSD-libsdp: 15.snap20250707041723 [FreeBSD-base]
        fastfetch: 2.48.1 [FreeBSD]
        hwdata: 0.397,1 [FreeBSD]
        yyjson: 0.11.1 [FreeBSD]

Number of packages to be installed: 5

The process will require 12 MiB more space.
2 MiB to be downloaded.
[1/5] Fetching FreeBSD-bluetooth-15.snap20250721070021.pkg: 100%  182 KiB 186.6kB/s    00:01
[2/5] Fetching yyjson-0.11.1.pkg: 100%  126 KiB 129.1kB/s    00:01
[3/5] Fetching fastfetch-2.48.1.pkg: 100%  446 KiB 228.3kB/s    00:02
[4/5] Fetching hwdata-0.397,1.pkg: 100%    2 MiB 344.8kB/s    00:05
[5/5] Fetching FreeBSD-libsdp-15.snap20250707041723.pkg: 100%    8 KiB   8.3kB/s    00:01
Checking integrity... done (0 conflicting)
[1/5] Installing FreeBSD-bluetooth-15.snap20250721070021...
[1/5] Extracting FreeBSD-bluetooth-15.snap20250721070021: 100%
[2/5] Installing FreeBSD-libsdp-15.snap20250707041723...
[2/5] Extracting FreeBSD-libsdp-15.snap20250707041723: 100%
[3/5] Installing hwdata-0.397,1...
[3/5] Extracting hwdata-0.397,1: 100%
[4/5] Installing yyjson-0.11.1...
[4/5] Extracting yyjson-0.11.1: 100%
[5/5] Installing fastfetch-2.48.1...
[5/5] Extracting fastfetch-2.48.1: 100%
root@myvm:~ # fastfetch 
```                        `        root@myvm
  ` `.....---.......--.```   -/     ---------
  +o   .--`         /y:`      +.    Kernel: FreeBSD 15.0-CURRENT
   yo`:.            :o      `+-     Uptime: 7 mins
    y/               -/`   -o/      Shell: sh
   .-                  ::/sy+:.     Terminal: /dev/pts/0
   /                     `--  /     CPU: Intel(R) Core(TM) i5-7500T (2) @ 3.30 GHz
  `:                          :`    Memory: 342.90 MiB / 1.96 GiB (17%)
  `:                          :`    Swap: 0 B / 2.00 GiB (0%)
   /                          /     Disk (/): 501.37 MiB / 27.12 GiB (2%) - ufs
   .-                        -.     Local IP (vtnet0): 192.168.8.2/24
    --                      -.      Locale: C.UTF-8
     `:`                  `:`
       .--             `--.                                 
          .---.....----.                                    
root@myvm:~ # 
Shared connection to 100.78.139.110 closed.
$ overlord apply -f app.yml
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      state: UNFINISHED
      last_log: 2025-08-07_15h47m57s
      locked: True
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'myvm'}
      up:
        operation: RUNNING
        last_update: 13.58 seconds
        job_id: 119
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      state: DONE
      last_log: 2025-08-07_15h47m57s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'myvm'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 55.5 seconds
        job_id: 119
        restarted: False
$ overlord get-info -f app.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    myvm:
      virtual-machines:
          operation: COMPLETED
          output: |
            vm_list:  -> myvm
             Starting myvm
               * found guest in /vm/myvm
               * booting...
          last_update: 1 minute and 22.62 seconds
          job_id: 119

There are more changes that can be noticed here: the fact that the deployment takes less time since the virtual machine is not customized like the first time and simply starts up.

$ ssh root@100.78.139.110
Last login: Thu Aug  7 15:47:46 2025 from 100.65.139.52
root@myvm:~ # fastfetch 
```                        `        root@myvm
  ` `.....---.......--.```   -/     ---------
  +o   .--`         /y:`      +.    Kernel: FreeBSD 15.0-CURRENT
   yo`:.            :o      `+-     Uptime: 4 mins
    y/               -/`   -o/      Shell: sh
   .-                  ::/sy+:.     Terminal: /dev/pts/0
   /                     `--  /     CPU: Intel(R) Core(TM) i5-7500T (2) @ 3.30 GHz
  `:                          :`    Memory: 192.38 MiB / 1.96 GiB (10%)
  `:                          :`    Swap: 0 B / 2.00 GiB (0%)
   /                          /     Disk (/): 564.89 MiB / 27.12 GiB (2%) - ufs
   .-                        -.     Local IP (vtnet0): 192.168.8.2/24
    --                      -.      Locale: C.UTF-8
     `:`                  `:`
       .--             `--.                                 
          .---.....----.                                    
root@myvm:~ # 
Shared connection to 100.78.139.110 closed.

Even if our jail is destroyed over and over again, it's not a problem because the data is stored outside the jail.

$ tree /var/appjail-vm/myvm
/var/appjail-vm/myvm
└── data
    └── myvm
        ├── console
        ├── disk0.img
        ├── myvm.conf
        ├── run.lock
        └── vm-bhyve.log

3 directories, 5 files

We're not done yet, there's more to say here.

debian.yml:

kind: vmJail
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
vmName: 'debian'
makejail: 'gh+DtxdF/vm-makejail'
overwrite: true
datastore: '/var/appjail-vm/debian/data'
options:
  - pkg: grub2-bhyve
  - pkg: qemu-tools
  - fstab: '/var/appjail-vm/debian/data vm-data <volumefs>'
  - fstab: '/var/os-images/uploads /vm/.img nullfs ro'
template:
  loader: 'grub'
  cpu: '4'
  memory: '4G'
  network0_type: 'virtio-net'
  network0_switch: 'public'
  wired_memory: 'YES'
  grub_run_partition: '1'
  grub_run_dir: '/boot/grub'
  uuid: 'c0f801f2-8b43-4004-b778-d701879d7359'
diskLayout:
  driver: 'ahci-hd'
  size: '40G'
  from:
    type: 'img'
    imgFile: 'debian-13-generic-amd64-daily.qcow2'
cloud-init:
  meta-data:
    instance-id: 'c0f801f2-8b43-4004-b778-d701879d7359'
    local-hostname: debian.lan
  network-config:
    version: 2
    ethernets:
      id0:
        match:
            name: 'enp0s5'
        addresses:
          - 192.168.8.2/24
        routes:
            - to: default
              via: 192.168.8.1
        nameservers:
          search: []
          addresses: [172.0.0.1]
  user-data:
    resize_rootfs: True
    manage_etc_hosts: localhost
    user:
        name: user
        homedir: '/user'
        ssh_authorized_keys:
            - !ENV '${SSH_KEY}'
        sudo: 'ALL=(ALL) NOPASSWD:ALL'
    package_update: True
    package_upgrade: True
    runcmd:
      - ['sh', '-c', 'curl -x socks5://10.0.0.50:9050 -fsSL https://tailscale.com/install.sh | sh']
      - ['tailscale', 'up', !ENV '--auth-key=${TS_AUTH_KEY}']

The above deployment is similar to the first one, but there is an additional entry:

...
options:
  ...
  - fstab: '/var/appjail-vm/debian/data vm-data <volumefs>'
  - fstab: '/var/os-images/uploads /vm/.img nullfs ro'

And this is where datastore is most valuable: datastore will create the subdirectories .img and .iso in the specified directory (datastore's value), which is necessary before mounting the directory containing the images (in this case /var/os-images/uploads) in /vm/.iso or /vm/.img. This is necessary because fstab(5) will first attempt to mount /var/appjail-vm/debian/data on the vm-data volume, and if it does not contain the .iso and .img subdirectories, the next entry /var/os-images/uploads, which will be mounted on /vm/.img, will fail.

console:

$ overlord apply -f debian.yml
$ overlord get-info -f debian.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    debian:
      state: UNFINISHED
      last_log: 2025-08-07_16h15m49s
      locked: True
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'debian'}
      up:
        operation: RUNNING
        last_update: 30.9 seconds
        job_id: 123
$ overlord get-info -f debian.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    debian:
      state: DONE
      last_log: 2025-08-07_16h15m49s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'debian'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 6.1 seconds
        job_id: 123
        restarted: False
$ overlord get-info -f debian.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    debian:
      virtual-machines:
          operation: COMPLETED
          output: |
            vm_list:  -> debian
             Starting debian
               * found guest in /vm/debian
               * booting...
          last_update: 54.26 seconds
          job_id: 123
$ ssh user@100.89.3.114
The authenticity of host '100.89.3.114 (100.89.3.114)' can't be established.
ED25519 key fingerprint is SHA256:QJvf2940g8JlsnTiWosIYR8VKHeBjfooxoj2qmS+jHE.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '100.89.3.114' (ED25519) to the list of known hosts.
Linux debian 6.12.35+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.35-1 (2025-07-03) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
user@debian:~$ sudo apt install fastfetch
Installing:
  fastfetch

Installing dependencies:
  libyyjson0

Summary:
  Upgrading: 0, Installing: 2, Removing: 0, Not Upgrading: 0
  Download size: 632 kB
  Space needed: 2083 kB / 38.9 GB available

Continue? [Y/n] y
Get:1 file:/etc/apt/mirrors/debian.list Mirrorlist [30 B]
Get:2 https://deb.debian.org/debian trixie/main amd64 libyyjson0 amd64 0.10.0+ds-1+b1 [89.3 kB]
Get:3 https://deb.debian.org/debian trixie/main amd64 fastfetch amd64 2.40.4+dfsg-1 [543 kB]
Fetched 632 kB in 1s (481 kB/s)
Selecting previously unselected package libyyjson0:amd64.
(Reading database ... 29601 files and directories currently installed.)
Preparing to unpack .../libyyjson0_0.10.0+ds-1+b1_amd64.deb ...
Unpacking libyyjson0:amd64 (0.10.0+ds-1+b1) ...
Selecting previously unselected package fastfetch.
Preparing to unpack .../fastfetch_2.40.4+dfsg-1_amd64.deb ...
Unpacking fastfetch (2.40.4+dfsg-1) ...
Setting up libyyjson0:amd64 (0.10.0+ds-1+b1) ...
Setting up fastfetch (2.40.4+dfsg-1) ...
Processing triggers for man-db (2.13.1-1) ...
Processing triggers for libc-bin (2.41-12) ...
user@debian:~$ fastfetch
        _,met$$$$$gg.          user@debian
     ,g$$$$$$$$$$$$$$$P.       -----------
   ,g$$P""       """Y$$.".     OS: Debian GNU/Linux 13 (trixie) x86_64
  ,$$P'              `$$$.     Host: BHYVE (1.0)
',$$P       ,ggs.     `$$b:    Kernel: Linux 6.12.35+deb13-amd64
`d$$'     ,$P"'   .    $$$     Uptime: 9 mins
 $$P      d$'     ,    $$P     Packages: 327 (dpkg)
 $$:      $$.   -    ,d$$'     Shell: bash 5.2.37
 $$;      Y$b._   _,d$P'       Terminal: /dev/pts/0
 Y$$.    `.`"Y$$$$P"'          CPU: 4 x Intel(R) Core(TM) i5-7500T (4) @ 3.30 GHz
 `$$b      "-.__               Memory: 445.67 MiB / 3.83 GiB (11%)
  `Y$$b                        Swap: Disabled
   `Y$$.                       Disk (/): 1.27 GiB / 39.17 GiB (3%) - ext4
     `$$b.                     Local IP (enp0s5): 192.168.8.2/24
       `Y$$b.                  Locale: C.UTF-8
         `"Y$b._
             `""""

user@debian:~$
logout
Shared connection to 100.89.3.114 closed.
$ overlord apply -f debian.yml
$ overlord get-info -f debian.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    debian:
      state: UNFINISHED
      last_log: 2025-08-07_16h30m27s
      locked: True
      services:
        - {'name': 'vm', 'status': 66, 'jail': 'debian'}
      up:
        operation: RUNNING
        last_update: 12.67 seconds
        job_id: 125
$ overlord get-info -f debian.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888                               
  entrypoint: main            
  chain: None      
  labels:                      
    - all                   
    - desktop                                                                  
    - services             
    - vm-only                                                                  
  projects:          
    debian:                                                                    
      state: DONE
      last_log: 2025-08-07_16h30m27s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'debian'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 3 minutes and 33.49 seconds
        job_id: 125
        restarted: False
$ overlord get-info -f debian.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    debian:
      virtual-machines:
          operation: COMPLETED
          output: |
            vm_list:  -> debian
             Starting debian
               * found guest in /vm/debian
               * booting...
          last_update: 3 minutes and 34.73 seconds
          job_id: 125
$ ssh user@100.89.3.114
Linux debian 6.12.38+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.38-1 (2025-07-16) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Thu Aug  7 20:28:43 2025 from 100.65.139.52
user@debian:~$ fastfetch
        _,met$$$$$gg.          user@debian
     ,g$$$$$$$$$$$$$$$P.       -----------
   ,g$$P""       """Y$$.".     OS: Debian GNU/Linux 13 (trixie) x86_64
  ,$$P'              `$$$.     Host: BHYVE (1.0)
',$$P       ,ggs.     `$$b:    Kernel: Linux 6.12.38+deb13-amd64
`d$$'     ,$P"'   .    $$$     Uptime: 5 mins
 $$P      d$'     ,    $$P     Packages: 327 (dpkg)
 $$:      $$.   -    ,d$$'     Shell: bash 5.2.37
 $$;      Y$b._   _,d$P'       Terminal: /dev/pts/0
 Y$$.    `.`"Y$$$$P"'          CPU: 4 x Intel(R) Core(TM) i5-7500T (4) @ 3.30 GHz
 `$$b      "-.__               Memory: 348.50 MiB / 3.83 GiB (9%)
  `Y$$b                        Swap: Disabled
   `Y$$.                       Disk (/): 1.27 GiB / 39.17 GiB (3%) - ext4
     `$$b.                     Local IP (enp0s5): 192.168.8.2/24
       `Y$$b.                  Locale: C.UTF-8
         `"Y$b._
             `""""

user@debian:~$
logout
Shared connection to 100.89.3.114 closed.

And the last character in this scenario is the iso installation type, which is a little different from the others.

app.yml:

kind: vmJail
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
vmName: 'fbsd143'
makejail: 'gh+DtxdF/vm-makejail'
overwrite: true
datastore: '/var/appjail-vm/fbsd143/data'
options:
  - fstab: '/var/appjail-vm/fbsd143/data vm-data <volumefs>'
  - fstab: '/var/os-images/uploads /vm/.iso nullfs ro'
template:
  loader: 'uefi'
  cpu: '4'
  memory: '2G'
  graphics: 'yes'
  graphics_listen: '0.0.0.0'
  graphics_res: '1280x720'
  xhci_mouse: 'yes'
  network0_type: 'virtio-net'
  network0_switch: 'public'
  wired_memory: 'YES'
diskLayout:
  driver: 'nvme'
  size: '40G'
  from:
    type: 'iso'
    isoFile: 'FreeBSD-14.3-RELEASE-amd64-disc1.iso'

Similar to the others mentioned, with the subtle difference that instead of /vm/.img we use /vm/.iso.

console:

$ overlord apply -f app.yml
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888      
  entrypoint: main
  chain: None   
  labels: 
    - all     
    - desktop    
    - services              
    - vm-only             
  projects:        
    fbsd143:                 
      state: UNFINISHED    
      last_log: 2025-08-07_16h47m22s
      locked: True
      services: 
        - {'name': 'vm', 'status': 0, 'jail': 'fbsd143'}
      up:
        operation: RUNNING
        last_update: 42.16 seconds                                             
        job_id: 129
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      state: DONE
      last_log: 2025-08-07_16h47m22s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'fbsd143'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 2 minutes and 37.92 seconds
        job_id: 129
        restarted: False
$ overlord get-info -f app.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      virtual-machines:
          operation: COMPLETED
          output: |
            Starting fbsd143
               * found guest in /vm/fbsd143
               * booting...
          last_update: 2 minutes and 46.78 seconds
          job_id: 129

After installing the guest operating system from our VNC client, mark this deployment as installed and reapply the deployment, and Overlord will mark this virtual machine as complete, however, this will not destroy the jail. After marking this virtual machine as complete, Overlord can recreate the jail after you reapply the deployment. Therefore, the deployment must be applied at least twice.

app.yml:

kind: vmJail
datacenters:
  main:
    entrypoint: !ENV '${ENTRYPOINT}'
    access_token: !ENV '${TOKEN}'
deployIn:
  labels:
    - desktop
vmName: 'fbsd143'
makejail: 'gh+DtxdF/vm-makejail'
overwrite: true
datastore: '/var/appjail-vm/fbsd143/data'
options:
  - fstab: '/var/appjail-vm/fbsd143/data vm-data <volumefs>'
  - fstab: '/var/os-images/uploads /vm/.iso nullfs ro'
template:
  loader: 'uefi'
  cpu: '4'
  memory: '2G'
  graphics: 'yes'
  graphics_listen: '0.0.0.0'
  graphics_res: '1280x720'
  xhci_mouse: 'yes'
  network0_type: 'virtio-net'
  network0_switch: 'public'
  wired_memory: 'YES'
diskLayout:
  driver: 'nvme'
  size: '40G'
  from:
    type: 'iso'
    isoFile: 'FreeBSD-14.3-RELEASE-amd64-disc1.iso'
    installed: true

console:

$ overlord apply -f app.yml
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      state: DONE
      last_log: 2025-08-07_16h47m22s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'fbsd143'}
      up:
        operation: NOOP
        last_update: 6.5 seconds
        job_id: 131
$ overlord get-info -f app.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      virtual-machines:
          operation: COMPLETED
          output: |
            vm_list:  -> fbsd143
             Starting fbsd143
               * found guest in /vm/fbsd143
               * booting...
          last_update: 24.17 seconds
          job_id: 131
$ overlord apply -f app.yml
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      state: DONE
      last_log: 2025-08-07_16h47m22s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'fbsd143'}
      up:
        operation: RUNNING
        last_update: 3.28 seconds
        job_id: 133
$ overlord get-info -f app.yml -t projects --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      state: DONE
      last_log: 2025-08-07_17h00m24s
      locked: False
      services:
        - {'name': 'vm', 'status': 0, 'jail': 'fbsd143'}
      up:
        operation: COMPLETED
        output:
         rc: 0
         stdout: {'errlevel': 0, 'message': None, 'failed': []}
        last_update: 8.72 seconds
        job_id: 133
        restarted: False
$ overlord get-info -f app.yml -t vm --filter-per-project
datacenter: http://controller.namespace.lan:8888
  entrypoint: main
  chain: None
  labels:
    - all
    - desktop
    - services
    - vm-only
  projects:
    fbsd143:
      virtual-machines:
          operation: COMPLETED
          output: |
            vm_list:  -> fbsd143
             Starting fbsd143
               * found guest in /vm/fbsd143
               * booting...
          last_update: 22.45 seconds
          job_id: 133

Clone this wiki locally