← All Guides
intermediate

How to Create a Proxmox LXC Template for Docker

Build a reusable Proxmox LXC template preconfigured for Docker. Cuts new Docker-host setup from 30 minutes to 30 seconds, with the kernel module and apparmor settings that actually make it work.

Budget Homelab ·
proxmoxdockerlxchow-to

A Proxmox LXC container with Docker installed is one of the fastest ways to provision a new Docker host. A clone of a properly built template comes up in under five seconds, uses 200-300MB of RAM idle, and behaves like a normal Linux box from the moment it boots. The trade-off is a one-time setup that requires getting two LXC feature flags right, and most guides on the internet skip the part that actually breaks if you miss them.

This guide walks through building the template once, then cloning it whenever you need a new Docker environment.

This post contains affiliate links. If you buy through them, I earn a small commission at no extra cost to you.

Why bother with an LXC template for Docker

The default recommendation in the Proxmox VM vs LXC guide is to run Docker inside a single Ubuntu VM. That’s still the right call for your main Docker host. Full isolation, no privileged flags, behaves exactly as Docker documentation describes.

But there’s a real case for a Docker-on-LXC template alongside that VM:

The catch is that Docker inside an unprivileged LXC is a fight. Cgroup delegation, user namespace mapping, overlay filesystem permissions: you can make it work, but it’s hours of yak shaving. A privileged LXC with nesting enabled is the pragmatic compromise: weaker isolation than a VM, much better resource efficiency, and Docker works without exotic configuration.

If you haven’t installed Proxmox yet, start with the Proxmox install guide and the post-install checklist. This guide assumes a working Proxmox host with internet access.

Prerequisites

Hardware-wise, an N100 mini PC with 16GB of RAM handles a Docker VM plus three or four Docker LXCs simultaneously without strain. If you’re still picking hardware, the best mini PCs roundup covers the current options.

Step 1: Download the Debian 12 base template

In the Proxmox web UI, click your node, then click on the local storage (or whichever storage you’ve designated for CT templates). Open the CT Templates tab.

Click Templates. A search box appears. Search for debian-12-standard and click Download.

Debian 12 is the right base for a Docker LXC for three reasons. It ships a recent enough kernel-userspace combination to play nicely with Docker Engine. The default install is small (about 250MB) so clones stay efficient. And it has no surprises: no snap daemon, no cloud-init quirks, no preinstalled junk to disable.

Wait for the download to finish before continuing.

Step 2: Create the container

Click Create CT in the top-right of the Proxmox UI.

TabSettingValue
GeneralHostnamedocker-template
GeneralUnprivileged containerUncheck this
GeneralPassword(set a temporary password; you’ll change it later)
TemplateStoragelocal
TemplateTemplatedebian-12-standard
DisksDisk size16 GiB
CPUCores2
MemoryMemory2048 MB
MemorySwap512 MB
NetworkIPv4static, in your management range
NetworkBridgevmbr0
DNSUse host settings(or set your Pi-hole or Technitium IP)

Do not start the container yet. You need to enable two feature flags first, and Proxmox makes you set those before first boot to avoid weird state.

The unprivileged checkbox is the critical one. Unprivileged is the safer default for normal LXC use, but running Docker inside an unprivileged container requires deep cgroup configuration that’s not worth the time savings. Privileged is the right call here.

Step 3: Enable nesting and keyctl

In the Proxmox resource tree, select the new container and open the Options tab.

Find the row labeled Features and click Edit.

Check two boxes:

Click OK.

Without nesting, Docker will install fine but containers will fail to start with cgroup or apparmor errors. Without keyctl, certain operations (especially anything that touches the kernel keyring) will throw permission errors that are nearly impossible to diagnose if you don’t know to look for them.

These two flags are the entire reason most “Docker in LXC” guides on the internet end in tears. Set them now and avoid the rabbit hole.

Step 4: Boot, update, install Docker

Start the container from the Proxmox UI and open its console.

Log in as root with the password you set, then run a full update:

apt update && apt upgrade -y
apt install -y curl ca-certificates

Install Docker Engine using the official Docker convenience script:

curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
rm get-docker.sh

The script adds Docker’s apt repository, installs docker-ce, docker-ce-cli, containerd.io, the Compose v2 plugin, and the Buildx plugin. It also enables and starts the Docker systemd service.

Verify Docker works:

docker run --rm hello-world

You should see the standard “Hello from Docker!” message. If you see a cgroup or apparmor error, double-check that nesting and keyctl are both enabled on the container. pct config <vmid> | grep features from the Proxmox host will show the current flags.

While you’re here, install whatever else you want baked into every clone:

apt install -y htop ncdu vim git

A minimal base is fine. Don’t install service-specific configuration here. Keep the template generic so it’s useful for any future Docker workload.

Step 5: Clean the container before templating

This is the step most guides skip and the reason new clones often have weird networking, duplicate SSH host keys, or shared machine IDs.

Inside the container, run:

# Remove SSH host keys so each clone generates its own
rm -f /etc/ssh/ssh_host_*

# Clear the machine ID so each clone gets a unique one
truncate -s 0 /etc/machine-id
rm -f /var/lib/dbus/machine-id
ln -s /etc/machine-id /var/lib/dbus/machine-id

# Clear bash history and apt cache
> /root/.bash_history
apt clean
rm -rf /var/lib/apt/lists/*

# Clear logs
find /var/log -type f -exec truncate -s 0 {} \;

# Power off cleanly
poweroff

The SSH host key cleanup is the one that matters most. If you skip it, every clone will have the same SSH host fingerprint and your SSH client will start refusing connections with WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! errors that look like a man-in-the-middle attack.

Wait until the container is fully stopped in the Proxmox UI before continuing.

Step 6: Convert to a template

Right-click the container in the Proxmox resource tree. Select Convert to template.

The container icon turns gray. It can no longer be started, only cloned. This is intentional. A template is a frozen base; modifying it after this point requires either cloning, modifying the clone, and converting back, or unlocking via the CLI (which defeats the purpose).

You now have a reusable Docker base. Time from docker-template to running clone: under 30 seconds on most hardware.

Cloning the template to a new Docker host

Right-click the template in the resource tree and choose Clone.

FieldValue
Target node(your node)
VM ID(next available)
Namedocker-jellyfin (or whatever the new host is for)
ModeFull Clone

Full Clone copies the disk; Linked Clone is a copy-on-write reference to the template. For a Docker host you’ll be writing data to constantly, Full Clone is the right choice. Linked Clones bind you to the template’s lifecycle in ways you don’t want for production data.

Start the clone, log in, and do three things before treating it as a real host:

# Regenerate SSH host keys
dpkg-reconfigure openssh-server

# Update the hostname
hostnamectl set-hostname docker-jellyfin

# Update /etc/hosts
sed -i 's/docker-template/docker-jellyfin/' /etc/hosts

If you set a static IP during clone creation, networking should already be correct. If not, edit /etc/network/interfaces and reboot.

The clone is now a fresh, unique Docker host. Deploy whatever you want with the patterns from the Docker Compose basics guide.

When to use this template vs your main Docker VM

The single-Docker-host-VM model from the VM vs LXC guide is still the right primary pattern. Everything important (your Jellyfin, Paperless-ngx, Immich, Vaultwarden, Nginx Proxy Manager) lives in a single VM where Docker has full kernel isolation and behaves like every tutorial on the internet says it should.

The LXC template is for the side cases:

What you should not use this for: anything internet-facing, anything handling sensitive data, anything you’d be upset to lose to a privileged-container escape. The threat model for a privileged LXC is lower than a VM. For a home LAN behind a NAT with no port forwards, that’s a fine trade. For anything past that boundary, use a VM.

Common gotchas

Overlay filesystem errors after install. If docker run hello-world fails with a graph driver error and your Proxmox host uses ZFS for the container’s storage, switch the container’s root disk to a directory storage instead. Overlay-on-ZFS-on-LXC has known issues; the workaround is to put the LXC’s disk on local (directory) rather than local-zfs.

Container won’t start after enabling features. Stop the container fully, run pct rescan <vmid> from the Proxmox host shell, then try again. Occasionally the feature flag changes don’t take effect until the container config is re-read.

Clones have the wrong network. Static IPs from the template clone will conflict with the template’s original IP. Set a new IP when you clone, or accept the conflict and fix it on first boot.

Apparmor profile errors. If you’ve customized apparmor on the Proxmox host, you may need to add lxc.apparmor.profile: unconfined to the container config (/etc/pve/lxc/<vmid>.conf). Default Proxmox installs don’t need this. Most home users will never see this issue.

What you’ve built

A reusable, 30-second-to-clone Docker host base. Combine with the Docker Compose basics guide for deployment patterns, the Watchtower guide for keeping clones updated, and the Portainer guide if you want a web UI across all of them.

If you’re planning to back up your new Docker hosts, the Proxmox snapshots and backups guide covers the LXC-specific quirks. Container backups are a different code path than VM backups and have their own gotchas around live state.

See the full homelab stack at /stack/.