Every time I spin up a new LXC on Proxmox, I ask myself the same question: why can’t I just hand it a cloud-init file like I do with VMs? The short answer is that cloud-init is a VM-only feature in Proxmox. LXC containers don’t support it natively. But there are several approaches that get you to the same place — or better.
This post covers the options, the trade-offs, and what I actually use for a fleet of Ubuntu + Docker + Docker Compose + Dockge containers.
Why cloud-init doesn’t apply to LXC
Cloud-init works by injecting a config drive into a VM at boot time. The VM kernel sees it as a block device and the cloud-init service reads it before anything else starts. LXC containers don’t have their own kernel — they share the host’s — so there is no boot sequence of that kind and nowhere to inject a drive. Proxmox itself never implemented a substitute.
The alternatives
1. Hook scripts (native Proxmox)
Proxmox supports hook scripts on any container, triggered at four lifecycle points: pre-start, post-start, pre-stop, post-stop. The script runs on the host, not inside the container, but you can combine it with pct exec to run commands inside:
|
1 |
pct set 200 --hookscript local:snippets/provision.sh |
A post-start hook that calls pct exec 200 -- bash /provision/setup.sh is the closest native equivalent to a cloud-init user-data script. The downside is that the script runs on every start, not just the first one, so you need to add your own “already provisioned” guard.
2. pct exec provisioning
You can drive the whole setup from the Proxmox host over pct exec without SSH:
|
1 2 3 4 5 6 |
pct create 201 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst \ --hostname docker-01 \ --cores 2 --memory 2048 \ --net0 name=eth0,bridge=vmbr0,ip=192.168.1.10/24,gw=192.168.1.1 pct start 201 pct exec 201 -- bash -c "apt-get update && apt-get install -y docker.io docker-compose-plugin" |
This works well for a one-off but doesn’t scale. Running the same ten commands on twenty containers by hand defeats the point.
3. distrobuilder
distrobuilder is Canonical’s tool for building LXC/LXD images from a declarative YAML manifest. It is the closest 1:1 analogy to cloud-init for LXC: you describe the OS, the packages, the files and the scripts, and the output is a .tar.zst template ready to import into Proxmox.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
image: distribution: ubuntu release: jammy packages: manager: apt install: - ca-certificates - curl - docker.io - docker-compose-plugin actions: - trigger: post-packages action: |- #!/bin/sh systemctl enable docker # install Dockge mkdir -p /opt/stacks /opt/dockge curl -o /opt/dockge/compose.yaml https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml |
The result is a fully baked template: every container cloned from it already has Docker, Docker Compose and Dockge installed. No provisioning step at start time.
4. Golden template + clone
The manual version of distrobuilder: build one LXC by hand (or with a script), install everything you need, then convert it to a template with pct template. Cloning is near-instant and the clone is ready to start.
|
1 2 3 4 5 6 7 |
# Clone template 9000 into ten new containers for i in $(seq 1 10); do ID=$((200 + i)) pct clone 9000 $ID --hostname docker-$(printf "%02d" $i) --full pct set $ID --net0 name=eth0,bridge=vmbr0,ip=192.168.1.$((10+i))/24,gw=192.168.1.1 pct start $ID done |
Simple and fast, but updating the template means rebuilding it by hand and re-cloning. Fine for a stable base image; painful if the “template” keeps changing.
5. Terraform with bpg/proxmox
The bpg/proxmox provider (more feature-complete than the older Telmate one) supports LXC with full resource declaration:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
resource "proxmox_virtual_environment_container" "docker_host" { node_name = "pve" initialization { hostname = "docker-01" ip_config { ipv4 { address = "192.168.1.10/24" gateway = "192.168.1.1" } } user_account { keys = [var.ssh_public_key] } } operating_system { template_file_id = "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst" type = "ubuntu" } cpu { cores = 2 } memory { dedicated = 2048 } } |
After creation you add a null_resource with a remote-exec provisioner to run your setup script over SSH. Or you point it at a pre-built template instead of a raw rootfs, and the provisioner only needs to handle the per-container config (hostname, IP, Dockge stack paths).
6. Ansible
The community.general.proxmox module creates containers, and Ansible’s community.general.pct connection plugin lets you run roles against them over pct exec without SSH. If you’re already in the Ansible world this fits naturally. If you’re not, it’s a lot of scaffolding for what is essentially a parameterised shell script.
What I actually use
For a fleet of identical Docker/Dockge containers, the combination that makes the most sense is:
- distrobuilder (or a manually built golden template) for the base image: Ubuntu, Docker, Docker Compose, Dockge, SSH key, sensible defaults. Build it once. Update it when the base changes.
- Terraform with bpg/proxmox for the per-container config: clone the template, set the hostname, IP and resources, done. The whole fleet is a
terraform apply.
That’s as close as you get to the cloud-init experience on LXC: a declarative image definition and a declarative infrastructure layer on top of it.
Summary
| Approach | Cloud-init analogy | Best for |
|---|---|---|
| Hook scripts | Post-boot triggers | Simple per-container tasks |
pct exec provisioning |
User-data scripts | One-off automation |
| Golden template + clone | Pre-baked image | Stable base, manual process |
| distrobuilder | Image baking (declarative) | Reproducible base templates |
| Terraform bpg/proxmox | IaC declarative | Fleet management |
| Ansible over pct exec | Configuration management | Existing Ansible shops |
One thing that is explicitly not on the list: the Proxmox helper scripts. They work and they’re convenient for a single install, but they’re not reproducible and not version-controlled. A fleet of twenty containers built from helper scripts is twenty containers you can’t redeploy consistently.