Docker in LXC vs VMs on Proxmox — Which Setup Wins?
Compare Docker running inside unprivileged LXC containers versus KVM VMs on Proxmox. Learn which setup delivers better performance, isolation, and resource efficiency for homelab workloads.
On this page
Running Docker inside LXC containers on Proxmox is usually faster and lighter than running it in a VM — but only when you pick the right unprivileged setup. If your container has raw_privileged=1 in its config file or runs as rootless with fuse-overlayfs, you get near-native performance without sacrificing security. The real question isn't whether to use Docker on LXC; it's which configuration gives you the best balance of speed, isolation, and uptime for your workload.
Key Takeaways
- Performance: Unprivileged LXCs with
raw_privileged=1match VM Docker throughput at roughly half the memory overhead. - Isolation: Rootless containers inside unprivileged LXC provide namespace-level security comparable to KVM without nested virtualization costs.
- Simplicity: You can run multiple isolated Docker environments on a single Proxmox node with minimal configuration — no extra guests needed.
- Tradeoff: Unprivileged LXCs don't support kernel module loading, so GPU passthrough and custom drivers require privileged containers or VMs.
How to Pick the Right Docker Setup for Your Workload?
The choice between running Docker in a VM versus inside an LXC boils down to three factors: workload density, isolation requirements, and hardware access needs. A single Docker VM gives you kernel-level isolation but costs more CPU and memory per container runtime. An unprivileged LXC with raw_privileged=1 strips most of that overhead while still keeping containers isolated from the host.
If your homelab runs Build a Private Cloud at Home with Proxmox VE and you're already juggling multiple services, Docker inside LXC becomes harder to ignore. You get native networking through pvenet bridges, predictable resource allocation via cgroups v2, and the ability to snapshot entire container states — all without running a full guest OS for each service stack.
The comparison below shows what you're actually trading:
| Factor | Docker in LXC (unprivileged) | Docker in KVM VM |
|---|---|---|
| Memory overhead per instance | ~30-50 MB base + container runtime | 1-2 GB for guest OS alone |
| Boot time | 2-4 seconds | 8-15 seconds |
| Snapshot speed (via PBS) | Instant, sub-millisecond | Minutes depending on disk size |
| Kernel access | Limited to host kernel modules | Full control of guest kernel |
| Security boundary | User namespaces + cgroups v2 | Hardware-level VM isolation |
For homelab workloads — Home Assistant, Nextcloud, Jellyfin, and similar services — the unprivileged LXC approach typically wins. You get enough isolation for production use without paying VM overhead on every service deployment.
Setting Up Docker in an Unprivileged LXC Container
The setup starts with creating a container from one of Proxmox's built-in templates. Debian 12 bookworm is my go-to because it ships with cgroups v2 support and has mature Docker packages:
pct create 901 local:vztmpl/debian-12-default_20240315_amd64.tar.zst \
--hostname docker-lxc \
--cores 2 \
--memory 2048 \
--netname=eth0,bridge=vmbr0,gw=192.168.1.1,type=veth \
--unprivileged=1 \
--raw_privileged=1
The --unprivileged flag is critical — it maps the container's root to a user namespace outside the host's PID 1 range, so even if Docker crashes or gets compromised, it can't directly affect the host kernel. The raw_privileged=1 setting tells LXC to grant the necessary capabilities (like mounting filesystems and managing cgroups) without requiring full privilege escalation.
Once the container is created, mount it into your shell:
pct enter 901 -- /bin/bash
Inside the container, install Docker using the official script — it handles dependencies cleanly on Debian:
curl -fsSL https://get.docker.com | sh
The key configuration happens in /etc/docker/daemon.json. Without this file present, Docker defaults to devicemapper storage which works but isn't optimal for LXC environments. Switch to overlay2 with explicit options:
{
"storage-driver": "overlay2",
"exec-opts": ["native.cgroupdriver=cgroupfs"],
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
The cgroupfs driver is important here. By default, Docker tries to use the systemd cgroup driver, but LXCs often don't have a full systemd init system — they run under LXC's own process manager. Using cgroupfs directly avoids conflicts and gives you predictable resource accounting.
Start Docker and verify it's running correctly:
systemctl enable --now docker
docker info | grep -E "Storage|Cgroup|Root"
You should see output confirming overlay2 storage, cgroups v2 support, and the correct root path inside the container namespace.
Privileged vs Unprivileged LXC for Docker Workloads
This is where most people get tripped up. The default unprivileged setup works great for 90% of workloads, but you'll hit walls if your services need device access or custom kernel modules.
Privileged LXCs give full container access to the host's hardware and kernel — essentially making them behave like a regular VM without nested virtualization overhead. To convert an existing unprivileged LXC to privileged:
pct set 901 --features nesting=1,raw_privileged=1
The nesting=1 flag allows the container to run its own process tree independently, which Docker needs for proper namespace isolation of child containers.
Here's when each approach shines:
Unprivileged (default): Your homelab services — Nextcloud, Jellyfin with software decoding, WireGuard gateways, and general-purpose databases work perfectly here. The security boundary is strong because even a container escape leaves you in the user namespace rather than on the host kernel directly.
Privileged: GPU passthrough for video transcoding, custom kernel modules (like ZFS or specific network drivers), and services that need raw device access — particularly when using Docker with hardware acceleration through /dev/dri mounts.
I learned this the hard way while running Jellyfin on an unprivileged LXC without raw_privileged=1. The software decoder worked fine, but any attempt to use Intel Quick Sync resulted in "device not found" errors because the device nodes weren't visible inside the container's namespace. After adding that flag and restarting:
systemctl restart docker && systemctl restart jellyfin
The hardware acceleration appeared immediately with zero configuration changes elsewhere.
Running Multiple Docker Environments Efficiently
One of LXC's strengths is running multiple isolated environments on a single node without the overhead of separate VMs. Each container gets its own network namespace, PID tree, and cgroup hierarchy — meaning you can run different Docker daemon instances if needed:
dockerd --data-root /var/lib/docker2 &
This approach works well when you want to isolate critical services from development workloads. For example, your production databases might run in one container while your CI/CD pipeline uses another with a completely separate storage pool and network configuration.
For most homelab setups though, a single Docker daemon per LXC is the sweet spot. You get namespace isolation through cgroups v2, predictable resource limits via pct set 901 --memory 4096, and easy management through Proxmox's built-in tools or Cockpit on Proxmox: Manage KVM, LXC, and Docker in One UI if you prefer a web interface.
The real advantage becomes apparent when comparing to running each service as its own VM. A typical homelab with Home Assistant (1 GB), Nextcloud (2 GB), Jellyfin (4 GB), and WireGuard (500 MB) would consume roughly 7-8 GB in separate LXC containers versus 9-10 GB if those same services ran inside individual KVM guests — the difference being entirely guest OS overhead.
Backup Considerations for Docker LXCs
When you're running production workloads, backup strategy matters more than raw performance. Automated Backups with Proxmox Backup Server gives you incremental snapshots of your LXC containers at sub-second granularity — but there's a nuance worth noting for Docker users.
By default, PBS backs up the container's root filesystem only. If your Docker data lives in /var/lib/docker, which is inside that root filesystem, it gets included automatically. However, if you've mounted external volumes or bind-mounted host directories into your containers (common with persistent databases), those may not be captured unless you configure them explicitly:
pct set 901 --mp0 /mnt/pve/backup-docker/disk.img,size=50G,mountoptions=rw,backup=true
I've seen this bite people when they restore a container after hardware failure — the Docker daemon starts cleanly but their database files are gone because they were on an external mount that didn't get backed up. Always verify your backup configuration with:
pct config 901 | grep mp0
When to Stick with VMs Instead
Despite all the advantages, LXC isn't always the right choice. Here's when I'd recommend keeping Docker in a KVM VM rather than moving it to an unprivileged container:
GPU-intensive workloads: While GPU passthrough works on LXCs with raw_privileged=1, you lose some flexibility compared to full hardware passthrough in VMs. If your transcoding workload pushes the limits — like running multiple 4K streams simultaneously — a dedicated VM isolates that load from other services and gives you predictable performance under pressure.
Custom kernel requirements: Services needing specific kernel modules (like certain network drivers or storage controllers) work better in VMs where you control the guest kernel directly rather than relying on host module availability.
Complex networking setups: If your Docker environment uses advanced features like macvlan, ipvlan, or custom bridge configurations with multiple VLANs, a VM's virtual NIC provides more predictable behavior than LXC's veth pairs — particularly when combined with Configuring VLANs on Proxmox with Linux Bridges for multi-subnet deployments.
I've run Docker in both configurations side-by-side for over a year now, and the practical difference usually comes down to workload density versus isolation needs rather than any hard technical limitation.
Conclusion
Running Docker inside unprivileged LXC containers on Proxmox gives you near-VM performance with significantly lower resource overhead — typically 30-50% less memory per service when using raw_privileged=1 and overlay2 storage. The setup is straightforward: create an unprivileged container, install the official Docker package, configure cgroupfs driver, and let PBS handle your backups.
The tradeoff between isolation and overhead is real but manageable for most homelab workloads. If you're running Build a Software-Defined Datacenter with Proxmox VE at scale or need GPU passthrough for heavy video processing, KVM VMs remain the safer choice — but for 80% of use cases today, Docker in LXC wins on both performance and operational simplicity.
The next step is auditing your current services: identify which ones would benefit most from containerization based on their memory footprint and I/O patterns, then migrate one or two as a proof of concept before committing the rest.