I am using Incus on a few Debian hosts in my homelab where Docker would be a bit too limited. It is especially nice for small services which need a complete Linux userspace, their own network config or a persistent system container.
Incus is the community fork of LXD and is a good option if you do not like Canonical's packaging and licensing direction. The commands will feel familiar if you used LXD before, but I prefer keeping the Incus setup separate and explicit.
Installing Incus on Debian
On current Debian releases, Incus is available directly from the Debian repositories. I start with the daemon and client packages:
# apt update
# apt install incus incus-client
My normal user needs access to the Incus socket. Members of incus-admin can fully manage containers and host resources, so this is effectively administrative access to the machine.
# usermod -aG incus-admin yves
Log out and in again afterwards, or start a new shell with the new group membership.
$ id -nG
yves adm cdrom sudo incus-admin
For a simple single-server setup I use the interactive minimal initialization. This creates the default directory based storage pool. It is not as space efficient as ZFS or btrfs, but it keeps the first setup simple.
# incus admin init --minimal
The minimal setup does not create a managed bridge. I create one myself and attach it to the default profile:
# incus network create incusbr0 ipv4.address=auto ipv6.address=none
# incus profile device add default eth0 nic network=incusbr0 name=eth0
Now a normal Debian container should get an address through Incus' managed DHCP service.
$ incus launch images:debian/13 test-debian
Launching test-debian
$ incus list
+-------------+---------+-----------------------+------+-----------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+-------------+---------+-----------------------+------+-----------+-----------+
| test-debian | RUNNING | 10.137.252.123 (eth0) | | CONTAINER | 0 |
+-------------+---------+-----------------------+------+-----------+-----------+
The
dirstorage driver is fine for testing and a few small containers. For bigger homelab installations I would use ZFS or btrfs, mostly because snapshots and quotas are much more useful there.
Unprivileged containers by default
Incus containers are unprivileged by default. Root inside the container is mapped to an unprivileged UID range on the Debian host. This is one of the reasons why I prefer Incus for system containers instead of just running everything with a privileged Docker socket.
You should not set security.privileged=true unless you really know why an application needs it. Most services do not.
$ incus config get test-debian security.privileged
An empty value here means the default is used, which is false. I sometimes set it explicitly for important containers, just so the intention is visible in the configuration.
$ incus config set test-debian security.privileged false
You can inspect the user namespace mapping from inside the container:
$ incus exec test-debian -- cat /proc/1/uid_map
0 100000 65536
The exact host UID range can differ, but container UID 0 should map to a non-root host UID.
A common pitfall is bind mounting a host directory into an unprivileged container. The host ownership must match the mapped UID range, otherwise the container sees an unreadable directory. I usually avoid direct bind mounts for application data and use Incus storage volumes instead.
Separating services with projects
For my homelab I use Incus projects to separate unrelated services. A project is not a replacement for separate physical hosts, but it is a clean way to keep test containers, public services and internal tools apart.
Here I create an apps project with its own profiles, images, networks and storage volumes:
$ incus project create apps \
-c features.images=true \
-c features.networks=true \
-c features.profiles=true \
-c features.storage.volumes=true
The project needs its own network because managed networks are otherwise owned by the default project.
$ incus network create --project apps appsbr0 ipv4.address=auto ipv6.address=none
$ incus profile device add --project apps default eth0 nic \
network=appsbr0 name=eth0
I can then launch a container directly into the project:
$ incus launch --project apps images:debian/13 status-page
Launching status-page
Using explicit project names is less error prone than constantly switching the local Incus context.
$ incus list --project apps
$ incus exec --project apps status-page -- bash
For simple web services I still use rootless Podman and Quadlet sometimes. My Rootless Podman with Quadlet: systemd Services and Automatic Updates setup is lighter for one-process containers. Incus is what I use when the service needs a full Debian environment, systemd, several processes or a separate network.
Portable backup exports
Snapshots are useful, but they are not backups when they live on the same storage pool as the original container. I export important instances to another disk or a backup host.
A portable export does not use storage-driver-specific optimization. It is slower and somewhat larger, but I can import it on a different Incus host with another storage backend.
# install -d -m 0700 /srv/incus-backups
$ incus export --project apps status-page \
/srv/incus-backups/status-page-$(date +%F).tar.zst \
--compression=zstd
Restoring the container on another Incus machine is straightforward:
$ incus import /srv/incus-backups/status-page-2026-11-08.tar.zst
I also save the project, network and profile configuration beside the exports. The instance backup contains the container, but not the complete project setup around it.
$ incus project show apps > /srv/incus-backups/apps-project.yaml
$ incus profile show --project apps default > /srv/incus-backups/apps-profile.yaml
$ incus network show --project apps appsbr0 > /srv/incus-backups/apps-network.yaml
For daily exports I use a small script and trigger it with a systemd timer:
#!/bin/bash
set -euo pipefail
backup_root=/srv/incus-backups
date_dir="$backup_root/$(date +%F)"
tmp_dir="$date_dir.tmp"
mkdir -p "$tmp_dir"
incus project show apps > "$tmp_dir/project.yaml"
incus profile show --project apps default > "$tmp_dir/profile.yaml"
incus network show --project apps appsbr0 > "$tmp_dir/network.yaml"
incus list --project apps --format csv -c n | while read -r name; do
incus export --project apps "$name" \
"$tmp_dir/$name.tar.zst" --compression=zstd
done
mv "$tmp_dir" "$date_dir"
find "$backup_root" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} +
An Incus export is filesystem-consistent, but not automatically application-consistent. For databases I still create a PostgreSQL or MariaDB dump inside the container before the export. This matters more than the container backup itself.
I copy the resulting backup directory off the Incus host afterwards. The same rule applies here as with my Linux Backup changed files of the last 7 days script: a backup on the same machine only protects against accidental deletion, not against a dead disk or broken host.
If you know a better way to handle Incus exports in a homelab, let me know.