SSD Nodes Learn 🎉 VPS from $5.50/mo
Guides Matt ConnorBy Matt Connor

Incus system containers on a VPS

Incus gives you a container with its own init, users and services. Set one up on a VPS: virtualisation checks, storage pools, networking, and what breaks.

What an Incus system container is

Incus system containers on a VPS give you a whole machine with its own init system and its own user accounts, not a single process with a filesystem attached. The container boots, runs an init as PID 1, and answers systemctl. It shares the host's kernel, so it is not a virtual machine. Everything above the kernel behaves like one.

Incus is the community fork of LXD, maintained under the Linux Containers project. The client command is incus. It also runs real virtual machines through QEMU when you pass --vm, but the system container is the reason most people install it, and it is what the rest of this guide covers.

Why Docker comparisons mislead people

Docker packages one process. Incus packages one operating system. The Incus documentation states the split directly: "Application containers (as provided by, for example, Docker) package a single process or application. System containers, on the other hand, simulate a full operating system similar to what you would be running on a host or in a virtual machine."

That difference changes what you do with the thing every day.

  • A Docker image has no init, so systemctl inside it fails. An Incus container runs an init system, so services and timers work the way they do on a server.
  • A Docker container is meant to be destroyed and rebuilt from a Dockerfile. An Incus container is meant to be kept, patched and snapshotted.
  • A Docker image is a build artefact you push to a registry. An Incus instance is state on disk in a storage pool, and you move it with incus export.
  • Docker isolates a workload. Incus isolates a machine, so one container can hold several workloads and several user accounts.

You can run Docker inside an Incus system container. You would not run Incus inside a Docker application container. If one process per container with an image build step is what you actually want, Podman and Docker on a VPS is the comparison to read first. If you want a separate kernel per workload instead of a shared one, Firecracker microVMs on a VPS goes the other direction.

Will Incus run inside a VPS?

It depends on your VPS's virtualisation type and on its kernel, so check both before you install anything. Do not take a provider's marketing page for it. Run these four commands on the box.

systemd-detect-virt
uname -r
stat -fc %T /sys/fs/cgroup
cat /sys/fs/cgroup/cgroup.controllers

systemd-detect-virt printing kvm or qemu means your VPS is a virtual machine with its own kernel. That is the easy case, because Incus then behaves as it would on hardware. Printing lxc, lxc-libvirt or openvz means your VPS is itself a container sharing the provider's kernel. Incus containers inside that are nested containers, and nesting works only if the provider enabled it on your container. You cannot enable it from inside, because the setting lives on the host you do not control.

stat -fc %T /sys/fs/cgroup should print cgroup2fs. Anything else means the box is on a cgroup (control group) v1 or hybrid layout, which current Incus does not target.

cat /sys/fs/cgroup/cgroup.controllers lists the control-group controllers delegated to you. Incus documents blkio, cpuset, devices, freezer, memory and pids as required. On a nested VPS that list is often shorter than on a KVM one, because the provider chose what to hand down. A controller missing from that file is a controller Incus cannot use, so the instance limit that depends on it is not available to you.

Kernel version matters more than it used to. As of August 2026 the Incus documentation carries two different minimums for the two branches upstream maintains. The 6.0 LTS (long term support) branch says "The minimum supported kernel version is 5.4." The current stable branch says "The minimum supported kernel version is 6.12." Ubuntu 24.04 packages the 6.0 LTS series in its own repository and pairs it with a 6.8 kernel, which is a supported combination. Installing the current stable build from the upstream repository onto that same 6.8 kernel puts you below the documented minimum, so read uname -r before you pick a repository.

If your goal is full virtual machines rather than containers, the constraint is different and harder. See nested virtualisation on a VPS for whether your VPS can expose /dev/kvm at all, and Proxmox against a rented VPS for the case where you own the hardware.

Install Incus on Ubuntu or Debian

Debian 13 and Ubuntu 24.04 and newer ship Incus in their own repositories.

sudo apt update
sudo apt install -y incus

On Debian, incus-base installs container support without the virtual machine parts. On Ubuntu, add qemu-system if you also want --vm instances.

For a newer release than your distribution carries, the upstream packages live at pkgs.zabbly.com. These commands come from the project's own repository README.

sudo apt update && sudo apt install -y curl
sudo mkdir -p /etc/apt/keyrings/
sudo curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
sudo sh -c 'cat <<EOF > /etc/apt/sources.list.d/zabbly-incus-stable.sources
Enabled: yes
Types: deb
URIs: https://pkgs.zabbly.com/incus/stable
Suites: $(. /etc/os-release && echo ${VERSION_CODENAME})
Components: main
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/zabbly.asc
EOF'
sudo apt-get update
sudo apt-get install -y incus

Then give your user access to the daemon socket.

sudo usermod -aG incus-admin "$USER"
newgrp incus-admin
incus info

incus info printing the server's configuration means the socket works. A permission error means the group change has not reached your shell, which newgrp incus-admin fixes for the current shell and a fresh login fixes properly. Treat membership of incus-admin as equal to root on the host, because access to that socket is full control of a daemon that runs as root. Some distributions also create a plain incus group for restricted user access.

Now initialise the daemon.

sudo incus admin init

Answer the questions instead of reaching for incus admin init --minimal. The minimal path picks the dir storage driver, and the next section is about why that choice follows you around.

Launch something and confirm it works.

incus launch images:debian/13 web
incus list
incus exec web -- bash

incus list should show web as RUNNING with an IPv4 address on the incusbr0 subnet. No address means DHCP (dynamic host configuration protocol) did not complete, which the networking section covers. A container that fails to start prints its reason in incus info web --show-log, and daemon-level failures land in sudo journalctl -u incus -n 50. On a VPS where systemd-detect-virt returned lxc or openvz, this launch is the honest test of whether nesting is available to you.

Why the default storage backend matters

The storage backend decides whether a snapshot is instant or a full copy of the container's disk. It is the one choice at install time that you cannot cheaply change later.

Incus supports dir, btrfs, lvm, zfs, Ceph and several remote drivers. On a single-disk VPS the real choice is between dir and btrfs.

The dir driver keeps each container as ordinary files and directories under /var/lib/incus. Incus documents it as "much slower than all the other drivers", because it has to unpack every image and make real copies instead of referencing shared blocks. A snapshot of a 4 GiB container writes 4 GiB and takes as long as cp -a takes. Disk quotas work only on ext4 or XFS with project quotas enabled at the filesystem level, which is not on by default on most VPS images, so a disk limit on a dir pool often has no effect.

btrfs and zfs are copy-on-write, so a snapshot records only the blocks that change after it. Incus names those two as the recommended backends. Snapshots become near instant. Disk quotas work through the filesystem's own quota support.

Most VPS plans give you one disk with no spare partition, so put the pool on a loop file. Incus does that for you when you give no source=.

sudo apt install -y btrfs-progs
incus storage create fast btrfs size=30GiB
incus profile device set default root pool=fast
incus launch images:debian/13 web2 -s fast

Without size=, a loop-backed pool takes 20% of the free disk space, with a floor of 5 GiB and a ceiling of 30 GiB. Set it deliberately. The loop file is a file on your root filesystem, so the pool and the host share the same free space, which means filling the pool fills the host's disk.

ZFS on Debian and Ubuntu is a DKMS module rather than an in-tree one, so it rebuilds on every kernel upgrade and can fail to build after one. On a server you do not watch daily, btrfs is the lower-maintenance of the two.

The three networking modes, and what each one exposes

incus admin init creates a managed bridge called incusbr0 and puts every new instance on it. That is one of three ways to attach a container, and the other two exist because the first one hides your containers behind NAT (network address translation).

Managed bridge. incusbr0 gets a private subnet. The host holds the first address on it and acts as the gateway, Incus runs DHCP and DNS (domain name system) on it, and outbound traffic leaves through the host's public address with source NAT applied. Nothing from outside reaches the container until you say so. Forward a port with a proxy device.

incus config device add web http proxy listen=tcp:0.0.0.0:8080 connect=tcp:127.0.0.1:80 nat=true

nat=true forwards with netfilter rules instead of proxying through a separate userspace connection, so the client's real address survives into the container's logs. Incus supports that mode only when the host is the instance's gateway, which is exactly the incusbr0 case.

macvlan. The container gets its own MAC (media access control) address on the host's physical network. On most VPS platforms this fails, because the virtual switch port is bound to your VM's MAC address and drops frames from any other. There is a second limit that catches people even where it works. Incus documents that "macvlan devices, while able to communicate between themselves and to the outside, cannot talk to their parent device. This means that you can't use macvlan if you ever need your instances to talk to the host itself."

Routed. This is the mode that usually works on a VPS with extra addresses. Incus documents the device as one that "creates a virtual device pair to connect the host to the instance and sets up static routes and proxy ARP/NDP entries to allow the instance to join the network of a designated parent interface". ARP is the address resolution protocol. The container keeps a public address. The host answers ARP for it, so the provider still sees only the host's MAC address.

incus config device add web eth0 nic nictype=routed parent=enp1s0 ipv4.address=203.0.113.20

Take the parent interface name from ip route show default. Current images use names like enp1s0 or ens3, rarely eth0. Naming the device eth0 overrides the one the default profile supplies, so the container ends up on the routed interface instead of the bridge. Check the result from inside with ip a and ip route.

Why a container reached a service on the host

A container on incusbr0 has its own network namespace. It does not have a firewall boundary against the host. The host sits on that bridge at the gateway address, so from inside the container the host is a directly reachable neighbour, and every host service bound to 0.0.0.0 answers there.

Check it yourself. On the host, list what is listening.

sudo ss -tlnp

Then, from inside a container, aim at the gateway that ip route reports.

ip route show default
nc -zv 10.0.0.1 6379

If a database, a metrics endpoint or an admin panel on the host is bound to 0.0.0.0, that check succeeds. Your provider's network firewall never saw the packet, because the packet never left the machine. This is the surprise behind most "how did it reach that" questions: the container is isolated from the internet by NAT, and it is isolated from the host by nothing.

Bind host services to 127.0.0.1 wherever you can. Then filter the bridge on the host. On a ufw box the default deny policy already blocks container-to-host traffic, which breaks Incus DNS and DHCP, and the fix the Incus documentation gives is sudo ufw allow in on incusbr0. That single command re-opens every host port to every container. Allow only what containers actually need instead.

sudo ufw allow in on incusbr0 to any port 53 proto udp
sudo ufw allow in on incusbr0 to any port 53 proto tcp
sudo ufw allow in on incusbr0 to any port 67 proto udp
sudo ufw route allow in on incusbr0
sudo ufw route allow out on incusbr0

The two ufw route rules are what let instance traffic pass through the host to the internet. Without them, ufw's routed policy drops forwarded packets, so containers get an address and reach nothing.

Snapshots and profiles

A snapshot is a point-in-time copy of the instance inside its storage pool.

incus snapshot create web pre-upgrade
incus info web
incus snapshot restore web pre-upgrade
incus snapshot delete web pre-upgrade

incus info web lists the snapshots the instance holds. Schedule them per instance.

incus config set web snapshots.schedule=@daily
incus config set web snapshots.expiry=4w

A snapshot lives in the same pool, on the same disk, on the same server. It protects you from a bad upgrade. It does not protect you from a dead disk or a deleted instance. The backup is incus export, and the file has to leave the box.

incus export web /root/web-backup.tar.gz
incus import /root/web-backup.tar.gz

A profile is a named set of config keys and devices applied to instances. Every instance gets the default profile unless you say otherwise, and that profile is what supplies its root disk and its network interface. Editing default changes every instance using it, which is useful and is also how people detach the network from twenty containers at once.

incus profile create small
incus profile set small limits.memory=512MiB
incus profile set small limits.cpu=1
incus launch images:debian/13 api -p default -p small

Profiles apply in order, so a key set in the last profile listed wins. Read what an instance actually ended up with using incus config show api --expanded.

Running Docker inside an Incus container

Docker inside an Incus system container needs nesting turned on, because Docker creates namespaces and mounts of its own that a container is not allowed to create by default.

incus config set web security.nesting=true
incus restart web

Incus documents security.nesting as "Whether to allow nesting inside of the instance", and it defaults to false for containers. Two more points come straight from the Incus FAQ. A container cannot load kernel modules, so a module Docker needs must be loaded on the host and listed with incus config set web linux.kernel_modules overlay,br_netfilter. And creating a /.dockerenv file inside the container makes Docker skip some checks that fail in a nested environment.

On Ubuntu 24.04 hosts, AppArmor's unprivileged user namespace restrictions can block the pivot_root that runc performs. Docker inside the container prints:

failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error jailing process inside rootfs: pivot_root .: permission denied

and the host's dmesg shows a line containing apparmor="DENIED" operation="pivotroot" class="mount". The setting people reach for is kernel.apparmor_restrict_unprivileged_userns. Turning it off is not a reliable fix: the upstream Incus bug report for this exact denial records that setting it to 0 did not resolve it. Read dmesg for the denial first, so you know whether AppArmor is really your problem before you change a security default.

If you would rather run containers directly on the VPS and skip a layer, running Docker on a VPS covers that setup on its own.

Failure modes, with the strings you will see

Instances lose all network after you install Docker on the host. The Incus documentation names the cause: "Docker sets the global FORWARD policy to drop, which prevents Incus from forwarding traffic and thus causes the instances to lose network connectivity." Instances keep their addresses and reach nothing. Set ip-forward-no-drop to true in /etc/docker/daemon.json, then make forwarding persistent and let the bridge through Docker's own chain.

echo "net.ipv4.conf.all.forwarding=1" | sudo tee /etc/sysctl.d/99-forwarding.conf
sudo systemctl restart systemd-sysctl
sudo iptables -I DOCKER-USER -i incusbr0 -j ACCEPT
sudo iptables -I DOCKER-USER -o incusbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Those iptables rules do not survive a reboot on their own. Persist them.

Containers stop starting with a cgroup error. The Incus FAQ documents this one. A message about Failed to mount "/sys/fs/cgroup" usually means a VPN client on the host mounted the net_cls cgroup v1 controller over cgroup v2, which Incus uses. sudo umount /sys/fs/cgroup/net_cls clears it.

Instance gets no IPv4 address. incus list shows it running with an empty address column. DHCP replies from the host are being dropped, most often by a host firewall that does not know about the bridge. On ufw, sudo ufw allow in on incusbr0 to any port 67 proto udp restores it. Watch the requests arrive with sudo tcpdump -ni incusbr0 port 67.

Instance refuses to start on a nested VPS. Read incus info <name> --show-log first, then sudo journalctl -u incus -n 50. If systemd-detect-virt said lxc or openvz, the missing piece is on the provider's side, and no setting inside your VPS changes it.

Snapshots are slow and the disk keeps filling. You are on a dir pool. incus storage list prints the driver for each pool. Moving to a copy-on-write pool means creating the new pool, copying instances into it with incus copy web web-new -s fast, then deleting the originals once you have checked the copies start.

FAQ

Is an Incus container the same thing as a Docker container?

No. Docker packages a single process or application. An Incus system container simulates a full operating system, with its own init, its own users, its own services and its own package manager. You keep an Incus container and patch it like a server. You throw a Docker container away and rebuild it from an image. You can run Docker inside an Incus container by setting security.nesting=true on the container. The reverse does not work.

Can I run Incus on a VPS?

On a KVM VPS, yes. systemd-detect-virt prints kvm or qemu, you have your own kernel, and Incus behaves as it does on hardware. If it prints lxc, lxc-libvirt or openvz, your VPS is itself a container, so Incus containers inside it are nested and work only if the provider enabled nesting on your container. Check uname -r too, because as of August 2026 the current Incus stable branch documents a minimum kernel of 6.12 while the 6.0 LTS branch documents 5.4.

Which storage backend should I pick for Incus on a VPS?

btrfs on a loop file, unless you have a spare block device to hand it. The dir driver is documented as much slower than the others, because it copies files instead of using copy-on-write, so every snapshot writes the whole container again. incus admin init --minimal selects dir, which is why answering the interactive questions is worth the two minutes. Create the pool with incus storage create fast btrfs size=30GiB.

Why can my Incus container reach a service running on the host?

Because the default incusbr0 bridge puts the host on the same subnet as the container, at the gateway address, and nothing filters between them. Any host service bound to 0.0.0.0 answers there, and your provider's firewall never sees those packets because they never leave the machine. Bind host services to 127.0.0.1, and on a ufw host allow only DNS and DHCP in on incusbr0 rather than the blanket sudo ufw allow in on incusbr0.

How do I back up an Incus container?

incus export web /root/web-backup.tar.gz writes the instance and its snapshots to one file, and incus import restores it on the same server or another one. Snapshots made with incus snapshot create are not backups: they sit in the same storage pool on the same disk, so they survive a bad upgrade and not a dead server. Schedule them with incus config set web snapshots.schedule=@daily, and copy the exports off the box.

#incus#lxd#system-containers#virtualization#vps