Setting up a host without the package

The Debian package does a few things that you have to do yourself on a development or test host where the package is not installed: the user, the subordinate uid/gid ranges, lingering, the data directories, the Containerfiles. This page lists all of them, on a fresh Debian 13 (trixie) host. Everything here is run as root unless it says otherwise.

Packages

apt install podman passt uidmap git btrfs-progs systemd-container \
    build-essential

btrfs-progs is not used by dzo (it talks to the kernel itself) but you need mkfs.btrfs and btrfs subvolume list to create and inspect the filesystem. For building dzo itself see Development; the Go toolchain is the current upstream one, not Debian’s.

Backports kernel

Snapshots and subvolume handling need Linux 6.12 or newer. trixie ships 6.12, which is enough; use the backports kernel if you want a newer btrfs.

cat > /etc/apt/sources.list.d/trixie-backports.sources <<'EOT'
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie-backports
Components: main
Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp
EOT
apt update
apt install -t trixie-backports linux-image-amd64
reboot
uname -r        # the new kernel

On a cloud image use linux-image-cloud-amd64 instead. Packages from backports are only installed when you ask for them with -t trixie-backports, so the rest of the system stays on trixie.

The service user and group

groupadd dayz
useradd --gid dayz --home-dir /var/lib/dzo --no-create-home \
    --shell /bin/bash --comment "DayZ server operator" dayz

(Not --system: a system user, UID below 1000, has no journal of its own and dzo logs could not read its services’ logs. The package uses systemd-sysusers with r - 1000-59999 and u dayz - "DayZ server operator" /var/lib/dzo -, which picks a free UID from that range; the shell is nologin there. On a development host a real shell makes sudo -iu dayz and machinectl shell easier.) The home directory is not created here: it is the mount point of the btrfs filesystem below, so it is created once, empty, together with the mount.

Rootless podman needs a range of subordinate ids, and the user’s services must keep running without a login:

usermod --add-subuids 100000-165535 --add-subgids 100000-165535 dayz
grep '^dayz:' /etc/subuid /etc/subgid
loginctl enable-linger dayz

Check that the ranges do not overlap with another user’s in /etc/subuid. To get a shell with a working user systemd session (systemctl --user), use machinectl shell dayz@, or sudo -iu dayz after enabling lingering and setting XDG_RUNTIME_DIR=/run/user/$(id -u dayz).

To work on the host as your own user as well, add yourself to the group (usermod -aG dayz <you>) and make what you need group-readable; do not run dzo as root, dzo setup refuses it.

The btrfs filesystem

Instance data and snapshots must be on one btrfs filesystem. Pick one of these.

A whole disk (here a second disk /dev/vdb; check with lsblk first, the next command destroys what is on it):

mkfs.btrfs -L dzo /dev/vdb

A logical volume, if you use LVM:

lvcreate --name dzo --size 100G vg0
mkfs.btrfs -L dzo /dev/vg0/dzo

A file on an existing filesystem is enough for a throw-away test:

truncate -s 40G /var/lib/dzo-btrfs.img
mkfs.btrfs -L dzo /var/lib/dzo-btrfs.img
# mount with -o loop,... instead of the fstab line below

Mount it on the data directory (the default is /var/lib/dzo; the user’s home is on it) with the options dzo wants:

echo 'LABEL=dzo /var/lib/dzo btrfs noatime,compress=zstd:1,user_subvol_rm_allowed 0 0' >> /etc/fstab
systemctl daemon-reload
mkdir -p /var/lib/dzo
mount /var/lib/dzo
chown dayz:dayz /var/lib/dzo
chmod 0750 /var/lib/dzo
findmnt -no FSTYPE,OPTIONS /var/lib/dzo

If /var/lib/dzo already contains files (for example a home directory that was created earlier), mount on a temporary directory, copy them over with cp -a and mount again.

What the users need for snapshots

dzo creates, snapshots and deletes subvolumes as the unprivileged dayz user with the btrfs ioctls, so no sudo rule or helper is needed. What it relies on:

  • The user owns paths.instances and paths.snapshots (the chown above, or the directories dzo setup creates below an owned data directory). Creating a subvolume or snapshot only needs write access to the directory it goes into.

  • user_subvol_rm_allowed in the mount options. Without it only the owner’s rmdir of an empty subvolume works, so dzo deletes a snapshot file by file, which is slow for a big instance. Enable it without a reboot with mount -o remount,user_subvol_rm_allowed /var/lib/dzo.

  • Both directories on the same filesystem, because a snapshot cannot cross filesystems. If you moved paths.snapshots elsewhere, dzo backup says so.

Do not use noexec or nosuid options that stop the game server binary from running if you keep server builds on this filesystem. Quotas are not needed; leave them off, they slow snapshot deletion.

Data directories and configuration

install -d -o dayz -g dayz -m 0750 /run/dzo /run/dzo/status
install -d /etc/dzo

/run is a tmpfs, so these two directories vanish at reboot. The package recreates them with a tmpfiles.d snippet; do the same. /var/lib/dzo is the user’s home and the btrfs mount point, so it is not part of the snippet (it was created with the mount above):

cat > /etc/tmpfiles.d/dzo.conf <<'EOT'
d /run/dzo 0750 dayz dayz -
d /run/dzo/status 0750 dayz dayz -
EOT

Write /etc/dzo/config.yaml as described in Installation and Configuration (site.remote points at your site repository; use a placeholder-free path of your own, or a local git directory for tests).

The dzo binary and its files

From a checkout of this repository, as your own user:

make build
make servermods          # needs the submodules

and as root:

install -m 0755 bin/dzo /usr/local/bin/dzo
install -d /usr/share/dzo
cp -a images /usr/share/dzo/images
cp -a dist/servermods /usr/share/dzo/servermods

dzo setup --images-dir <dir> takes another directory for the Containerfiles, so you can leave them in the checkout. The dzo-* timers and quadlets that dzo writes call the binary named by binary: in config.yaml (default /usr/bin/dzo), and the containers mount it.

A running binary cannot be overwritten (“Text file busy”), but it can be replaced: write the new build next to it and mv it over the old one. That needs a directory you may write to, which /usr/bin is not for the service user. So on a host where you deploy by hand, set binary: /var/lib/dzo/bin/dzo in config.yaml, run dzo units sync once, and deploy with install -m 0755 bin/dzo /var/lib/dzo/bin/dzo.new && mv /var/lib/dzo/bin/dzo.new /var/lib/dzo/bin/dzo. Running processes (and the containers, which mount the old file) keep the old build until they are restarted.

Check and continue

sudo -iu dayz podman info --format '{{.Host.Security.Rootless}}'   # true
sudo -iu dayz dzo setup --dry-run --images-dir /usr/share/dzo/images
sudo -iu dayz dzo setup --images-dir /usr/share/dzo/images

Then log in to Steam and continue as in Installation and Quick start: the first server.

Testing in a virtual machine

A Debian 13 cloud image under libvirt is a convenient test host. Create it with virt-install --osinfo debian13 --boot uefi (the image does not boot with BIOS), attach a second disk for the btrfs filesystem and bridge it to your network, then follow this page. A game server only accepts BattlEye connections from real players if the VM is reachable from the internet; local tests (boot test, A2S query, RCon) work from the host network alone.