Running a full virtual machine just to host a handful of Docker containers is wasteful. A dedicated Ubuntu or Debian VM for Docker typically reserves 2-4 GB of RAM and a virtual CPU allocation whether or not the containers inside it are busy, and it boots in the better part of a minute. An LXC container, by contrast, shares the Proxmox host's kernel, starts in a second or two, and can be sized down to a few hundred megabytes of RAM with no wasted overhead. For homelabs and resource-constrained clusters, running Docker inside an LXC container is one of the most effective ways to pack more workloads onto the same hardware.

It is also one of the more misunderstood setups in the Proxmox ecosystem. Docker was designed around the assumption that it owns the kernel namespaces it creates, and LXC containers deliberately restrict exactly the syscalls and filesystem features Docker wants by default. Get the container features wrong and you'll see Docker's daemon refuse to start, overlay2 storage driver errors, or containers that silently fail to launch after a host reboot.

This guide walks through a working, current configuration for Proxmox VE 9.2 (kernel 7.0, LXC 7.0), explains why each setting is required, and covers the storage-driver and backup caveats that catch people out in production.

Should You Use an LXC Container or a VM for Docker?

Proxmox's own documentation is explicit about this trade-off: nesting containers inside an LXC container works, but for workloads that demand maximum isolation or need to be live-migrated between nodes, running Docker inside a QEMU VM is the safer, officially recommended path. An LXC container sharing the host kernel means a container escape - while rare - has a larger blast radius than a VM escape, because there's no hardware-virtualization boundary in between.

In practice, most homelab and small-team deployments accept that trade-off deliberately:

  • Use an LXC container when you're running your own trusted workloads (media servers, dashboards, internal tools, CI runners), you care about density and boot speed, and you don't need live migration for that specific guest.
  • Use a VM when you're running untrusted or multi-tenant workloads, you need to live-migrate the Docker host between cluster nodes without downtime, or the workload needs full kernel-level isolation (for example, exposing it directly to the internet without a reverse proxy in front).

If an LXC container fits your use case, the rest of this guide gets you a stable setup.

Prerequisites

  • A Proxmox VE 9.x host (this guide assumes 9.2, kernel 7.0, LXC 7.0 - earlier 8.x hosts work too, with kernel 6.x)
  • A Debian 13 (Trixie) or Ubuntu 24.04 LXC template downloaded on the host
  • Root or a user with permissions to create containers and edit /etc/pve/lxc/*.conf
  • Basic familiarity with the Proxmox web UI or pct/pvesh CLI

If you don't already have a template cached, pull one first:

pveam update
pveam available --section system | grep -i debian-13
pveam download local debian-13-standard_13.5-1_amd64.tar.zst

Step 1: Create the Container

Create an unprivileged container. Unprivileged is the right default here - Docker works fine inside one once nesting and keyctl are enabled, and it keeps the container's root user mapped to an unprivileged UID on the host, which meaningfully limits what a compromised container can do to the node.

From the web UI: Create CT → choose a hostname and password → select the Debian 13 or Ubuntu 24.04 template → allocate at least 2 CPU cores and 2 GB RAM as a starting point (Docker and its containers need headroom; scale up per workload) → give it a disk of at least 8 GB, more if you'll be pulling many images.

Or from the CLI:

pct create 201 local:vztmpl/debian-13-standard_13.5-1_amd64.tar.zst \
  --hostname docker01 \
  --cores 2 \
  --memory 2048 \
  --swap 512 \
  --rootfs local-zfs:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1 \
  --features nesting=1,keyctl=1

Note the --features nesting=1,keyctl=1 flag at creation time - you can also add it after the fact, which the next step covers in more detail.

Step 2: Enable Nesting and Keyctl

These two container features are the difference between Docker starting cleanly and the daemon failing immediately.

FeatureWhat it doesWhy Docker needs it
nesting=1Exposes procfs and sysfs in a way that allows namespaces to be created inside the containerDocker creates its own network, PID, and mount namespaces for every container it runs - without nesting, those calls are blocked
keyctl=1Allows the keyctl() syscall inside unprivileged containers (blocked by default)The kernel keyring is used by systemd and containerd/runc during container startup; without it, dockerd or the containers it spawns fail to start with permission errors

If you didn't set these at creation time, stop the container and edit its config directly:

pct stop 201
echo "features: nesting=1,keyctl=1" >> /etc/pve/lxc/201.conf
pct start 201

Or through the GUI: select the container → OptionsFeaturesEdit → check Nesting and keyctl → OK, then restart the container.

Verify the config landed correctly:

cat /etc/pve/lxc/201.conf | grep features
# features: nesting=1,keyctl=1

Step 3: Handle the Storage Driver (ZFS Hosts Need fuse-overlayfs)

This is the step most guides skip, and it's the one that produces the infamous overlayfs: upper fs missing required features error.

Docker's preferred storage driver, overlay2, needs the container's root filesystem to itself be backed by a filesystem that supports overlayfs mounts cleanly from inside a nested namespace. If your Proxmox storage pool for the container is ZFS (a very common setup - local-zfs is the default on most fresh installs), the container's rootfs is a ZFS dataset, and the kernel's native overlayfs driver cannot layer on top of it correctly from inside an unprivileged nested container. Docker containers will fail to start, or images will fail to build, with overlay-related errors in journalctl -u docker.

There are two ways around this:

Option A: Use fuse-overlayfs (recommended on ZFS)

fuse-overlayfs reimplements overlayfs in userspace via FUSE, sidestepping the kernel-level incompatibility. Install it inside the container and point Docker at it:

# Inside the LXC container
apt update
apt install -y fuse-overlayfs

mkdir -p /etc/docker
cat <<'EOF' > /etc/docker/daemon.json
{
  "storage-driver": "fuse-overlayfs"
}
EOF

systemctl restart docker

fuse also needs to be enabled as an LXC feature so the FUSE device node is available inside the container:

pct stop 201
echo "features: nesting=1,keyctl=1,fuse=1,mknod=1" >> /etc/pve/lxc/201.conf
pct start 201

(Merge this with your existing features line rather than adding a duplicate one - Proxmox only reads the last features: line in the config file.)

Important: after any host kernel update, the FUSE module can fail to auto-load before the container starts, which makes Docker silently fail on boot. Pin it to load early:

# On the Proxmox host
echo "fuse" >> /etc/modules-load.d/fuse.conf
update-initramfs -u

Option B: Use LVM-thin or a directory-backed storage pool instead

If you can choose where the container's rootfs lives, placing it on an LVM-thin or plain directory/ext4 storage pool avoids the ZFS-on-ZFS overlay problem entirely, and Docker's native overlay2 driver works without any extra configuration. This is the simpler path if your host has LVM-thin storage available and you don't specifically need ZFS's snapshotting for this container.

Step 4: Install Docker Inside the Container

With nesting, keyctl, and (if needed) fuse sorted out, installing Docker itself is the standard procedure:

# Inside the LXC container
apt update
apt install -y ca-certificates curl gnupg

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Use Docker 26 or newer. Older Docker/runc combinations have known issues creating nested namespaces correctly inside unprivileged LXC containers, and the failure mode is often a hang or a cryptic permission error rather than a clear message.

Confirm the daemon is healthy:

systemctl status docker
docker run --rm hello-world

If hello-world pulls and runs, nesting, keyctl, and the storage driver are all correctly configured.

Step 5: Run a Real Workload

Docker Compose works exactly as it would on bare metal or a VM. A simple example:

mkdir -p /opt/stacks/whoami && cd /opt/stacks/whoami
cat <<'EOF' > docker-compose.yml
services:
  whoami:
    image: traefik/whoami
    ports:
      - "8080:80"
    restart: unless-stopped
EOF

docker compose up -d

Visit http://<container-ip>:8080 and you should get a response identifying the container. From here, treat the LXC container exactly like any other Docker host - Portainer, Watchtower, and standard Compose stacks all work without modification.

Troubleshooting Common Failures

"overlayfs: upper fs missing required features"

Your storage backend is ZFS (or another incompatible filesystem) and Docker is trying to use the native overlay2 driver. Switch to fuse-overlayfs as described in Step 3, or move the container to an LVM-thin/directory storage pool.

Docker daemon fails to start with a permission or keyring error

Almost always missing keyctl=1 on an unprivileged container. Check /etc/pve/lxc/<CTID>.conf and confirm the features line includes it, then restart the container (not just the Docker service - the feature only takes effect on container start).

Containers fail to start only after a host reboot

Usually the FUSE kernel module wasn't loaded before the container came up. Confirm /etc/modules-load.d/fuse.conf exists on the host and that update-initramfs -u was run, then reboot the host to test.

systemd warnings about cgroup delegation inside the container

Newer systemd versions (255+) actively check whether the environment they're running in delegates cgroup controllers correctly. Without nesting=1, systemd inside the container detects the restricted environment and refuses to start certain services, which can look like unrelated Docker failures in the logs. Re-verify nesting is enabled if you see cgroup-related warnings in journalctl.

Networking between containers on the same LXC/Docker host works, but external access doesn't

Check that you're not double-NATing - Docker's own bridge network sits behind Proxmox's bridge (vmbr0), and any Proxmox firewall rules on the container's network interface apply on top of Docker's own port mappings. Confirm the Proxmox firewall (if enabled on the container) permits the mapped ports before assuming Docker is misconfigured.

Backups and Snapshots: What to Watch For

Proxmox's own documentation flags a real limitation here: FUSE mounts inside a container can interact badly with the Linux kernel's freezer subsystem, which Proxmox uses to quiesce a container's filesystem state during snapshots and backups. If you're using fuse-overlayfs (Step 3, Option A), a vzdump backup or snapshot can occasionally hang or produce an inconsistent backup while Docker is actively writing to overlay layers.

Two practical mitigations:

  • Prefer stop-mode backups (vzdump --mode stop) for Docker-in-LXC containers rather than snapshot-mode, so the container is fully shut down (and FUSE unmounted) for the duration of the backup.
  • If you went with Option B (LVM-thin/directory storage, native overlay2, no FUSE), snapshot-mode backups are much safer, since there's no FUSE layer for the freezer to fight with.

Either way, test a restore at least once before relying on this container for anything you can't easily rebuild - that's true of any backup strategy, but it matters more here because of the extra moving parts.

Resource Limits: Keep Docker Honest

An LXC container's CPU and memory limits are enforced by the same cgroups Docker itself uses internally, which means limits nest correctly - a container capped at 2 GB RAM will not let its Docker workloads collectively exceed that, even if individual docker run commands don't specify their own --memory flags. Still, it's worth setting per-service memory limits in your Compose files so a single runaway container doesn't starve everything else on the same LXC host:

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

Monitor actual usage from the Proxmox host with pct exec <CTID> -- docker stats, and adjust the container's allocated cores/memory from the Proxmox UI as your workloads grow - both can be changed live without restarting the container, memory more reliably than CPU depending on your kernel.

Frequently Asked Questions

Does this work on Proxmox VE 8.x, or only 9.x?

The same nesting/keyctl/fuse-overlayfs approach works on Proxmox VE 8.x with LXC 5 or 6 and an older kernel. The feature flags and Docker installation steps are identical; only the underlying kernel and LXC version numbers differ.

Do I need fuse-overlayfs if my storage is LVM-thin instead of ZFS?

No. The overlay incompatibility is specific to layering Docker's overlay2 driver on top of a ZFS-backed rootfs. On LVM-thin or ext4/directory storage, Docker's native overlay2 driver works out of the box - skip Option A and just enable nesting=1,keyctl=1.

Is a privileged container ever the right call for Docker?

It's easier to set up (fewer edge cases with device access and some volume mounts), but it removes the user-namespace isolation that makes unprivileged containers safer. Reach for a privileged container only for trusted, non-networked workloads where convenience clearly outweighs the reduced isolation - and understand that a compromise of Docker or a container inside it is then effectively a compromise of the LXC container as root on the host's UID space.

Can I run Docker Swarm or Kubernetes (k3s) the same way?

k3s is commonly run in LXC containers with the same nesting/keyctl features, sometimes needing additional flags for its embedded containerd. Docker Swarm works with the same setup described here. Neither is meaningfully harder than plain Docker once nesting and keyctl are enabled - the failure modes are the same ones covered in the troubleshooting section above.

Will live migration work for a Docker-in-LXC container?

LXC containers can be live-migrated on Proxmox VE with shared storage, but nested container workloads add risk - in-flight network connections inside Docker's own overlay network are more likely to need a reconnect after migration than a VM's would. If live migration without any workload disruption is a hard requirement, that's the scenario where Proxmox's own guidance to use a VM instead applies most directly.

How This Compares to Proxmox VE's Native OCI Container Support

Recent Proxmox VE releases ship an early, technology-preview feature for running OCI-format application containers directly as a guest type, separate from the LXC and QEMU workflows covered in this guide. It's worth understanding how that differs from what you've just set up, since the two are easy to conflate.

The native OCI support is aimed at running a single container image as a lightweight guest with minimal setup - useful for a one-off service you'd otherwise wrap in a whole LXC container just to run one process. It does not give you the Docker CLI, Compose stacks, volume management, or the ecosystem of tooling (Portainer, Watchtower, custom bridge networks) that a real Docker installation provides. Because it's still a technology preview, it also lacks the maturity and community troubleshooting history that the LXC-plus-Docker approach in this guide has built up over several Proxmox VE release cycles.

For anyone running actual multi-service Compose stacks, or who wants Docker Hub images, private registries, and Compose files to just work the way they do anywhere else, the nested Docker-in-LXC setup remains the more practical choice today. Treat native OCI containers as an option worth revisiting as it matures, not a replacement for this workflow right now.

Security Hardening Checklist

Docker's own daemon runs as root by default, and that root is now nested inside your LXC container's root, which is itself mapped to an unprivileged host UID if you followed Step 1. That mapping is your primary safety net - don't undermine it. A few concrete steps worth taking before exposing the container to anything untrusted:

  • Never run the container privileged unless you have a specific, understood reason to (see the FAQ above). The unprivileged UID mapping is what limits the damage of a container escape.
  • Don't bind-mount the Proxmox host's filesystem into the LXC container or into a Docker container running inside it. If a workload needs persistent storage, use a Docker named volume or a dedicated disk attached to the LXC container instead.
  • Put a reverse proxy in front of anything internet-facing rather than mapping container ports directly with -p. This also sidesteps the double-NAT confusion mentioned in the troubleshooting section.
  • Keep Docker itself patched. apt update && apt upgrade docker-ce docker-ce-cli containerd.io inside the container on the same cadence you patch the Proxmox host, since Docker and containerd both regularly ship CVE fixes.
  • Isolate the container on its own VLAN if it's running anything you wouldn't want talking freely to your management network, using Proxmox's SDN or a simple VLAN-aware bridge.

Summary

Running Docker inside a Proxmox VE LXC container is a well-trodden path once you know the three things that trip people up: enable nesting and keyctl on the container, switch to fuse-overlayfs if your storage pool is ZFS, and prefer stop-mode backups if you're using FUSE. Get those three right and you get VM-like functionality with LXC-level density - a meaningful win for homelabs and any cluster where RAM and boot time matter.