Incus host guide
Run the container scripts on an Incus host, as root or as a normal user, locally or against a remote server.
The container scripts (ct/) run on Incus as well as on Proxmox VE. The script
is the same; the engine in core
detects the host and creates the container with the incus client instead of
pct. This page covers what you need, where files end up, how settings map to
Incus configuration, and what does not carry over from Proxmox VE.
Requirements
| Requirement | Why | Check |
|---|---|---|
incus client that can reach a server | incus_check runs incus info and stops if it fails | incus info |
| At least one storage pool | incus_check stops with "No Incus storage pools found." otherwise | incus storage list |
bash, curl and whiptail (package newt) | The script, its downloads and the dialogs | command -v bash curl whiptail |
Access to the images: remote and to GitHub | Container images and the scripts | incus image list images: debian/13 |
| Free space in the pool | The root disk is created in the pool, not on the host filesystem | incus storage info default |
The machine that runs the script does not have to be Debian: the architecture
is read from the Incus server (incus info).
incus_check names the cause when incus info fails:
| Message | Fix it prints |
|---|---|
Incus daemon is not running. | systemctl start incus |
No permission to reach the Incus daemon as <user>. | sudo usermod -aG incus-admin <user>, then log in again or newgrp incus-admin |
Incus is installed but not initialised. | incus admin init |
No Incus storage pools found. | incus storage create default dir |
The install command
Use the command from the script's page on community-scripts.org with Incus selected, or build it yourself from the Incus repository:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Incus/main/ct/debian.sh)"Where that file comes from:
ct/andinstall/in the Incus repository are mirrored from ProxmoxVE by the workflowsync-scripts.yml. It runs daily at 04:00 UTC and whenever ProxmoxVE reports a change toct/orinstall/.- The mirror changes two lines per script: it sets
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/Incus/main"so the container fetches itsinstall/script from the Incus repository, and it makes the script loadcore/build.funcfrom core. vm/,tools/incus/andjson/are maintained in the Incus repository and are not touched by the sync. Proxmox-only folders (tools/pve/,turnkey/, the Proxmoxvm/scripts) are not mirrored.- Scripts that exist only in DevScripts have no Incus copy.
A fix to an application script belongs in ProxmoxVE; a change made by hand in
the Incus repository's ct/ or install/ is overwritten by the next sync.
To skip the menu, pass a mode:
mode=default bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Incus/main/ct/debian.sh)"See Unattended deployments for the modes and Script origin for running from a fork, a branch or a local clone.
Root and non-root users
You do not need root. incus_root_check accepts any user for whom incus info
works, which in practice means membership in the incus-admin group. If
neither that nor root applies, the script exits with code 152.
Everything the engine stores on the host goes into one state directory, chosen
by community_scripts_dir:
| Situation | State directory |
|---|---|
COMMUNITY_SCRIPTS_STATE_DIR is set | that directory |
| root | /usr/local/community-scripts |
non-root, /usr/local/community-scripts exists and is writable | /usr/local/community-scripts |
| non-root otherwise | $XDG_CONFIG_HOME/community-scripts, by default ~/.config/community-scripts |
Inside it:
| Path | Content |
|---|---|
default.vars | Your global defaults ("User Defaults") |
defaults/<app>.vars | Saved defaults for one app |
diagnostics | The telemetry choice, DIAGNOSTICS=yes or DIAGNOSTICS=no |
logs/incus-create-<session>.log | Host side of a run: the incus launch arguments and every configuration step |
logs/incus-install-<session>-combined.log | Host log plus the install log pulled out of the container, written when an install fails |
If the logs directory cannot be created, the logs go to /tmp under the same
names. Inside the container the install log is /root/.install-<session>.log.
Driving a remote server
The incus client always talks to a remote, and local is only the default.
The backend never names a remote itself, so switching the client's default
remote points the whole run at another server. This is how you reach IncusOS,
which has no shell to run a script in.
incus remote add incusos https://<server>:8443 --token <token>
incus remote switch incusos
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Incus/main/ct/debian.sh)"What changes with a remote:
incus_targetreads the default remote withincus remote get-defaultand takes the server's architecture and kernel fromincus info. When the remote is notlocal, the run printsTarget: Incus remote '<name>' (<architecture>).- The architecture check and
arch_resolveuse the server's architecture, not the architecture of the machine you type on. - Pool free space comes only from
incus storage info. Thedffallback fordirpools is skipped because the path is on the other machine. - GPU and USB detection (
lspci,/sys/class/drm,/dev/ttyUSB0) read the machine that runs the script. Do not rely on them for a remote server.
Switch back with incus remote switch local.
Settings and the Incus keys they become
The wizard is shared with Proxmox VE. incus/backend.func translates its
answers when it calls incus launch:
| Setting | Variable | Incus configuration |
|---|---|---|
| CPU cores | var_cpu | limits.cpu |
| RAM | var_ram | limits.memory (in MiB) |
| Disk size | var_disk | root device, size=<n>GiB |
| Storage | var_container_storage | --storage <pool> |
| Network | var_brg | --network <name> |
| Nesting | var_nesting (default 1) | security.nesting=true |
| Privileged container | var_unprivileged=0 | security.privileged=true |
| mknod | var_mknod=1 | security.syscalls.intercept.mknod=true |
| Autostart | always | boot.autostart=true |
| Profile | var_profile | --profile <name>, only when it is not default |
| Tags | var_tags | user.tags |
| Instance name | var_ctid | the instance name, also written to /etc/hostname inside the container |
| Hostname | var_hostname | user.hostname |
| MAC, VLAN, MTU | var_mac, var_vlan, var_mtu | hwaddr, vlan, mtu on the eth0 device |
| Static IPv4 | var_net | ipv4.address on eth0 (managed networks only, see below) |
| Static IPv6 | var_ipv6_method=static | ipv6.address on eth0 (managed networks only) |
| GPU | var_gpu=yes | one gpu device per card, gputype=physical |
| TUN | var_tun=yes | unix-char device for /dev/net/tun |
| FUSE | var_fuse=yes | unix-char device for /dev/fuse |
| Extra disk | var_extra_disk, var_extra_disk_path | disk device named extradata |
After a successful install the instance also carries
user.description="<App> LXC (community-scripts)" and user.community-scripts=1.
Incus instances have names, not numeric IDs. The default name is the app name
in lower case (debian); if it is taken, the next free <name>-<n> is used.
var_ctid sets the name yourself. It must start with a letter or digit and may
contain letters, digits, ., _ and -; underscores become hyphens.
Profile
var_profile adds one profile to the launch. It is not one of the engine keys,
so the global default.vars ignores it; pass it in the environment.
var_profile=lan mode=default bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Incus/main/ct/debian.sh)"The CPU, memory, network and root disk values from the table are still passed on the command line and win over what the profile says.
Extra disk
var_extra_disk attaches a second disk after the container exists. Its value
becomes the source of an Incus disk device, so it can be a host path. The
mount point defaults to /mnt/extra; change it with var_extra_disk_path.
Like var_profile, neither is read from default.vars.
var_extra_disk=/srv/media var_extra_disk_path=/mnt/media mode=default \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Incus/main/ct/debian.sh)"If Incus rejects the device, the run prints
Could not add extra disk (source=...) and continues.
GPU
GPU passthrough is attempted when the script sets var_gpu="yes" (for example
ct/agentdvr.sh) or you enable it in the advanced settings. Display
controllers are found with lspci and added by PCI address:
incus config device add <name> gpu0 gpu gputype=physical pci=0000:00:02.0If no card is found through lspci, /sys/class/drm is used and the device is
selected by its PCI address, or with id=<n> when that cannot be resolved. When adding by address fails for a vendor, one attempt
by vendor follows (vendorid=8086, 1002 or 10de). On a host with several
GPU vendors you are asked which to pass through; the default after 60 seconds
is all of them. For NVIDIA, the /dev/nvidia* character devices are attached
as well. Once the container's groups exist, each gpu device gets gid= of
the container's video group and mode=0660.
Privileged containers and devices
A privileged container (var_unprivileged=0) gets security.privileged=true.
For those, /dev/ttyUSB0, /dev/ttyUSB1 and /dev/ttyACM0 are attached as
unix-char devices when they exist. A Coral TPU at /dev/apex_0 is attached
to every container when present.
linux.sysctl.kernel.keys.maxkeys=2000 is set only for privileged containers
and only when the host value is lower (or the target is a remote server). If the container then fails to restart,
the key is removed again and the container is started without it.
Restricted projects
A project created with restricted=true blocks security.nesting and syscall
interception unless it is told otherwise, and Incus then refuses the whole
launch. The backend recognises that refusal, names the key and the project,
prints the two commands that lift it, and exits with code 229:
incus project set <project> restricted.containers.nesting allow
incus project set <project> restricted.containers.interception allowRun them on the server and start the script again. No other image is tried in this case, because the image is not the problem.
Storage pools and space checks
Incus has no template storage. The only storage question is which pool holds
the root disk: var_container_storage if it names an existing pool, otherwise
a menu titled INCUS STORAGE POOL (skipped when there is exactly one pool).
Free space is measured per pool by incus_storage_pool_free_gb:
incus storage info --bytes <pool>: the free value, or total minus used.- The same without
--bytes, parsing the human-readable sizes. - For a local
dirpool whosesourceis a directory,dfon that path.
All of it runs under LC_ALL=C, because incus translates its output.
The figure is the pool's free space, not the host disk's. A default loop-backed pool is often far smaller than the disk it sits on. When the pool has less free space than the container asks for, the run warns and continues:
Pool 'default' reports ~3GB free, container wants 8GBand prints how to grow the pool:
incus storage set default size=50GiBIf the free space cannot be determined at all, that is also only a warning. Check the pool yourself before installing something large; a full pool shows up later as a failed package install inside the container.
Networking
When var_brg is not set, incus_pick_default_network chooses the network.
Only bridges are candidates, in three classes:
- Unmanaged bridges that carry a physical or VLAN interface (a
br0you built in netplan or/etc/network/interfaces). - Managed networks without NAT.
- Managed networks with
ipv4.natoripv6.nat, such asincusbr0.
An unmanaged bridge without such an interface (docker0, a compose network) is
never a candidate. The candidates are ordered by how many instances already use
them, most first; with equal counts the class order above decides. If the
default profile's network is a candidate from class 1 or 2, it moves to the
front. The first candidate not named incusbr0 is taken, and incusbr0 only
when nothing else is left. Physical interfaces are never offered: attaching one
moves the host's NIC into the container.
The advanced settings list bridge, OVN and macvlan networks and label them
LAN bridge (uplink: <if>), Incus network, NAT - not reachable from your LAN
or private bridge (no uplink).
A static address only works on a network Incus manages (type bridge, ovn or
macvlan), because ipv4.address is a reservation in the DHCP server Incus
runs for that network. On an unmanaged bridge the run warns that the address
cannot be reserved and the container takes DHCP from your LAN; set the address
inside the container or reserve it on your router. A gateway from the wizard is
never applied; if it differs from the one the network hands out you get a
warning.
The run waits up to 40 seconds for an address (90 seconds on physical,
macvlan and sriov networks) and continues with a warning if none appears.
Images
There are no Proxmox templates. The image is images:<os>/<version>, tried in
this order: a matching local image, images:<os>/<version>, the /cloud
variant, then the release name (debian/trixie, ubuntu/noble). If the
requested version is not on the image server, the newest numbered version of
that OS is used and the run says so. If the image exists but not for this
architecture, the run stops with exit code 106.
What is different from Proxmox VE
| Proxmox VE | On Incus |
|---|---|
| Numeric container ID | Instance name; var_ctid is a name here |
Template storage (var_template_storage) | None. Images come from the images: remote |
keyctl (var_keyctl) | Not applied. Incus has no equivalent option |
Mount filesystems (var_mount_fs) | Not applied |
Container protection (var_protection) | Not applied from the wizard or .vars files |
Console mode (var_cmode) | Not applied |
DNS server and search domain (var_ns, var_searchdomain) | Not applied |
SDN vnet (var_sdn_vnet) | Not applied |
Host CA inheritance (var_inherit_host_ca) | Not applied |
Post-install hook (var_post_install) | Not run |
Static gateway (var_gateway) | Not applied; the network's own gateway is used |
| Static address with prefix length | Only the address is reserved, and only on managed networks |
| Proxmox version check | Replaced by incus_check |
| SSH-session warning, arm64 notice dialog | Skipped |
vm/ scripts | Not shared. The Incus repository has its own vm/ folder |
tools/pve/ host tools, TurnKey | Not available |
Failure handling matches Proxmox VE: a failed install offers to remove the container, keep it, rebuild in verbose mode, and, depending on the error, repair the package manager, double RAM and CPU, or retry with public DNS servers. Without an answer the container is removed after 60 seconds.
Closing the terminal does not abort the install. It runs detached inside the container and the host waits for its completion marker. Ctrl-C does abort.
Updating a container
Updates run inside the container, exactly as on Proxmox VE. Either open a shell
and run the update command the installer wrote, or run the script again from
the host:
incus exec debian -- updateincus exec debian -- bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/Incus/main/ct/debian.sh)"Inside a container the engine detects the platform as incus-container (or
container) and goes straight to the script's update_script, without
touching the host. On a positively identified host it never does that: if the
backend could not be loaded, the script refuses to run its update path instead
of updating the host's packages.
Where to report what
| Problem | Where |
|---|---|
| An application installs wrongly on both platforms | ProxmoxVE issues |
| Something only fails on Incus | Incus issues |
| Storage pools, networks, images, projects as such | Incus documentation and forum |