02

KVM vs LXC

Proxmox runs two very different kinds of guests from one UI. A VM is a complete computer emulated by QEMU and accelerated by KVM: its own firmware, its own kernel, any OS. A container is a group of Linux processes on the host kernel, fenced off with namespaces and cgroups. Neither is "better": the difference is where the isolation boundary sits, and that one fact explains the overhead, the security model and what each can do.

Where the guest ends and the host begins

VM 100 (KVM)
your app
nginx, postgres…
guest userspace
any OS: Linux, Windows, BSD
guest kernel
its own kernel, own modules
virtual hardware
q35 chipset · OVMF · virtio-net/-scsi
QEMU process + KVM
/usr/bin/kvm · /dev/kvm · VT-x
host kernel
proxmox-kernel 6.x
hardware
CT 101 (LXC)
your app
nginx, postgres…
guest userspace
Linux distro rootfs only
namespaces · cgroups · AppArmor · seccomp
a fence around processes
host kernel (shared)
the same kernel for every CT
hardware
dashed box = what the guest owns · its edge is the isolation boundary · bottom layers are the host
Full virtual machine (qm)

Boundary: virtual hardware. The guest talks to emulated/paravirtual devices and the CPU traps privileged instructions to KVM. To escape, an attacker needs a bug in QEMU's device model or KVM itself.

System container (pct)

Boundary: the syscall interface. Container processes are host processes making normal syscalls to the shared kernel; namespaces hide the rest of the system, seccomp and AppArmor filter dangerous calls. A kernel bug is a potential escape for every container at once.

overhead

Boot time and memory: paying for a kernel

Most of a VM's overhead is not virtualization itself (KVM runs guest code at near-native speed) but the fact that every VM boots and maintains a whole operating system.

What a guest kernel costs

simulated, typical small Debian guests
t = 0.0 sCT ready at 1.5 s · VM at 11.0 s
VM 100stopped
CT 101stopped

A VM boots like a physical server: firmware, bootloader, its own kernel, then init. A container skips straight to init, because the kernel is already running: pct start only sets up namespaces and cgroups around a new process tree. Real VM numbers range from ~6 s (SeaBIOS, tuned initramfs) to a minute (Windows).

10 VMs4.6 GiB (2.1 GiB overhead)
10 CTs2.7 GiB (0.2 GiB overhead)
overhead / VM
~220 MiB
overhead / CT
~25 MiB
saved
1.9 GiB

Solid = the app, hatched = per-guest overhead. A rough model: every VM carries its own kernel, slab and page cache; containers share the host's. KSM (same-page merging) and the balloon driver claw some VM memory back.

security

Unprivileged containers: root, but not really

Proxmox creates containers as unprivileged by default. A user namespace shifts every UID by 100000, so the container's root is an ordinary unprivileged user on the host. Pick a UID and see where it lands, then try the bind-mount scenario that trips everyone up.

UID mapping in unprivileged containers

inside the container (what `id` prints)065535on the host (what `ps` / `ls -n` show)⋯0 … 65535 (host's real users)100000 … 165535 (subuid range)
CT uid 0→host uid 100000
Container root is a nobody on the host
Root inside CT 101 is UID 100000 on the host: it owns nothing there. Even if a process escapes the namespace, it lands as an unprivileged user.
bind mount /tank/media → /srv/media, owned by host uid 1000
Inside the CT the files show up as nobody:nogroup (65534): host 1000 isn't in the map. The container can read world-readable files but can't write. This is the classic "permission denied on my bind mount" problem.
# /etc/pve/lxc/101.conf
unprivileged: 1
mp0: /tank/media,mp=/srv/media

# /etc/subuid  and  /etc/subgid
root:100000:65536
capabilities

What each one can and can't do

VM (qm / KVM)Container (pct / LXC)
Guest OSAnything x86-64: Linux, Windows, BSD, appliancesLinux distributions only (templates via pveam)
KernelIts own, any versionThe host's proxmox-kernel; uname -r shows the host
Kernel modules, sysctlsLoad whatever you wantModules must be loaded on the host; only namespaced sysctls
Live migrationYes, online (RAM copied while running); local disks too with storage migrationNo. Restart migration: stop, move, start (seconds of downtime)
Docker / Kubernetes insideNative, the recommended wayWorks with nesting=1 but officially discouraged: storage drivers, AppArmor and kernel updates can break it
MemoryAllocated to the guest; ballooning and KSM reclaim someA cgroup limit; unused memory stays free for the host
DisksVirtual block devices (virtio-scsi), guest formats themA file system mounted by the host; bind mounts (mpN) share host directories
Hardware passthroughWhole PCIe devices via IOMMU (GPUs, HBAs, NICs)Device nodes only (devN: /dev/dri/renderD128)
Isolation boundaryHardware virtualization: small, well-audited surfaceHost syscall interface: one kernel bug can affect all CTs
config

The same idea, two config files

Both configs live in /etc/pve and use storage volume IDs, so backup, replication and HA treat them alike. The difference shows in the keys: a VM describes hardware (machine, bios, scsihw, efidisk), a container describes a process environment (unprivileged, features, mount points).
VM 100
# qm config 100   →  /etc/pve/qemu-server/100.conf
agent: 1
balloon: 2048
bios: ovmf
boot: order=scsi0
cores: 4
cpu: x86-64-v2-AES
efidisk0: local-lvm:vm-100-disk-1,efitype=4m,pre-enrolled-keys=1,size=4M
machine: q35
memory: 8192
name: web01
net0: virtio=BC:24:11:5E:20:01,bridge=vmbr0,firewall=1
ostype: l26
scsi0: local-lvm:vm-100-disk-0,discard=on,iothread=1,ssd=1,size=32G
scsihw: virtio-scsi-single
CT 101
# pct config 101   →  /etc/pve/lxc/101.conf
arch: amd64
cores: 2
features: nesting=1,keyctl=1
hostname: ct-web
memory: 1024
swap: 512
mp0: /tank/media,mp=/srv/media
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:3A:7F:02,ip=dhcp,type=veth
ostype: debian
rootfs: local-lvm:vm-101-disk-0,size=8G
unprivileged: 1
virtual hardware

Choosing a VM's hardware: VirtIO, q35, CPU type

For VMs, the defaults matter more than they look. Paravirtual VirtIO devices avoid emulating real hardware, and the machine and CPU type define the "computer" the guest believes it is running on, which must stay the same when it moves to another node.
  • virtio-scsi-single + iothread
    One SCSI controller per disk, each with its own I/O thread: disk I/O no longer queues behind the main QEMU loop. The default for new Linux VMs.
  • virtio-net
    Paravirtual NIC: the guest driver knows it's virtual and exchanges packets via shared-memory rings (vhost-net in the host kernel) instead of emulating an Intel e1000 register by register.
  • virtio-balloon
    Lets the host reclaim memory the guest isn't using (balloon: min, memory: max) and reports guest memory stats to the UI.
  • qemu-guest-agent (agent: 1)
    A daemon in the guest over a virtio-serial channel: freezes file systems for consistent backups, reports IPs, runs clean shutdowns.
machine: q35 (vs the older i440fx)
q35 emulates a modern chipset with native PCIe, which PCIe passthrough and many modern guests expect. The machine version (e.g. pc-q35-11.0+pve1) is pinned for Windows guests and on live migration, so a QEMU upgrade never changes the hardware the guest sees under its feet.
Windows needs drivers
Windows has no VirtIO drivers built in: attach the virtio-win ISO during install (or start with SATA + e1000 and switch later). Linux has had them in the kernel for over a decade.

CPU type decides where a VM may live-migrate

guest sees:SSE4.2POPCNTAES-NIAVXAVX2AVX-512
pve1x86-64-v4
AMD EPYC 9354 (Zen 4)
▶VM 100 running here
pve2x86-64-v4
AMD EPYC 9354 (Zen 4)
✓Supports the model: live migration works
pve3x86-64-v3
Intel Xeon E5-2690 v4 (Broadwell)
✓Supports the model: live migration works
pve4x86-64-v2
Intel Xeon E5-2670 (Sandy Bridge)
✓Supports the model: live migration works

The default for new VMs. Runs on anything from Intel Westmere / AMD Opteron G4 up, so it migrates across mixed-generation clusters, and includes AES-NI so TLS and disk encryption stay fast. Rule of thumb: pick the newest model that every node in the cluster supports.

cli

Creating and moving each kind

qm vs pct
# A VM: you bring the OS (ISO or cloud image), Proxmox brings virtual hardware
qm create 100 --name web01 --machine q35 --bios ovmf --cpu x86-64-v2-AES \
  --cores 4 --memory 8192 --balloon 2048 --scsihw virtio-scsi-single \
  --scsi0 local-lvm:32,iothread=1,discard=on --efidisk0 local-lvm:1,efitype=4m \
  --net0 virtio,bridge=vmbr0 --agent 1 --ostype l26 \
  --ide2 local:iso/debian-13.1.0-amd64-netinst.iso,media=cdrom

# A container: you pick a template, Proxmox unpacks it as the root file system
pveam update && pveam available --section system | grep debian-13
pveam download local debian-13-standard_13.1-2_amd64.tar.zst   # exact name from the list above
pct create 101 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname ct-web --cores 2 --memory 1024 --unprivileged 1 \
  --features nesting=1 --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp --ssh-public-keys ~/.ssh/id_ed25519.pub

qm migrate 100 pve2 --online                 # live: guest keeps running
pct migrate 101 pve2 --restart               # stop → move → start on pve2
Containers for app images?
For Docker/OCI workloads the Proxmox docs recommend a VM. Proxmox VE 9.1 added the ability to create LXC containers from OCI images (system containers from suitable images; application containers are a technology preview), but they still run as LXC on the host kernel, not under a Docker daemon.
  • why it mattersThe isolation boundary is the whole story: VMs stop at virtual hardware, containers at the syscall interface. Overhead, security and capabilities all follow from that.
  • why it mattersKeep containers unprivileged. When a bind mount says permission denied, fix the UID mapping (lxc.idmap + /etc/subuid), don't flip the container to privileged.
  • why it mattersPick the CPU type for the cluster, not the node: the newest x86-64-vN level every node supports keeps live migration safe; host only for identical CPUs.