Přeskočit na hlavní obsah

Building a Cloud-Init Debian Template on Proxmox

·4 min

Installing a VM from an ISO through the Proxmox console is slow: boot the installer, click through partitioning, wait for package installation, reboot, then configure networking and users by hand. A cloud-init template skips all of that. You import a pre-built cloud image once, turn it into a Proxmox template, and every clone afterwards boots in seconds with networking, users and SSH keys already in place — set entirely through cloud-init on first boot.

Building a cloud-init template on Proxmox

Why this is faster than a normal install #

A cloud image ships as a minimal, already-installed root filesystem with cloud-init baked in. Proxmox just needs to import that disk image, attach a small cloud-init drive, and boot it — there’s no installer to run at all. The IP address is picked up automatically over DHCP (in my homelab that’s handled by dnsmasq, which ships as part of OPNsense, itself running as a VM on the same host), so there’s nothing to configure on the network side either.

1. Download the image #

cd /var/lib/vz/images/
wget https://cloud.debian.org/images/cloud/trixie/latest/debian-13-generic-amd64.qcow2

This must be the “generic” variant. Debian (and most other distros) also publish a “nocloud” image — that build has no cloud-init package installed at all, so the cloud-init drive you attach later is simply ignored and the VM boots with no network config, no user and no SSH key.

2. Create the VM shell #

qm create 9000 --name debian-tpl --memory 4096 --cores 8 \
  --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci --ostype l26 --agent 1

Use vmbr0 unless your setup has a reason to use a different bridge — it’s the default bridge Proxmox creates on install.

3. Import and attach the disk #

qm importdisk 9000 debian-13-generic-amd64.qcow2 local-lvm
qm set 9000 --scsi0 local-lvm:vm-9000-disk-0,discard=on,ssd=1
qm resize 9000 scsi0 30G

discard=on,ssd=1 matters beyond this template — see the note on reclaiming space below.

4. Add the cloud-init drive and set boot options #

qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --serial0 socket --vga serial0

The serial console occasionally doesn’t come up cleanly on the very first boot — if the console looks blank or stuck after cloning a VM from this template, a second restart usually clears it.

5. Configure cloud-init #

Proxmox’s built-in cloud-init fields work fine for a quick test:

qm set 9000 --ciuser myuser --sshkeys /var/lib/vz/snippets/myuser.pub \
  --cipassword mypassword --ipconfig0 ip=dhcp

For anything beyond a one-off VM, use a custom snippet instead — it lets you set up multiple users, sudoers, swap and startup commands in one place:

qm set 9000 --cicustom "user=local:snippets/debian-common.yaml"

Important: once --cicustom is set, Proxmox’s own --ciuser, --cipassword and --sshkeys fields are silently ignored — the custom user-data snippet takes over completely. Don’t set both and expect them to merge.

A second gotcha with the same root cause: qm cloudinit dump 9000 user (the command normally used to preview what will actually be applied) only shows the default Proxmox-generated cloud-init config, not the contents of your custom snippet. If you’re using --cicustom, that command is not a reliable way to check what will run — read the snippet file itself instead.

Example snippet (/var/lib/vz/snippets/debian-common.yaml), with all identifying details replaced by placeholders:

#cloud-config
manage_etc_hosts: true

users:
  - name: myuser
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: sudo
    shell: /bin/bash
    lock_passwd: false
    passwd: '<SHA-512 hash — generate with mkpasswd, see below>'
    ssh_authorized_keys:
      - '<your public key here>'

chpasswd:
  expire: false

swap:
  filename: /swapfile
  size: 10G

runcmd:
  - systemctl enable --now fstrim.timer

systemctl enable --now fstrim.timer matters together with discard=on,ssd=1 from step 3 — trimming inside the guest OS is what actually returns freed blocks to Proxmox’s thin LVM pool. Without both sides in place (guest-side TRIM and discard on the virtual disk), deleted data on the VM never shrinks the space used on the host.

To generate the password hash for the passwd field:

mkpasswd -m sha-512

(mkpasswd is part of the whois package on Debian/Ubuntu if it’s not already installed.)

6. Convert to a template #

qm template 9000

A templated VM can no longer be started directly — it only exists to be cloned.

Cloning and growing the disk #

qm clone 9000 101 --name workstation-example --full
qm start 101

Cloning a template and growing the disk

If a clone needs more disk than the template’s 30G, grow it after cloning, before or after the first boot:

qm resize 101 scsi0 +20G

This only grows the underlying LVM block device. As long as the growpart and resizefs cloud-init modules are enabled (they are by default, unless explicitly disabled in a custom snippet), cloud-init grows the partition and filesystem to match automatically on the next boot — no manual growpart/resize2fs needed.

Summary #

  • Always use the generic, not nocloud, cloud image variant.
  • vmbr0 as the default bridge, unless your network setup says otherwise.
  • The serial console can need a second restart after cloning before it displays anything.
  • --cicustom silences --ciuser/--cipassword/--sshkeys — pick one approach, not both.
  • qm cloudinit dump does not reflect a custom snippet’s contents.
  • Reclaiming space on thin LVM needs discard=on,ssd=1 on the disk and fstrim.timer enabled inside the guest.
  • Disk resize after cloning grows the block device; cloud-init’s growpart/resizefs handle the rest automatically on next boot.