KVM vs LXC
Where the guest ends and the host begins
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.
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.
Boot time and memory: paying for a kernel
What a guest kernel costs
simulated, typical small Debian guestsA 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).
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.
Unprivileged containers: root, but not really
UID mapping in unprivileged containers
# /etc/pve/lxc/101.conf unprivileged: 1 mp0: /tank/media,mp=/srv/media # /etc/subuid and /etc/subgid root:100000:65536
What each one can and can't do
| VM (qm / KVM) | Container (pct / LXC) | |
|---|---|---|
| Guest OS | Anything x86-64: Linux, Windows, BSD, appliances | Linux distributions only (templates via pveam) |
| Kernel | Its own, any version | The host's proxmox-kernel; uname -r shows the host |
| Kernel modules, sysctls | Load whatever you want | Modules must be loaded on the host; only namespaced sysctls |
| Live migration | Yes, online (RAM copied while running); local disks too with storage migration | No. Restart migration: stop, move, start (seconds of downtime) |
| Docker / Kubernetes inside | Native, the recommended way | Works with nesting=1 but officially discouraged: storage drivers, AppArmor and kernel updates can break it |
| Memory | Allocated to the guest; ballooning and KSM reclaim some | A cgroup limit; unused memory stays free for the host |
| Disks | Virtual block devices (virtio-scsi), guest formats them | A file system mounted by the host; bind mounts (mpN) share host directories |
| Hardware passthrough | Whole PCIe devices via IOMMU (GPUs, HBAs, NICs) | Device nodes only (devN: /dev/dri/renderD128) |
| Isolation boundary | Hardware virtualization: small, well-audited surface | Host syscall interface: one kernel bug can affect all CTs |
The same idea, two config files
# 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# 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: 1Choosing a VM's hardware: VirtIO, q35, CPU type
- 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.
CPU type decides where a VM may live-migrate
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.
Creating and moving each kind
# 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- 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.