Skip to content

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:

wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

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:

qm set 9000 --scsi0 local-lvm:0,import-from=/root/noble-server-cloudimg-amd64.img

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:

qm set 9000 --ide2 local-lvm:cloudinit

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:

qm set 9000 --boot order=scsi0

Most Ubuntu and Debian cloud images expect a serial console for their display output. Without this, the Proxmox web console shows a blank screen:

qm set 9000 --serial0 socket --vga serial0

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.

qm set 9000 --ciuser ubuntu
qm set 9000 --sshkeys ~/.ssh/id_ed25519.pub

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:

qm set 9000 --ipconfig0 ip=dhcp

Optionally configure DNS. Useful when DNS is not provided by DHCP:

qm set 9000 --nameserver 192.168.10.1
qm set 9000 --searchdomain lab.local

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:

qm set 9000 --ciupgrade 0

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:

qm template 9000

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):

qm clone 9000 101 --name webserver-01 --full

Set the network configuration for this specific VM before starting it:

qm set 101 --ipconfig0 ip=192.168.10.101/24,gw=192.168.10.1

Override user or SSH key if needed (otherwise the template defaults apply):

qm set 101 --ciuser ubuntu
qm set 101 --sshkeys ~/.ssh/id_ed25519.pub

Start the VM:

qm start 101

Cloud-Init runs during the first boot — typically 20–40 seconds. Connect via SSH once it completes:

ssh ubuntu@192.168.10.101

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:

qm set 101 --ipconfig0 ip=192.168.10.101/24,gw=192.168.10.1,ip6=2001:db8::101/64,gw6=2001:db8::1

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:

qm cloudinit dump 101 user
qm cloudinit dump 101 network
qm cloudinit dump 101 meta

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:

nano /var/lib/vz/snippets/base-config.yaml
#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:

qm set 101 --cicustom "vendor=local:snippets/base-config.yaml"

Every VM cloned from the template and configured with this snippet will, on first boot:

  1. Configure apt to route HTTP requests through the caching proxy
  2. Install qemu-guest-agent (required for proper Proxmox integration), curl, htop, and unattended-upgrades
  3. 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:

qm set 101 --cicustom "user=local:snippets/full-user.yaml"

Disk Resize

Cloud images typically ship with a small root partition (around 3–10 GB). Resize it before starting the VM:

qm resize 101 scsi0 +20G

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