VM, LXC, or Docker container
"Should I run this in a VM, an LXC container (Linux system container that shares the host kernel), or Docker?" is the most asked question in every self-hosting forum, and it is a category error disguised as a deployment choice (fight me, again). I asked it myself for years, badly. It treats the three as competitors in one category, and they are not: one is an isolation boundary, one is a lightweight operating system instance, and one is a packaging format. The day that clicked, most of my placements stopped being decisions and started being consequences.
What each one actually is
A VM is a kernel boundary. QEMU/KVM (open-source machine emulator and Linux kernel virtualization) gives the guest its own kernel and virtual hardware; guest code reaches the host through the virtualization interface, not the host's syscall table. That is the strongest isolation a hypervisor (software that runs and isolates virtual machines) hands out, and it is the boundary I reach for when I want to stop thinking about a workload's neighbors. It also runs anything - Linux, Windows, BSD - and can be live-migrated (moved between hosts without shutting down the guest) between nodes while running, which matters more to me than any benchmark.
An LXC container is a system container. A full distribution userland (operating-system programs and libraries outside the kernel) with its own init, administered like a small server - except it shares the host's kernel. It boots in moments and carries no second kernel, no QEMU device model, no per-guest page cache duplication. I like them because they are boring in the good way: a small server, administered like every other server. Proxmox runs them unprivileged by default: root inside maps to an unprivileged user outside, layered under AppArmor, seccomp, and cgroup limits (Linux controls for access policy, filtering kernel requests, and resource limits). Real risk reduction - but I never let myself forget that the shared kernel stays shared.
A Docker container is a packaged application. One service and its
dependencies, built as an image, started from ENTRYPOINT, designed to be
replaced rather than administered. Under the hood it leans on the same
kernel primitives LXC uses - namespaces (kernel feature that isolates process views of the system), cgroups, seccomp - configured to
different defaults; the difference that matters is lifecycle, not mechanism.
LXC asks "how do I run another Linux?", Docker asks "how do I ship this
app?". I spent an embarrassing amount of time comparing them on isolation
before noticing they answer different questions.
The decision, as actually made
Four criteria carry nearly all the weight, and each one is a scar with a date on it.
Trust and the kernel boundary. Proxmox's own documentation says it plainly: containers expose host-kernel attack surface, and full VMs provide better isolation for unknown or untrusted users. I do not argue with that sentence; I have read enough container-escape writeups to not want to test it personally. Anything internet-exposed, anything running semi-trusted code, anything whose compromise must not matter to its neighbors gets a VM. Unprivileged LXC is for services I trust and merely want cheap.
High availability and maintenance. Underrated until the first time you have to empty a node with guests still running. A running VM live-migrates: the guest moves and nobody notices. A running LXC container cannot - Proxmox only supports restart migration: shut down, move, start again. Usually brief, not guaranteed brief, and always an interruption I have to schedule around. Services that must glide through planned maintenance therefore live in VMs; and on actual node failure, HA (automatic restart of guests on another node after failure)-managed guests of both types restart elsewhere once the dead node is fenced (isolated from shared resources before recovery starts) - HA is automatic recovery, never zero downtime, and I stopped pretending otherwise.
Backup and restore granularity. Both types back up to Proxmox Backup Server as full restorable guests, so this is closer than it once was - VMs at block level with live restore, containers at file level where snapshot mode needs snapshot-capable storage. The real difference is what "restore" means: a VM restore brings back a machine; a container or Docker rebuild brings back a role. I have needed both, and the only restores that went badly were the ones where I had not decided which of the two I was counting on before the disk died.
Overhead. Every VM pays for its own kernel, its services, and its memory - ballooning (dynamic reclaim of unused guest memory back to the host) softens this but needs guest cooperation. Containers share the host kernel and pack far denser. That is real money on shared hardware, and it is exactly the budget the first criterion spends. Asking only "which is lighter?" is benchmark-brained nonsense; the useful question is "is this workload worth a kernel of its own?" - and I try to answer it with the invoice in mind, not the benchmark.
Where Docker actually goes
Not on the hypervisor. Proxmox's FAQ is explicit: do not run Docker directly on the host. I regard doing it anyway as avoidable operational sabotage. Docker writes firewall rules for its bridges, spawns cgroups per container, and runs a root daemon, all on the machine whose whole job is predictability. The conservative pattern, and the one used here, is host, then VM, then Docker: application containers get their own kernel to share, and that kernel is not the hypervisor's. Docker inside LXC can be made to work - I tried, twice - but it stacks namespace layers on the same host kernel and usually means loosening the container to get there. A trade to make knowingly or not at all; I made it unknowingly once, which is why it gets a sentence here.
Since 9.1, Proxmox can also instantiate OCI images (standardized container images that multiple runtimes can consume) directly as LXC guests - application containers without a Docker daemon. It is a technology preview, it inherits LXC semantics including restart-only migration, and it does not change the placement logic above yet. Worth watching, not yet worth migrating to.
Full disclosure: I almost always skip it
The flowchart above is the sober version. In practice, when a workload could go either way, Docker almost never wins here. Two reasons, neither flattering.
I am too dumb for it. Not for writing a Dockerfile - for operating the
result. A running Docker host accumulates state in more places than I can
keep in my head: images, layers, volumes that outlived their containers,
networks, iptables chains the daemon rewrote, and a compose file that no
longer quite matches reality. Every one of those is knowable; all of them
at once, at 2 AM, during an outage, is a memory test I keep failing (ask the graveyard of dangling volumes). A
systemd service is one unit file and one journalctl -u away from its
logs; an LXC container behaves like every Linux server I have administered
since the beginning. Tools are supposed to reduce what a person holds in
their head. For my workloads, Docker increases it, and the 2 AM version of me
is already a fucking liability.
I just do not like it. The default daemon is root and acts like it (rootless mode exists; defaults are what actually ship), images are a supply chain audited by nobody in this room, and the ecosystem's habit of answering "how do I run this?" with a 40-line compose file has trained upstreams to ship complexity instead of reducing it. None of that makes Docker bad engineering - it is engineering aimed at a problem I mostly do not have. Preferences are allowed to be preferences, but this one comes with receipts.
Which is exactly why the Docker VM from the flowchart exists: for the third-party apps that are only seriously distributed as images, where repackaging by hand is a worse use of a life than accepting the compose file. They earn their place by being someone else's packaging problem, and the VM contains the blast radius - and my opinion.
How that plays out here
Role-level examples from this setup, one per pattern:
- The PR review bot runs agent CLIs against untrusted pull-request content: own VM, without discussion.
- Dependency-heavy third-party apps that ship as compose files run in Docker, inside a VM that exists to hold them.
- Small trusted single-purpose services - internal tooling with no exposure and no exotic dependencies - sit in unprivileged LXC containers, dense and cheap.
The pattern behind the pattern, same as in the role-level note: every workload gets the cheapest boundary that still isolates it. The three technologies are not steps on a quality ladder. They are three different answers, and the skill is matching the answer to the question the workload is actually asking.