Proxmox Deep Dive Part 6: VMs vs LXC Containers

Part 5 covered the firewall. This part covers the decision Part 1 flagged but didn’t resolve: when a workload should be a KVM virtual machine, and when it should be an LXC container.

What actually differs.

A KVM VM gets its own complete kernel, its own complete OS, and hardware-level isolation from the host and from every other VM. An LXC container shares the host’s kernel, running as an isolated set of processes inside Linux namespaces and cgroups, with no separate kernel and no separate OS boot. That single difference cascades into everything else: a VM can run any OS the hypervisor supports, Windows, BSD, a different Linux distribution entirely, because it has its own kernel; a container can only run Linux, and in practice runs best when its userland is reasonably close to the host’s own.

The overhead difference is real and is the other side of that same tradeoff. A container starts in roughly the time it takes to fork a process, consumes memory proportional to what its actual workload uses rather than needing a fixed allocation for a whole guest OS, and a single host can run meaningfully more containers than VMs for equivalent hardware. A VM’s isolation is also correspondingly stronger: a kernel-level exploit inside a container has a shorter path to the host than the same exploit inside a VM, because the container’s processes are, underneath the isolation, still processes on the host’s own kernel.

The actual decision.

Needs a different OS than the host, or genuine hardware-level isolation matters, particularly for anything with an adversarial or untrusted workload: KVM. This is why every lab in the Sn1per and BloodHound series ran as full VMs rather than containers, a lab explicitly designed to be attacked or to run untrusted code wants the strongest isolation boundary available, not the lightest one.

A Linux-native service where isolation from other guests matters but isolation from a hostile workload doesn’t, and density or fast start/stop matters: LXC. A home automation stack, a DNS resolver, a lightweight web app, these are exactly the profile LXC was built for, genuinely isolated from each other, cheap enough to run a dozen of on modest hardware, and none of them need their own kernel.

Unprivileged containers as the default, not the exception.

An unprivileged LXC container maps its root user to an unprivileged UID range on the host, so a process that somehow escalates to root inside the container is still an unprivileged user outside it. A privileged container’s root actually is the host’s root, with a correspondingly larger blast radius if anything inside it is compromised. Proxmox defaults new containers to unprivileged, and there’s rarely a good reason to override that default, the main cases that genuinely need a privileged container are workloads needing direct hardware access or certain kernel features namespaces don’t fully virtualize, and those cases are the exception specifically because they’re accepting a real security tradeoff to get something a container otherwise can’t do.

Templates and cloud-init.

VM templates and container templates solve the same problem differently. A VM template is a preconfigured, stopped VM marked as a template, cloned (ideally on ZFS or LVM-thin, where cloning is a fast copy-on-write operation rather than a full disk copy) to create new VMs, then customized on first boot via cloud-init, which injects hostname, network config, SSH keys, and a first-boot script into a cloud-init-aware OS image without ever needing to log in manually. Container templates are simpler: a prebuilt root filesystem tarball (Proxmox ships an official template repository covering most common distributions) that gets unpacked directly into a new container, with Proxmox’s own container creation wizard handling hostname and network config directly, no cloud-init layer needed since the container never boots a separate kernel to hand configuration to in the first place.

Part 7 moves to what happens once a homelab grows past one node: corosync, pmxcfs synchronization in practice, and the quorum math that decides how many node failures a cluster can actually survive.