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.
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:
- Per-project Docker hosts. Spin up a fresh container for a new service, test it for a week, destroy it if it doesn’t earn its keep. Faster than creating and tearing down VMs.
- Lightweight dev environments. A 256MB-512MB LXC running Docker is almost free in resource terms. Use one as a sandbox for compose files you’re not ready to promote.
- Single-purpose hosts. If you want Pi-hole, AdGuard Home, or a small monitoring stack isolated from your main Docker host without burning a full VM, LXC is the right granularity.
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
- Proxmox VE 8.x with at least 4GB RAM free for the test container (you’ll be cloning, so you want headroom)
- The
localstorage available for CT templates (default install) - A static IP range you can assign to containers (outside your DHCP pool)
- About 45 minutes for the one-time build
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.
| Tab | Setting | Value |
|---|---|---|
| General | Hostname | docker-template |
| General | Unprivileged container | Uncheck this |
| General | Password | (set a temporary password; you’ll change it later) |
| Template | Storage | local |
| Template | Template | debian-12-standard |
| Disks | Disk size | 16 GiB |
| CPU | Cores | 2 |
| Memory | Memory | 2048 MB |
| Memory | Swap | 512 MB |
| Network | IPv4 | static, in your management range |
| Network | Bridge | vmbr0 |
| DNS | Use 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:
- nesting: allows running containers (Docker) inside this LXC
- keyctl: required for the kernel keyring operations Docker uses
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.
| Field | Value |
|---|---|
| Target node | (your node) |
| VM ID | (next available) |
| Name | docker-jellyfin (or whatever the new host is for) |
| Mode | Full 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:
- Testing a new service. Clone the template, deploy the compose file, evaluate for a week, destroy if it’s not worth keeping. Faster than building a VM you’ll throw away.
- One-off Docker hosts. A small home automation stack, a dev environment for a single project, a sandbox for a friend or a kid who wants to learn: all good fits for an LXC clone.
- Resource-constrained hosts. If you’re running on a Beelink with 16GB of RAM and your main VM is already using 8GB, you can fit two or three Docker LXCs in the remaining 4-6GB. You couldn’t fit two more 2GB VMs in the same space.
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/.