
Runtime
Docker vs LXC: Application Container or Whole Machine
Docker packages one application; LXC and Incus package a running Linux system. Compare PID 1, state, migration, audit scope and exit paths.
Bare-metal consolidation projects reach this fork in the first week, usually by accident. Someone installs Docker on the new host because that is the container they know, then runs a database, a mail relay and a monitoring stack — each of which expects a machine — on a model built for stateless replicas.
One packages an application, the other packages a machine
Incus states the split without hedging. System containers "run full Linux distributions using a shared kernel" and are "very similar to a virtual machine but sharing kernel with the host system". Application containers "run a single application through a pre-built image" — a model it credits to "the likes of Docker and Kubernetes" — arriving "pre-installed and mostly pre-configured" rather than as "a pristine Linux environment".
LXC itself is not inherently the system-container tool: it is "a userspace interface for the Linux kernel containment features" that can "create and manage system or application containers". The manager on top decides which. Proxmox VE "uses Linux Containers (LXC) as its underlying container technology", abstracted behind pct; Incus is the other manager, and the two no longer gain features in the same order.
Docker's side is a documented convention, not a kernel constraint, and comparisons state it backwards. Its build guidance says "Each container should have only one concern" — then qualifies that "Limiting each container to one process is a good rule of thumb, but it's not a hard and fast rule." Nothing stops you running a full init under Docker; the question is whether the images and tooling around it expect one.
One constraint binds both columns equally, worth stating before anyone sells shared-kernel containers as a substitute for virtual machines: "Only Linux distributions can be run in Proxmox Containers. It is not possible to run other operating systems like, for example, FreeBSD or Microsoft Windows inside a container."
PID 1 is the whole argument
Everything downstream follows from which process gets PID 1. In a Proxmox container the entrypoint option is documented as "Command to run as init", defaulting to /sbin/init. That is the whole proposition in one line: the distribution's own init reads unit files, runs timers, supervises services, reaps orphans and writes a journal — inside the container, with no sidecar and no supervisor shim in the image.
Docker's answer is deliberately smaller. The --init flag "inserts a tiny init-process into the container as the main process, and handles reaping of all processes when the container exits", which Docker calls "superior to using a full-fledged init process such as sysvinit or systemd". A reaper, not a service manager, because the application container is not meant to need one.
The interesting case is where the models meet. Building a Proxmox container from an OCI image, restore_oci_archive concatenates the image's OCI Entrypoint and Cmd. Only if that list is non-empty and its first element is not /sbin/init does the override fire — an image whose command already is /sbin/init keeps the default. When it fires, one branch does three things: writes the command into entrypoint, sets cmode to console, and auto-enables host-managed=1 on every netN device not already setting it.
That third effect is the one that bites. In pct's terms host-managed means "the host (rather than the container) is responsible for the interface's IP configuration" and "The container should not run its own DHCP client or network manager on this interface". Correct for a hello-world image; a surprise if you assumed the container owned its networking.
Regardless of whether that conditional fires, the same function applies the image's User to lxc.init.uid/gid/groups, its WorkingDir to lxc.init.cwd, its Env to the environment and its StopSignal to lxc.signal.halt. The entrypoint branch is conditional; those are not.
#!/usr/bin/env bash
set -euo pipefail
incus launch images:debian/13 app-01 --storage tank
# PID 1 is /sbin/init, not the workload.
incus exec app-01 -- ps -p 1 -o pid,comm,args --no-headers
# The distribution's own service manager supervises inside the container.
incus exec app-01 -- systemctl list-units --type=service --state=running --no-pager
# Install and enable a service the ordinary way. Nothing is rebuilt as an image.
incus exec app-01 -- apt-get update
incus exec app-01 -- apt-get install -y postgresql
incus exec app-01 -- systemctl enable --now postgresql
# Resource limits are applied live against the running instance.
incus config set app-01 limits.memory=8GiB
incus config set app-01 limits.cpu=4
incus config show app-01 | grep -E 'limits\.(memory|cpu)'State: a throwaway layer versus a root filesystem you keep
Docker's storage documentation is explicit about where the line falls: "A volume's contents exist outside the lifecycle of a given container. When a container is destroyed, the writable layer is destroyed with it." The writable layer is scratch space in a convincing disguise, and the failure mode is quiet — it works perfectly until the first docker rm.
A system container inverts the default. The root filesystem is the durable object: a disk that survives restarts and upgrades, with apt writing into it. A relief, because the operational model is the one your team already has — and a liability, because you have acquired another distribution to patch.
Incus refuses to guess where that filesystem lives: "There is no concept of a default storage pool in Incus. When you create a storage volume, you must specify the storage pool to use." The pool comes from the instance's configuration, set three ways — incus launch <image> <name> --storage <pool>, a profile whose root disk device names the pool, or the default profile.
Proxmox splits state the same way: "A container can have multiple mount points. This makes it possible to use the best suited storage for each application." Backups follow the storage — "The vzdump backup tool can use snapshots to provide consistent container backups." If you build the images rather than consume them, owning the base layer is this argument one level down.
# incus profile edit app-tier
#
# cluster.evacuate=refresh-migrate requires Incus 7.4 or a later feature
# release. It is not present in the Incus 7.0 LTS branch.
name: app-tier
description: System-container tier - pinned pool, explicit limits, refresh evacuation
config:
limits.memory: 8GiB
limits.cpu: "4"
# Near-live evacuation. Documented as supported only on ZFS or btrfs, and
# only for containers: other storage drivers have no efficient snapshot
# transfer mechanism for remote transfers.
cluster.evacuate: refresh-migrate
devices:
root:
type: disk
path: /
# Incus has no default storage pool. Name it, or instance creation has
# nothing to place the root filesystem on.
pool: tank
eth0:
type: nic
network: incusbr0
name: eth0Relocating a workload: migration, daemon continuity, and the /proc question
Proxmox VE states its limit in one sentence: "Running containers cannot live-migrated due to technical limitations. You can do a restart migration, which shuts down, moves and then starts a container again on the target node." Its figure — "normally only ... a downtime of some hundreds of milliseconds" — characterises container restart, not your workload; a Postgres container with a long shutdown will not match it.
Two timeouts sit underneath, routinely merged. A restart migration "will shut down the Container and kill it after the specified timeout (the default is 180 seconds)"; the per-container Shutdown timeout is a different setting, defaulting to 60.
Proxmox publishes its own escape hatch, routinely misattributed, so quote it exactly: "for use cases demanding maximum isolation and the ability to live-migrate, nesting containers inside a Proxmox QEMU VM remains a recommended practice."
Incus has moved further. Its howto still says CRIU is not the answer — "only very basic containers (non-systemd containers without a network device) can be migrated reliably", excluding every system container — and recommends you "stop the container, move it over and then start it again". But Incus 7.4 added near-live migration: "the container's filesystem is transferred to the target server through a series of incremental snapshots while the container keeps running. Only the last incremental transfer requires the container to be stopped." That is incus move c1 --target incus02 --stateless --refresh. Incus virtual machines remain the exception, moving "without any downtime" once migration.stateful is enabled.
Docker's two relevant mechanisms are both same-host, and both routinely overstated. Live restore "helps reduce container downtime due to daemon crashes, planned outages, or upgrades", against a default where "when the Docker daemon terminates, it shuts down running containers". Four bounds: "patch releases (YY.MM.x), not ... major (YY.MM) daemon upgrades"; unchanged daemon options "such as bridge IP addresses and graph driver"; "only ... standalone containers, and not ... Swarm services"; and not Windows containers.
Checkpoint and restore is the other: "This command is experimental", and "currently focused on single-host use cases". Its own page meets the cross-host question and declines it — moving a server to another machine "is possible with the current implementation, but not currently a priority". Not a claim it cannot cross hosts; a statement that nobody maintains the path. So the node-drain answer stays what it was: stop, reschedule, start, done by the scheduler above the runtime.
Operators notice on day one that /proc inside a system container describes the container rather than the host — a component, not a property of LXC. LXCFS makes /proc/cpuinfo, /proc/meminfo, /proc/stat and /proc/uptime "container aware such that the values displayed ... really reflect how long the container is running and not how long the host is running", and Proxmox ships it. Its README is equally clear that it "is useable by any runtime" — evidence of what LXCFS does, not of what Docker shows.
#!/usr/bin/env bash
set -euo pipefail
CTID=201
TARGET_NODE=pve-02
# Unprivileged is the default when creating a container; pass it explicitly so
# the intent is in the command, not in a default that differs from the schema.
pct create "$CTID" local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst \
--hostname app-01 \
--unprivileged 1 \
--memory 8192 \
--cores 4 \
--rootfs local-zfs:16 \
--mp0 local-zfs:200,mp=/var/lib/postgresql,backup=1 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp
pct start "$CTID"
# Restart migration is the only migration Proxmox VE offers for a RUNNING
# container (a stopped one migrates offline with plain 'pct migrate'): it shuts
# the container down, moves it, and starts it on the target node. Measure the
# outage your service actually takes.
start_ns=$(date +%s%N)
pct migrate "$CTID" "$TARGET_NODE" --restart --timeout 180
end_ns=$(date +%s%N)
echo "restart migration wall clock: $(( (end_ns - start_ns) / 1000000 )) ms"# /etc/pve/lxc/201.conf - excerpt, site-specific lines elided
arch: amd64
cores: 4
hostname: app-01
memory: 8192
ostype: debian
rootfs: local-zfs:subvol-201-disk-0,size=16G
mp0: local-zfs:subvol-201-disk-1,mp=/var/lib/postgresql,backup=1,size=200G
net0: name=eth0,bridge=vmbr0,ip=dhcp,type=veth
unprivileged: 1Security and audit scope: unprivileged by default, rootless by choice
The default isolation boundary differs, and so does what upstream treats as a bug. Proxmox creates unprivileged containers by default: "The root UID 0 inside the container is mapped to an unprivileged user outside the container", and "This is the default option when creating a new container." One caveat for anyone reading the schema rather than the chapter — the unprivileged key is documented with a default of 0, with creation defaulting to 1.
LXC upstream states the position plainly in both directions: "Unprivileged containers are safe by design. The container uid 0 is mapped to an unprivileged user outside of the container." And for the other kind: "those containers aren't and cannot be root-safe." The page pairs the first claim with staying current on kernel issues.
As privileged containers are considered unsafe, we typically will not consider new container escape exploits to be security issues worthy of a CVE and quick fix. We will however try to mitigate those issues so that accidental damage to the host is prevented.
That disclosure policy comes with the choice. If vulnerability management runs off CVE feeds, a privileged LXC container is a blind spot by design rather than oversight.
Docker's nearest equivalent is rootless mode, and it is opt-in: it "lets you run the Docker daemon and containers as a non-root user to mitigate potential vulnerabilities in the daemon and the container runtime". Note what it moves — the daemon as well as the containers. A Proxmox unprivileged container is not the same control: it sits under a root-owned host stack.
Then there is the scope of the guidance you will be measured against. NIST SP 800-190, published 25 September 2017, defines its subject as "Application container technologies ... a form of operating system virtualization combined with application software packaging." Section 2 separates the other model — "OS containers are different from the application containers that are the topic of this guide" — and its scope line puts all other virtualization outside the document.
Read that for what it says rather than what would be convenient. It does not put a system-container estate outside audit scope, and nothing on NIST's page says auditors arrive holding it as a checklist. It means the most widely cited container-security publication does not, on its own, cover the model you would run — so the control set comes from the host-hardening guidance you already apply, and expect to explain that mapping. The adjacent regulatory thread runs through the Cyber Resilience Act.
The boundary already leaks: Incus and Proxmox now run OCI images
The clean story — Docker for applications, LXC for machines — stopped being true in one direction. Incus consumes OCI registries as image sources: add one with incus remote add <remote_name> <URL> --protocol=oci, Docker Hub being the documented example, and launch straight from it.
Proxmox VE arrived a release later. Version 9.1, released 19 November 2025, added the ability to "Create LXC containers from OCI images", which "allows to create full system containers from suitable OCI images", with application containers "also available (technology preview)". The pct chapter adds the detail that matters: "A container created from an OCI image still uses the existing LXC framework."
The traffic runs one way. There is no documented Docker feature for running a system container, and --init is not it. That asymmetry, rather than any benchmark, is the difference between the two ecosystems in 2026.
What the OCI path gives you in Incus is a small, fixed set of instance options: oci.cwd, oci.dns.domain, oci.dns.nameservers, oci.dns.search, oci.entrypoint, oci.gid and oci.uid. There is no OCI network key — a smaller fact than it looks, because ordinary NIC devices still apply. Read it as what the image configuration can override, not the instance's whole capability.
The practical value is narrower than "Docker is obsolete" and more useful: a consolidation project need not pick one unit for the whole estate on day one. That is optionality designed in rather than hoped for — the property that lets WebAssembly runtimes sit beside OCI containers on a shared scheduler.
#!/usr/bin/env bash
set -euo pipefail
# Add an OCI registry as an Incus remote, then launch straight from it.
incus remote add oci-docker https://docker.io --protocol=oci
incus launch oci-docker:hello-world --ephemeral --console
# A longer-lived one. These are the OCI-conditioned instance keys Incus
# documents; there is no oci.* network key, so networking is an ordinary NIC
# device like any other instance.
incus launch oci-docker:nginx web-01 --storage tank
incus config set web-01 oci.entrypoint='/docker-entrypoint.sh nginx -g "daemon off;"'
incus config set web-01 oci.cwd=/
incus config set web-01 oci.uid=0
incus config set web-01 oci.gid=0
incus config set web-01 oci.dns.nameservers=192.0.2.53
incus config device add web-01 eth0 nic network=incusbr0 name=eth0
# Every oci.* key above is documented "Live update: no", unlike the limits.*
# keys earlier. Setting them on a running instance is accepted and does nothing
# until it restarts.
incus restart web-01# compose.yaml
services:
web:
image: nginx:1.29
# Inserts the tiny init process. A reaper, not a service manager.
init: true
restart: unless-stopped
ports:
- "8080:80"
volumes:
# The only durable part: the writable layer is destroyed with the
# container.
- web-content:/usr/share/nginx/html
volumes:
web-content:Exit ramps in both directions, and who owns the manager
Exits here are cheap, because the artefacts are ordinary: a system container's root filesystem is a directory tree, and an OCI image is an OCI image. The risk sits a level up, in who owns the manager you standardise on. Canonical confirmed in July 2023 that "Canonical is now the main maintainer and the infrastructure is hosted by Canonical." Incus "started as a community fork of Canonical's LXD following Canonical's takeover", is "free of any CLA and remains released under the Apache 2.0 license", and is "maintained by the same team of developers that first created LXD". Both are real options; only one of them cannot change its terms on you.
The licence position has edges, and the slogan version gets it wrong in both directions. canonical/lxd's COPYING is the "GNU AFFERO GENERAL PUBLIC LICENSE Version 3", and its contributing guide requires that "All contributors must sign the Canonical contributor license agreement (CCLA)", licensing contributions "under the project's AGPL-3.0-only license" by default — while recording that "Some files and commits are licensed under Apache-2.0". Canonical's FAQ pins the date — "Community contributions remain under Apache-2.0 - this refers to all contributions made before December 12th, 2023" — and keeps "the Go SDK client package ... under Apache-2.0". A mixed tree with a CLA on top.
The image server made the split operational, with a phase-out ending "2024/05/01 | All LXD users will lose access" and a carve-out: "This does not affect any existing instances you may have running, nor ... anyone exclusively using official Ubuntu images (ubuntu: and ubuntu-daily: remotes)." The maintainer confirmed that "Incus and LXC both have access to all the images on the image server."
Ubuntu's seeding explains why LXD is still everywhere regardless. The 22.04 (jammy) server seed still carries snap:lxd=5.0/stable/ubuntu-22.04; the 24.04 (noble) seed carries lxd-installer in the same position, and the Launchpad bug ties the change to the licence: "With LXD 5.20 there is a license change to AGPL and it has been decided to no longer seed the snap in Ubuntu 24.04 and later." That package is a shim — "Wrapper to install lxd snap on demand" — whose lxc script asks "Would you like to install LXD snap now (Y/n)?" when the snap is missing and a terminal is attached. So 22.04 estates still get LXD from the seed; newer ones get a prompt.
None of that makes LXD a bad tool. It makes the governance question concrete: who can change the terms underneath you, and whether a fork exists you could move to — the same calculation the OpenTofu fork forced one layer up.
The long game: what you are signing up to operate for a decade
Both projects publish horizons that outlast a procurement cycle, somewhere their own documentation has not caught up with. LXC 7.0 LTS was released on 30 April 2026 and "will be supported until June 2031"; the same announcement moves the previous LTS down a gear — "LXC 6.0 will now switch to a slower maintenance pace, only getting critical bugfixes and security updates". The introduction page still lists only 6.0 and 5.0.
Incus is the same shape. The 7.0.1 LTS announcement of 10 July 2026 states "This is the first bugfix release for Incus 7.0 which is supported until June 2031", while the support page still describes the five-year schedule for 6.0 — "supported until June 2029". Take the horizon from the release announcement, the support shape from the support page.
LXC 7.0 sets a floor you inherit on upgrade: it "removes support for: CGroupV1, Kernels without PIDFD support, Kernels without the new mount API". On Proxmox VE 9 the cgroup part is already behind you — that chapter records v1 as removed entirely in 9.0 — but PIDFD and the mount API are a newer floor, binding the hosts rather than the containers.
Which leaves the cost nobody budgets. The system container means owning a distribution's lifecycle inside every container: a package set, a patch cadence, a release upgrade, multiplied by instance count. Not an argument against it — it is the ownership you already carry on every virtual machine, and the ownership that makes the exit cheap. It is an argument for a tier split rather than a doctrine, with the consolidation arithmetic applied to operating systems rather than clusters.
The decade question is not which unit wins — both will still be here. It is whether the unit you picked leaves you able to change your mind. On that axis the system container does unusually well: what you hold is a root filesystem and a configuration file, which nobody can revoke or relicense. If the workloads forcing this choice are full virtual machines, that is a different fork, and a consolidation onto Proxmox VE without Kubernetes is how most estates reach it.
§FAQ/Common questions
Frequently asked
What is the difference between LXC and Docker?
They package different units. Incus documents system containers as running "full Linux distributions using a shared kernel", "very similar to a virtual machine but sharing kernel with the host system", while application containers "run a single application through a pre-built image" — a model it credits to "the likes of Docker and Kubernetes". The practical consequences follow from PID 1: in a Proxmox container the entrypoint option defaults to /sbin/init and is documented as the "Command to run as init", so the distribution's own service manager supervises inside the container. Docker's equivalent is deliberately smaller — the --init flag "inserts a tiny init-process into the container" for reaping, which Docker's own docs call "superior to using a full-fledged init process such as sysvinit or systemd". LXC itself is neutral: it is "a userspace interface for the Linux kernel containment features" that can "create and manage system or application containers". The manager on top is what makes it one or the other.
Can you live-migrate an LXC container between hosts?
Not under Proxmox VE: "Running containers cannot live-migrated due to technical limitations. You can do a restart migration, which shuts down, moves and then starts a container again on the target node." Proxmox characterises that as "normally only in a downtime of some hundreds of milliseconds", which describes container restart rather than your service's recovery, and the migration "will shut down the Container and kill it after the specified timeout (the default is 180 seconds)" — a different setting from the per-container Shutdown timeout, which defaults to 60. Proxmox's own answer where live migration is required is to nest: "for use cases demanding maximum isolation and the ability to live-migrate, nesting containers inside a Proxmox QEMU VM remains a recommended practice." Incus 7.4 added near-live migration, which transfers the filesystem "through a series of incremental snapshots while the container keeps running" rather than the memory state, via incus move --stateless --refresh — but it is "only supported on ZFS or btrfs and only for containers", and it is a feature-branch behaviour, not part of the Incus 7.0 LTS branch.
Does Docker have an equivalent to container live migration?
Not for moving a workload to another host. The two features operators reach for are both same-host. Live restore keeps containers running while the daemon is unavailable — Docker says it "helps reduce container downtime due to daemon crashes, planned outages, or upgrades" — but it is "only supported when installing patch releases (YY.MM.x), not for major (YY.MM) daemon upgrades", only works if daemon options "such as bridge IP addresses and graph driver" did not change, applies "only to standalone containers, and not to Swarm services", and is not supported for Windows containers. Checkpoint and restore via CRIU is marked "experimental" and "currently focused on single-host use cases"; its own page says moving a server to another machine "is possible with the current implementation, but not currently a priority (and so the workflow is not optimized for the task)". The operational node-drain answer is therefore stop, reschedule, start — performed by whatever schedules the containers, not by the runtime.
Can Incus or Proxmox VE run Docker images?
Yes, and the path is documented in both. Incus adds an OCI registry as a remote with "incus remote add <remote_name> <URL> --protocol=oci", with Docker Hub as the documented example, and then launches from it: "incus launch oci-docker:hello-world --ephemeral --console". Proxmox VE 9.1, released 19 November 2025, added the ability to "Create LXC containers from OCI images", which "allows to create full system containers from suitable OCI images", with application containers shipping as a technology preview — and the pct chapter notes that "A container created from an OCI image still uses the existing LXC framework." Incus exposes a fixed set of OCI-conditioned instance keys (oci.cwd, oci.dns.domain, oci.dns.nameservers, oci.dns.search, oci.entrypoint, oci.gid, oci.uid); there is no OCI network key, but ordinary NIC devices still apply. The traffic runs one way: there is no documented Docker feature for running a system container.
Are LXC containers secure enough for production?
Unprivileged ones, on a current kernel, are the model both projects stand behind. LXC upstream says "Unprivileged containers are safe by design. The container uid 0 is mapped to an unprivileged user outside of the container and only has extra rights on resources that it owns itself", and Proxmox makes unprivileged the default when creating a container. The policy consequence matters more than the slogan: for privileged containers, LXC upstream states it "will not consider new container escape exploits to be security issues worthy of a CVE and quick fix", so a privileged container is outside the CVE feed your vulnerability management probably relies on. Docker's comparable control, rootless mode, is opt-in and moves the daemon as well as the containers off root. On audit scope, note that NIST SP 800-190 defines its subject as "application container technologies" and says "OS containers are different from the application containers that are the topic of this guide" — so it does not by itself cover a system-container estate, and the control set has to be assembled from host-hardening guidance instead.
Further reading
- Docker Swarm vs Kubernetes: Four Gates, Not Node Count
- Containers vs VMs: which isolation boundary are you trusting
- Leaving VMware Without Kubernetes: Proxmox VE in Production
- KubeVirt: The VMware Exit for VMs You Cannot Containerise
- WebAssembly on Kubernetes: SpinKube and runwasi
- Hard Multi-Tenancy with vCluster: Retire 30 Clusters
- Distroless Without a Vendor: Building Your Own Base Images
- OpenTofu Migration: The Registry Is the Hard Part
- The EU Cyber Resilience Act: What It Means for Self-Hosted
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.