Proxmox — Cloud-Init VM Templates
Deploying a new VM normally means installing an OS, setting up a user, configuring the network, and copying SSH keys — every time. Cloud-Init eliminates that repetition. You configure a VM template once, and every clone you create from it gets its hostname, user, SSH keys, and network settings applied automatically on first boot.
Cloud-Init is the industry-standard tool for this across cloud providers and hypervisors. Proxmox has native support for it since PVE 5.2.
How Proxmox Implements Cloud-Init
Proxmox does not run Cloud-Init itself — it acts as a data source provider. When you set Cloud-Init parameters (user, SSH key, IP address) in the Proxmox UI or CLI, Proxmox generates a small ISO image containing that configuration data and attaches it to the VM as a virtual CD-ROM drive.
When the VM boots, the Cloud-Init agent inside the OS reads the ISO, applies the configuration, and removes the drive. The process happens once on first boot. After that, the VM is fully provisioned and the Cloud-Init drive can be removed.
This means:
- No Cloud-Init packages are needed on the Proxmox host — only inside the VM image
- Changes you make in Proxmox are written into the ISO; they apply on the next first-boot cycle
- Works with any Linux distribution that ships with Cloud-Init pre-installed (Ubuntu, Debian, Rocky, Fedora Cloud images all do)
Prerequisites
- Proxmox VE 7.x or later (6.x works but some options differ)
- A storage pool that supports disk images —
local-lvm,local-zfs, or a Ceph pool - Shell access to the Proxmox node (SSH or the web console)
- For custom snippets: a storage with the Snippets content type enabled (see Custom User-Data)
Step 1 — Download a Cloud Image
Cloud images are pre-built OS disk images with Cloud-Init already installed and configured. Ubuntu publishes them at https://cloud-images.ubuntu.com/. Download the Ubuntu 24.04 LTS (Noble) image directly onto the Proxmox node:
Verify the download against the published checksums:
wget https://cloud-images.ubuntu.com/noble/current/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing
The output should confirm noble-server-cloudimg-amd64.img: OK.
Tip
Ubuntu cloud images are updated roughly monthly. Re-download before creating a new template to keep the base image current. This reduces the number of packages that need updating at first boot.
Step 2 — Create the Template VM
Use a high VM ID for templates (e.g. 9000–9099) to keep them visually separated from your regular VMs. The VM created here will never be started directly — it becomes the template you clone from.
qm create 9000 \
--name ubuntu-2404-cloud \
--memory 2048 \
--cores 2 \
--net0 virtio,bridge=vmbr0 \
--scsihw virtio-scsi-pci
Import the downloaded image as the VM's boot disk. Replace local-lvm with the name of your storage pool:
Proxmox imports the image, resizes the disk entry, and registers it as scsi0. The .img file is no longer needed after this step.
Step 3 — Attach the Cloud-Init Drive
The Cloud-Init drive is the virtual CD-ROM that Proxmox writes your configuration to. Attach it before configuring anything else:
This reserves space on local-lvm for Proxmox to store the generated ISO. The drive shows up as ide2 in the VM configuration.
Step 4 — Configure Boot Order and Display
Restrict the boot order to the OS disk only — the Cloud-Init CD-ROM should not be a boot device:
Most Ubuntu and Debian cloud images expect a serial console for their display output. Without this, the Proxmox web console shows a blank screen:
Step 5 — Configure Cloud-Init Parameters
Cloud-Init parameters can be set in the Proxmox web UI or via the CLI. The CLI approach is faster for scripting and reproducibility.
Via CLI
Set the default login user and attach your SSH public key. Password login is strongly discouraged — use SSH keys.
Set the network configuration for the template. Clones will override this per-VM, but setting a default here ensures the template itself is in a defined state:
Optionally configure DNS. Useful when DNS is not provided by DHCP:
By default, Cloud-Init runs apt upgrade on first boot (--ciupgrade 1). Disable it if you want faster first boots and prefer to manage upgrades yourself:
Via Web UI
Navigate to the VM → Cloud-Init tab. The same parameters are available as form fields. Click Regenerate Image after any change to update the ISO immediately.
Step 6 — Convert to Template
Once the VM is configured, lock it down as a template. After this command, the VM can no longer be started directly — only cloned:
The VM entry in the web UI shows a template icon. The configuration is preserved; you can still inspect and adjust Cloud-Init settings.
Step 7 — Clone and Deploy VMs
Each new VM is a clone of the template. Use --full to create an independent disk copy (a linked clone shares the template disk and cannot be safely moved):
Set the network configuration for this specific VM before starting it:
Override user or SSH key if needed (otherwise the template defaults apply):
Start the VM:
Cloud-Init runs during the first boot — typically 20–40 seconds. Connect via SSH once it completes:
Multiple Network Interfaces
A VM with more than one network interface uses --ipconfig0, --ipconfig1, etc., matching the NIC index:
qm set 101 --net0 virtio,bridge=vmbr0
qm set 101 --net1 virtio,bridge=vmbr1
qm set 101 --ipconfig0 ip=192.168.10.101/24,gw=192.168.10.1
qm set 101 --ipconfig1 ip=10.10.0.101/24
IPv6 is supported alongside IPv4:
Cloud-Init Parameters Reference
| What it configures | CLI flag | Example value |
|---|---|---|
| Login username | --ciuser |
ubuntu |
| Password (not recommended) | --cipassword |
changeme |
| SSH public key(s) | --sshkeys |
~/.ssh/id_ed25519.pub |
| Network — DHCP | --ipconfig0 |
ip=dhcp |
| Network — static IPv4 | --ipconfig0 |
ip=192.168.10.100/24,gw=192.168.10.1 |
| Network — static IPv6 | --ipconfig0 |
ip6=2001:db8::100/64,gw6=2001:db8::1 |
| DNS server | --nameserver |
192.168.10.1 |
| DNS search domain | --searchdomain |
lab.local |
| Skip apt upgrade on boot | --ciupgrade |
0 |
| Custom config file | --cicustom |
vendor=local:snippets/base.yaml |
| Config format | --citype |
nocloud (Linux default) |
Inspecting the Generated Configuration
Proxmox generates three files from your settings. You can read them without booting the VM:
The user output shows the username, SSH keys, and any upgrade settings. The network output shows the IP configuration translated into the Cloud-Init network v2 format. Use this to verify the configuration before starting a VM.
Custom User-Data (Snippets)
The parameters exposed in the Proxmox UI cover the basics. For anything beyond that — installing packages, running commands, writing config files — use a custom user-data YAML (a Cloud-Init user-data file).
Enable Snippet Storage
Snippets must be stored on a storage with the Snippets content type enabled.
In the Proxmox web UI: Datacenter → Storage → select storage → Edit → tick Snippets. For the built-in local storage, this activates the directory /var/lib/vz/snippets/.
user= vs vendor=
The --cicustom flag supports four keys: user=, vendor=, network=, and meta=.
user=local:snippets/file.yaml— your file replaces the Proxmox-generated user-data entirely. The username and SSH key from the Cloud-Init tab are not applied. You must define them in the YAML.vendor=local:snippets/file.yaml— your file is applied alongside the Proxmox-generated user-data. Username and SSH key from the tab are still applied. Use this for additions.
For most use cases, vendor= is the right choice — it lets you add behaviour without rewriting the Proxmox-managed defaults.
Example: apt proxy and base packages
This snippet configures all new VMs to use an Apt-Cacher NG proxy on the LAN and installs a baseline set of packages.
Create the file on the Proxmox node:
#cloud-config
apt:
http_proxy: "http://192.168.10.5:3142"
https_proxy: "DIRECT"
packages:
- qemu-guest-agent
- curl
- htop
- unattended-upgrades
runcmd:
- systemctl enable --now qemu-guest-agent
Apply it to a VM as vendor-data:
Every VM cloned from the template and configured with this snippet will, on first boot:
- Configure apt to route HTTP requests through the caching proxy
- Install
qemu-guest-agent(required for proper Proxmox integration),curl,htop, andunattended-upgrades - Enable and start
qemu-guest-agent
Warning
Snippet files must exist on every Proxmox node in your cluster. If you migrate a VM to a different node that does not have the snippet file, the VM will fail to regenerate its Cloud-Init ISO. Copy snippets to all nodes or use shared storage.
Example: full user-data with user definition
When you need full control and use user=, you must define the user and SSH key yourself:
#cloud-config
users:
- name: ubuntu
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAA... your-public-key-here
apt:
http_proxy: "http://192.168.10.5:3142"
https_proxy: "DIRECT"
packages:
- qemu-guest-agent
- curl
runcmd:
- systemctl enable --now qemu-guest-agent
Apply with:
Disk Resize
Cloud images typically ship with a small root partition (around 3–10 GB). Resize it before starting the VM:
This adds 20 GB to the existing disk size. The number after + is the amount to add, not the target size. Cloud-Init expands the filesystem to fill the disk automatically on first boot.
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
| Web console blank after boot | Missing serial console config | Add --serial0 socket --vga serial0 to the template |
| SSH key not applied to clone | Forgot to set --sshkeys on clone |
Run qm set <vmid> --sshkeys ~/.ssh/id_ed25519.pub before first boot |
| VM boots but network is not configured | Clone started before --ipconfig0 was set |
Stop the VM, set the IP config, run qm cloudinit update <vmid>, start again |
| Custom snippet not applied | --cicustom path wrong or snippet storage not enabled |
Check qm cloudinit dump <vmid> user to see what Proxmox generated |
| VM migration fails | Snippet file missing on target node | Copy snippet files to all nodes or use shared storage for snippets |
| Disk too small after first boot | Cloud image default size | Run qm resize <vmid> scsi0 +<size>G before starting |