Docker on Proxmox: LXC vs VM, Done Right

The right way to run Docker on Proxmox — why a dedicated VM beats a privileged LXC, how to set both up, and Portainer for a web UI. Avoid the docker-in-LXC pitfalls that cost real downtime.

On this page
  1. The decision: VM, almost always
  2. Task 1: Build the Docker VM (recommended path)
  3. Task 2: The LXC path (only if you must)
  4. Task 3: Add Portainer
  5. What’s next

Docker is the key that unlocks the enormous ecosystem of self-hosted apps — most projects ship a docker run command or a compose.yaml as their primary install method, so sooner or later every Proxmox homelab needs a Docker host. But where you run Docker on Proxmox matters more than beginners expect, and it’s a question with a genuinely wrong answer. Get it wrong and a routine host kernel upgrade can take every one of your services down at once — a failure mode that has caught plenty of experienced people off guard.

Docker on Proxmox: LXC vs VM, Done Right — video walkthrough

This guide covers the real decision — LXC vs VM for Docker — the setup for both, and Portainer for a friendly web UI.


The decision: VM, almost always

Here’s the short version, then the reasoning:

Docker in a VM (recommended) Docker in an LXC
Kernel Its own — behaves as documented Shared with host
Upgrades Clean, isolated Can break on host kernel upgrade
Overhead ~500 MB–1 GB base RAM Very low
Isolation Strong Weak (esp. if privileged)
Best for Your real Docker host One throwaway lightweight app

The reasoning comes down to one fact: Docker and LXC are both Linux container systems. Each one works by slicing up a shared kernel with namespaces and cgroups, and running Docker inside LXC stacks those two slicing mechanisms on top of each other — something neither was designed for. It does work with the right features enabled, but it’s fragile in a specific, frustrating way: the setup that ran fine for months can break the day Proxmox ships a new host kernel, because the inner Docker layer depended on kernel behavior that just changed underneath it. A VM sidesteps the entire problem. It brings its own kernel, so Docker behaves exactly as its documentation and every tutorial online assume. Unless you’re squeezing a single tiny app onto an already-busy node, use a VM — the half-gigabyte of extra RAM is the cheapest insurance in your lab.

The honest exception

For one lightweight, stateless app on a RAM-constrained node, a nesting-enabled unprivileged LXC running Docker is fine — as long as you’re happy to rebuild it if a Proxmox upgrade breaks it. For everything else, the VM is worth the extra half-gig of RAM.


First: make these values your own

The commands below contain example values to replace with your own: youradmin (your actual login username), container ID 120 and hostname docker01 (any free ID and name in your cluster), the Debian template filename (match what pveam available lists on your node), and local-lvm (your storage name). If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.

1Create a Debian VM10 min

Create a VM following the first-VM walkthrough: Debian 12, 2 vCPU, 4 GB RAM, 32 GB disk, QEMU guest agent enabled. A cloud-init template makes this instant if you have one.

2Install Docker Engine3 min

Inside the VM, use Docker’s official convenience script, then verify:

Install Docker Engine on Debian

curl -fsSL https://get.docker.com | sh
docker run --rm hello-world

Add your user to the docker group so you don’t need sudo for every command:

Run docker without sudo

usermod -aG docker youradmin

Log out and back in for the group change to take effect.


Task 2: The LXC path (only if you must)

1Create a nesting-enabled unprivileged container3 min

If you’re deliberately going the LXC route, the container needs the nesting and keyctl features:

Create an LXC ready for Docker

pct create 120 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
--hostname docker01 \
--cores 2 --memory 2048 --swap 512 \
--rootfs local-lvm:16 \
--unprivileged 1 --features nesting=1,keyctl=1 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--onboot 1 --password --start 1

Then install Docker inside it exactly as in Task 1. See the Proxmox LXC docs on the feature flags.

Expect maintenance

Docker-in-LXC can break after a Proxmox host kernel upgrade — storage driver mismatches are the usual culprit. Keep your compose files and volumes backed up so the container is disposable.


Task 3: Add Portainer

1Run Portainer as a container3 min

Portainer gives you a web UI over your Docker host. Deploy it with a persistent volume:

Deploy Portainer CE

docker volume create portainer_data
docker run -d \
--name portainer \
--restart=always \
-p 9443:9443 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer_data:/data \
portainer/portainer-ce:latest

Open https://<docker-host-ip>:9443, create the admin account within a few minutes of first launch (a security timeout), and you’re in. From here you can deploy stacks (compose files) straight from the UI.

Nicer URLs

Rather than :9443 everywhere, put Portainer and your apps behind a reverse proxy with names from Pi-hole local DNS — e.g. https://portainer.homelab.lan.


What’s next

You’ve now got a stable Docker host that will survive Proxmox upgrades, and a UI to manage it without memorizing command-line flags. Two follow-ups are worth doing while the momentum is there: make your services reachable at clean HTTPS URLs with a reverse proxy, and back up the VM itself — your compose files and volumes all live inside it, which means one VM backup protects your entire Docker world.


Related posts:

Sources: Docker documentation, Portainer documentation, Proxmox VE Linux Container documentation.

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.