Container creation on Proxmox VE
What build_container and the Proxmox VE backend do on the host, from the template to the hand-over to the install script.
build_container is the call in a container script
that turns the resolved settings into a running container with the application in
it. On a Proxmox VE host it lives in
pve/backend.func.
On an Incus host core/build.func loads the Incus backend instead; see
Platforms and the Incus guide.
Before anything is created
start runs these checks before the menu opens. Each one stops the run with its
own message and exit code.
| Check | Stops when | Exit code |
|---|---|---|
shell_check | the shell is not Bash | 103 |
root_check | not root, or started through sudo | 104 |
pve_check | /etc/pve is not mounted or storage.cfg is missing | 228 |
pve_check | the node is in a cluster without quorum | 210 |
pve_check | the Proxmox VE version is not 8.0 to 8.9 or 9.0 to 9.2 | 1 |
arch_check | the host is neither amd64 nor arm64, or the script does not support arm64 | 106 |
maxkeys_check | the kernel key limits are unreadable, or nearly used up and raising them was declined | 107, 108 |
storage_content_check | no storage accepts container templates (vztmpl) | 120 |
storage_content_check | no storage accepts container disks (rootdir) | 119 |
The storage check offers a fix when it can: it lists the active storages that could hold the missing content type and enables it on the one you pick.
Order of work
- Check that this host's
pve-containercan set up the requested OS release. - Build the network string, the feature list and the
pct createoptions. - Validate the settings.
- Select and validate the template storage and the container storage.
- Find the template, download it if needed.
- Run
pct create. - Add passthrough entries to the container config.
- Start the container and wait for an address.
- Prepare the system inside (locale, time zone, base packages).
- Fetch
install/<app>-install.shand run it inside the container. - On failure: collect the logs, explain, and ask what to do with the container.
OS release support
A pve-container package that is older than the OS release cannot configure it,
and pct create would fail after the download. lxc_os_support_check asks the
host first, for Debian and Ubuntu. If the release is not supported it prints
pve-container <version> on this host cannot create Debian 13 containers.and, when interactive, offers Update the LXC stack to pve-container <version> (apt dist-upgrade),
Use Debian <older> instead (not every app supports it) or Cancel. Unattended,
or when neither option exists, it prints what to do and exits with 231.
From settings to pct create
The call has the form
pct create <CTID> <template storage>:vztmpl/<template> <options>. It runs with
umask 022, so that a restrictive umask in the operator's shell does not produce a
container whose /etc other users cannot enter.
| Setting | pct create option | Notes |
|---|---|---|
var_ctid | the container ID | Next free ID by default. An ID in use is replaced by the next free one |
var_hostname | -hostname | Defaults to the app name in lower case |
var_tags | -tags | community-script is always included |
var_cpu | -cores | |
var_ram | -memory | In MiB |
var_disk, var_container_storage | -rootfs <storage>:<size> | Size in GB |
var_unprivileged | -unprivileged | 1 or 0 |
var_brg, var_sdn_vnet | -net0 name=eth0,bridge=... | Default vmbr0. A bridge that does not exist on the node stops the run with exit code 208 |
var_net | ip= | dhcp or an address with prefix |
var_gateway | gw= | Ignored with dhcp or when outside the subnet |
var_mac, var_vlan, var_mtu | hwaddr=, tag=, mtu= | |
var_ipv6_method | ip6=auto, ip6=dhcp, or a static address with gw6= | none adds nothing |
var_ns, var_searchdomain | -nameserver, -searchdomain | |
var_pw | --password | At least 5 characters. Empty means automatic root login on the console |
var_protection | -protection 1 | |
var_cmode | -cmode | shell, console or tty |
var_timezone | -timezone | Defaults to the host's time zone |
| (always) | -onboot 1 | The container starts with the host |
| (always) | -features ... | See below |
For openeuler, rockylinux, almalinux and amazonlinux the backend adds
-ostype centos, because Proxmox VE cannot derive a usable OS type for them.
Where each setting comes from, and the precedence between command line, saved defaults and the wizard, is covered in Settings and the wizard and the Configuration reference.
Validation
validate_ct_settings checks the values pct create would reject, wherever they
came from: memory, root password length, container ID, cores, disk size, IPv4
address, gateway, DNS servers, search domain and mount types. Problems are listed
under These settings would be rejected by pct create: and the run ends with exit
code 208. A common one is a memory value given in GB:
RAM 4 MiB is too little (var_ram is in MiB; 4 GiB would be 4096)Features
The -features option is assembled from the settings.
| Feature | Set when | Typical use |
|---|---|---|
nesting=1 | always, unless var_nesting=0 | Docker, Podman, LXC inside LXC |
keyctl=1 | the container is unprivileged | Docker, systemd-networkd, kernel keyring operations |
fuse=1 | var_fuse=yes | rclone, mergerfs, AppImage |
mknod=1 | var_mknod=1 | creating device nodes inside the container |
mount=<types> | var_mount_fs lists filesystem types, e.g. nfs,cifs | mounting network shares from inside the container |
TUN, needed by VPN applications such as WireGuard, OpenVPN and Tailscale, is not
a Proxmox feature flag. With var_tun=yes the backend appends two lines to /etc/pve/lxc/<CTID>.conf after creation:
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=fileStorage
Two storages are needed: one for the template (vztmpl) and one for the root
disk (rootdir). They are taken from var_template_storage and
var_container_storage when those are set and valid. Otherwise a storage is
chosen automatically if only one qualifies, and a Storage Pools dialog asks if
there are several.
The container storage is then validated:
| Condition | Message | Exit code |
|---|---|---|
Storage not in /etc/pve/storage.cfg | Storage '<name>' not found in /etc/pve/storage.cfg | 213 |
Type iscsidirect | uses iSCSI-direct which does not support container rootfs | 212 |
Type iscsi or zfs (ZFS over iSCSI) | does not support container rootdir content | 213 |
Type cephfs | uses CephFS which is not supported for LXC rootfs | 219 |
Type pbs | is a Proxmox Backup Server — cannot be used for containers | 224 |
linstor, rbd, nfs, cifs not reachable | is not accessible or inactive | 217 |
Content type rootdir not enabled | does not support 'rootdir' content | 213 |
| LVM-thin pool full (data or metadata) | Thin pool <vg>/<pool> of storage '<name>' is full | 214 |
| Less free space than the disk size | Not enough space on '<name>' | 214 |
A storage that is 85 % full or more produces a warning. At 95 % an interactive run
asks Continue anyway?, and at 98 % an unattended run refuses with exit code 214.
Templates
Search
The template name is searched as <os>-<version>, for example debian-13,
restricted to the host's architecture. Debian and Ubuntu use the -standard_
templates; Alpine, Fedora, Rocky and CentOS the -default_ ones.
- Already downloaded. If the template storage holds a matching template, the newest one is used and the catalog is not contacted.
- Proxmox catalog. Otherwise
pveam updaterefreshes the catalog (with a 30-second limit) and the newest matching entry ofpveam available -section systemis taken. - linuxcontainers.org. If Proxmox offers nothing for this OS, version and
architecture, the image catalog at
images.linuxcontainers.orgis searched, see below.
If the version does not exist in the catalog, the wizard has usually already
handled it: resolve_os_version proposes the closest available version in an
OS VERSION NOT AVAILABLE dialog, or switches to it with a warning when
unattended. If the search still ends without a template, an interactive run lists
the versions the host does offer, and an unattended run stops with
No standard_ template found for debian 14 and exit code 225.
Download
pveam download <storage> <template> is tried up to three times, waiting 5 and
then 10 seconds in between. Success is judged by the template appearing in
pveam list, not by the exit code. If the catalog names a template the server no
longer has, the catalog is refreshed and the current file is taken.
After the third failure the backend prints
Template download failed after 3 attempts: <template>, the last relevant line
from pveam, and the most likely cause it can determine: no space on the storage,
the HTTP proxy from Datacenter → Options, a host that cannot resolve
download.proxmox.com, or a host that cannot reach it. Exit code 222. If the
template is still missing from the storage afterwards, exit code 223.
A template on disk is checked before use. A file smaller than 1 MB or one that
tar cannot list is downloaded again.
Two runs that need the same template wait for each other through a lock file
/tmp/template.<template>.lock. After ten waits of 30 seconds the run gives up
with exit code 211.
The linuxcontainers.org fallback
When Proxmox has no template the backend prints
No Proxmox template for <os> <version> on <arch>, trying linuxcontainers.organd reads https://images.linuxcontainers.org/streams/v1/images.json. It needs
jq on the host and installs it if missing. The newest build of the matching
release is downloaded into the template storage as
<os>-<release>-<arch>-rootfs.tar.xz and reused on later runs.
- The release is matched by name or by the image's aliases.
archlinuxwith versionbasemaps tocurrent, Devuan5todaedalus, and CentOS10to10-Stream. - Images published only as squashfs are unpacked and repacked as a tarball. This
installs
squashfs-toolson the host. - Images without
/etc/networkget an empty/etc/network/interfacesadded, because Proxmox VE expects the directory. - If the catalog cannot be reached or has no such image, a warning is printed and the normal Proxmox search continues, so the error you finally see is about what this host offers.
This is how distributions Proxmox does not ship as templates can be created at all.
arm64
Proxmox VE on arm64 is supported. The differences on such a host:
arch_checklooks at the script'svar_arm64.yescontinues,nostops with exit code 106, and an unset value asks whether to try. A noticeARM64 support is in active development.is shown before the menu.- Templates are filtered by architecture. Where the Proxmox catalog has no arm64 template, the image comes from linuxcontainers.org as described above.
- The base packages installed on Debian and Ubuntu additionally include
openssh-server,wgetandgcc. - Install scripts choose release assets with
arch_resolve, see Install scripts.
When pct create fails
The output of pct create goes to
/tmp/pct_create_<CTID>_<timestamp>_<SESSION_ID>.log and is appended to the build
log. The backend reads it to decide what to do.
| Output contains | Reaction | Exit code if it ends there |
|---|---|---|
Compilation failed or a Perl Bareword error | Container creation failed due to broken Perl modules on the PVE host. with the advice to run apt update && apt dist-upgrade -y and reboot | 232 |
Parameter verification failed | pct create rejected a setting: followed by the rejected options | 208 |
already exists or already in use | Retries once with the next free ID | 209 |
unable to open, corrupt or invalid | Downloads the template again and retries | |
| anything else | Retries, then retries with the template on storage local | 209 |
unsupported ... version after those retries | Offers a host upgrade of the LXC stack, or an older template | 231 |
After a successful create the backend waits up to 10 seconds for the ID to appear
in pct list (exit code 215 otherwise) and checks that the config has a rootfs
line (216).
Passthrough
Passthrough entries are appended to /etc/pve/lxc/<CTID>.conf between creation
and first start.
USB
For a privileged container (var_unprivileged=0) USB serial passthrough is
configured automatically: all devices are allowed (lxc.cgroup2.devices.allow: a),
the capability drop list is cleared (lxc.cap.drop:), character devices 188 and
189 are allowed explicitly, and /dev/serial/by-id, /dev/ttyUSB0, /dev/ttyUSB1,
/dev/ttyACM0 and /dev/ttyACM1 are bind-mounted when present. Unprivileged
containers get none of this.
GPU
With var_gpu=yes the backend looks for GPUs on the host:
| Vendor | Detected through | Devices passed |
|---|---|---|
| Intel | /dev/dri/renderD* and /dev/dri/card* with vendor ID 0x8086 | those nodes |
| AMD | the same nodes with vendor ID 0x1002 or 0x1022 | those nodes, plus /dev/kfd if present |
| NVIDIA | a PCI display device of vendor 10de and existing /dev/nvidia* nodes | /dev/nvidia* and /dev/nvidia-caps/* |
- One GPU type is configured without a question. With several, a prompt
Select GPU type(s)appears; no answer within 60 seconds means all of them. A host with several NVIDIA cards gets a second prompt to choose cards. - Each node becomes a
dev<N>: <node>,gid=<gid>line, a format the Proxmox VE interface displays as well. - After the first start the backend reads the
videoandrendergroup IDs inside the container (creating the groups if needed), stops the container, writes the correct IDs into thedev<N>lines and starts it again. - For an AMD GPU the root disk is grown by 4 GB to make room for ROCm.
- NVIDIA needs the host driver loaded. Otherwise the backend prints
NVIDIA GPU detected via PCI but no /dev/nvidia* devices foundand skips it. The matching driver libraries inside the container are the install script's job. - If the host has no GPU device at all, the summary says
GPU Passthrough: Requested - no GPU devices on this hostand the install goes on without it.
Other devices
var_tun=yes: the TUN lines shown under Features.- A Coral TPU at
/dev/apex_0on the host is detected and bind-mounted into every container the scripts create.
Start and network
pct start is checked. If the container does not start, the backend prints
Container <CTID> was created but would not start, the relevant lines from
journalctl -u pve-container@<CTID>, and a hint for a foreground start with
lxc-start; exit code 227. If it is not in state running within 10 seconds:
exit code 117.
Then the backend waits for an address on eth0, IPv4 first, then a global IPv6
address. It polls 60 times with growing pauses, about two minutes in total. If
nothing arrives:
No IP assigned to CT <CTID> after 60 attemptswith a checklist (bridge, DHCP server, static configuration, firewall, Tailscale MagicDNS) and exit code 118.
With an address, a connectivity test follows: ping to 1.1.1.1 and 8.8.8.8,
IPv6 ping, and a TCP connection to 1.1.1.1:443. A failed test is only a warning,
Network configured (IP: <ip>) but connectivity test failed - installation will continue,
because ICMP is often filtered where HTTPS works.
Alpine containers skip this wait and go straight to the package step.
Preparing the system
Under the message Customizing container the backend runs a few commands inside
the container with pct exec:
var_http_proxyset: writes/etc/profile.d/90-http-proxy.shand an APT proxy file.var_inherit_host_ca=yes: copies the host's/usr/local/share/ca-certificates/*.crtinto the container and updates its trust store.- Debian, Ubuntu, Devuan: generates the locale, sets the time zone, and
installs
sudo curl mc gnupg2 jq. Ifdeb.debian.orgorarchive.ubuntu.comdoes not resolve, public DNS servers are written to/etc/resolv.conffirst. Ifapt-get updatefails, other mirrors are probed in three phases; when all fail you are asked for a mirror host name. - Alpine: sets the
latest-stablerepositories and installsbash newt curl openssh nano mc ncurses jq, with a mirror fallback. - Fedora, Rocky, AlmaLinux, CentOS, openEuler, openSUSE, Arch, Gentoo and
others: sets the time zone, fixes the console terminal type, and makes sure
curlis installed.
If the container disappears during this step the backend says so instead of
blaming the package manager:
Container <CTID> was stopped during its setup (<task> by <user>) with exit code
143, or Container <CTID> stopped by itself during its setup with 199.
Handing over to the install script
The install script is fetched on the host, from a local checkout if there is one,
otherwise from the script repository. A failure here is reported as
Could not fetch install/<app>-install.sh with exit code 115, and a hint to set
COMMUNITY_SCRIPTS_URL when running a fork.
The backend exports what the script needs, among them:
| Variable | Content |
|---|---|
FUNCTIONS_FILE_PATH | the text of lxc/install.func |
APPLICATION, app | APP and NSAPP |
CTID, CTTYPE | container ID and type |
PCT_OSTYPE, PCT_OSVERSION, PCT_ARCH, PCT_DISK_SIZE | OS, release, architecture, disk size |
var_ram | the memory the container really got |
PASSWORD, SSH_ROOT, SSH_AUTHORIZED_KEY | login settings |
VERBOSE, MODE | output mode and install mode |
COMMUNITY_SCRIPTS_URL, COMMUNITY_SCRIPTS_CORE_URL | where scripts and engine come from |
INSTALL_LOG | /root/.install-<SESSION_ID>.log |
The script is then run with lxc-attach -n <CTID> -- bash -c "<script>". From
here on the terminal shows the install script's output; see
Install scripts.
Inside the container the run leaves small files the host reads afterwards:
| File | Meaning |
|---|---|
/root/.install-<SESSION_ID>.log | the install log |
/root/.install-<SESSION_ID>.log.errinfo | exit code, line, command and output of the failed step |
/root/.install-<SESSION_ID>.failed | written on failure, contains the exit code |
/root/.install-<SESSION_ID>.done | written on success |
If the console is closed
Closing the browser tab or the SSH session does not abort the install. The script
inside the container ignores the hangup and continues. The host process redirects
its output to the build log and checks every 15 seconds, for up to six hours, for
the .done or .failed file. Ctrl-C is different: it stops the container and
ends the run with exit code 130. The container is not removed.
If the install fails after the console was closed, nobody can answer the question below, so the container is kept.
When the install fails
The host prints Installation failed in container <CTID> (exit code: <n>),
builds a combined log from the host's build log and the container's install log,
and prints its path:
Installation log: /tmp/<app>-<CTID>-<SESSION_ID>.logIt then explains the exit code and looks at the output of the failed step to classify the problem. Depending on the result, extra choices appear in the menu:
What would you like to do?
1) Remove container and exit
2) Keep container for debugging
3) Retry with verbose mode (full rebuild)
4) Repair APT/DPKG state and re-run install (in-place)
5) Retry with more resources (RAM: 2048→4096 MiB, CPU: 2→4 cores)
Select option [1-5] (default: 1, auto-remove in 60s):| Option | Offered when | What it does |
|---|---|---|
| Remove container and exit | always | pct stop, pct destroy |
| Keep container for debugging | always | Leaves the container as it is |
| Retry with verbose mode (full rebuild) | always | Removes the container and builds again with a new ID, same settings, verbose output |
| Repair APT/DPKG state and re-run install (in-place) | exit code 100 to 102 from an APT command, or APT errors in the output. On Alpine the wording is Repair APK state | Runs dpkg --configure -a, apt-get -f install, apt-get clean, apt-get update in the container, then the install script again |
| Retry with more resources | exit code 134, 137 or 243, or out-of-memory messages | Rebuilds with twice the memory and twice the cores. At most twice |
| Retry with more disk space | "no space left" in the output, or curl's write error 23 | Rebuilds with twice the disk size. At most twice |
| Retry with DNS override in LXC (8.8.8.8 / 1.1.1.1) | a curl network exit code, or resolver errors in the output | Rebuilds and writes those name servers into the container's /etc/resolv.conf before the install |
How the answer is read matters:
- Enter without input selects option 1.
- No answer within 60 seconds removes the container.
- An answer that is not one of the offered numbers keeps the container.
- If the answer cannot be read at all, the container is kept and
Remove it manually if not needed: pct destroy <CTID>is printed. - With
dev_mode=keepthe question is skipped and the container kept. Withdev_mode=breakpointa shell in the container opens first. See Dev mode.
The run then ends with the install script's exit code.
A failure on the host after the container was created, for example while the
network was being awaited, goes through the general error handler instead. It
prints the error line and the log path and asks
Remove broken container <CTID>? (Y/n) [auto-remove in 60s]:. This prompt only
ever refers to the container the current run created.
After a successful install
description writes the container notes shown in the Proxmox VE interface, runs
the var_post_install hook on the host if one is configured (its output goes to
/var/log/community-scripts/post-install-<CTID>.log), and reports the run as
finished. The container script then prints the address of the application.