---
title: "Cloud-init on Proxmox: auto-install packages on boot · NexoVirt"
description: "How cloud-init configures a Proxmox VM on first boot: users, SSH keys, network and packages, plus copy-paste user-data snippets via cicustom and vendor-data."
url: https://nexovirt.com/blog/cloud-init-on-proxmox
---

# Cloud-init on Proxmox: auto-install packages on boot

- By NexoVirt Team
- June 26, 2026
- Updated October 14, 2026
- 9 min read

Short answer

Proxmox VE attaches cloud-init settings to a VM as a small config drive. For a login user, SSH key and IP, set the VM's cloud-init options (`qm set <vmid> --ciuser --sshkeys --ipconfig0`). To auto-install packages and run commands on first boot, write a `#cloud-config` file to a storage with the `snippets` content type and attach it:

```
qm set 9000 --cicustom "user=local:snippets/web-01.yaml"
```

Start from a distribution's cloud image, not an installer ISO: only cloud images ship with cloud-init.

A fresh cloud image, Ubuntu, Debian, Rocky, boots up generic: no users, no SSH keys, no hostname, a DHCP NIC and nothing installed beyond the base system. Cloud-init is the mechanism that turns that generic image into *your* server on the very first boot: it creates the login user, drops in your SSH key, sets the network, installs packages and runs commands, all before you ever log in. This post explains how cloud-init works, how Proxmox VE wires it up, and gives real, copy-paste examples that install packages and run commands.

## What cloud-init actually is

Cloud-init is the de-facto standard for early initialisation of cloud instances. It ships inside virtually every official cloud image. On first boot it looks for a **data source**, a small configuration payload attached to the VM, reads it, and applies it in stages while the system comes up. Crucially, the heavy lifting runs **once**: cloud-init records that it has run (under `/var/lib/cloud/`) and skips the one-time modules on subsequent boots.

The payload is split into a few well-known documents:

- **meta-data**: identity: instance ID, hostname.
- **user-data**: the main config you write: users, packages, commands, files. Usually a `#cloud-config` YAML document.
- **network-config**: interfaces, addresses, gateways, DNS.
- **vendor-data**: config supplied by the platform/host (Proxmox), merged with your user-data.

## How Proxmox delivers cloud-init

Proxmox VE implements the **NoCloud** data source: it builds a tiny config drive (an extra CD-ROM disk) and attaches it to the VM. Cloud-init inside the guest finds that drive and applies it. You don't assemble the drive by hand. Proxmox generates it from the VM's options.

The typical workflow is: download a cloud image, import it as the VM's disk, add a cloud-init drive, set the cloud-init fields, then boot. With `qm` on the host it looks like this:

```
# 1. Create a VM shell
qm create 9000 --name ubuntu-cloud --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0

# 2. Import the downloaded cloud image as the VM's disk
qm set 9000 --scsi0 local-lvm:0,import-from=/var/lib/vz/template/iso/noble-server-cloudimg-amd64.img
qm set 9000 --scsihw virtio-scsi-pci --boot order=scsi0

# 3. Add the cloud-init drive
qm set 9000 --ide2 local-lvm:cloudinit

# 4. Set the cloud-init fields
qm set 9000 --ciuser admin --cipassword 'changeme' \
    --sshkeys ~/.ssh/id_ed25519.pub \
    --ipconfig0 ip=dhcp

# 5. (optional) grow the disk and boot
qm resize 9000 scsi0 +18G
qm start 9000
```

Those `--ciuser`, `--sshkeys` and `--ipconfig0` options become cloud-init user-data and network-config behind the scenes. That covers the basics: a login user, a key and an IP. The interesting part is going beyond that: installing software and running setup.

## How do you pass your own user-data to a Proxmox VM?

The `--ciuser` and `--sshkeys` options cover the basics, but Proxmox has no field for packages or commands. For those, write the `#cloud-config` document yourself and attach it as a **snippet**. Snippets live on a storage that has the `snippets` content type enabled; on a default install, add it to `local` (keep the content types it already has):

```
# allow snippets on the "local" storage (directory: /var/lib/vz/snippets)
pvesm set local --content iso,vztmpl,backup,snippets

# save the cloud-config there, then attach it as the VM's user-data
nano /var/lib/vz/snippets/web-01.yaml
qm set 9000 --cicustom "user=local:snippets/web-01.yaml"

# check what cloud-init will see
qm cloudinit dump 9000 user
```

A custom `user=` snippet **replaces** the user-data Proxmox would generate, so the `--ciuser`, `--cipassword` and `--sshkeys` values no longer apply: put the user and key into your snippet (see the complete example below). Network settings from `--ipconfig0` still apply unless you also pass `network=`. The examples in the next sections all go into such a snippet file.

## Installing packages on first boot

Package installation lives in user-data, in a `#cloud-config` document. The relevant keys are `package_update`, `package_upgrade` and `packages`:

```
#cloud-config
package_update: true
package_upgrade: true
packages:
  - nginx
  - git
  - htop
  - qemu-guest-agent
```

On first boot cloud-init refreshes the package index, upgrades existing packages, then installs the listed ones using the distro's native package manager (apt on Debian/Ubuntu, dnf on Rocky/Alma/Fedora). You don't specify which; cloud-init detects it. Installing `qemu-guest-agent` here is a common trick: it lets Proxmox read the VM's IP and do clean shutdowns.

## Running commands: runcmd and bootcmd

For anything package lists can't express, use `runcmd` (runs late in boot, once) or `bootcmd` (runs very early, on every boot). `runcmd` is what you want for setup steps:

```
#cloud-config
runcmd:
  - systemctl enable --now nginx
  - mkdir -p /var/www/app
  - [ sh, -c, "echo 'deploy ok' > /var/www/html/index.html" ]
```

Each item is either a string (run via the shell) or a list (run directly, no shell, safer for arguments with spaces or quotes). Commands run as root.

## Writing files

`write_files` drops configuration onto disk before `runcmd` runs, so you can write a config and then reload the service that uses it:

```
#cloud-config
write_files:
  - path: /etc/nginx/sites-available/app.conf
    permissions: '0644'
    content: |
      server {
        listen 80;
        root /var/www/app;
        index index.html;
      }
runcmd:
  - ln -sf /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.conf
  - nginx -t && systemctl reload nginx
```

## A complete, realistic example

Putting it together: a VM that comes up as a hardened web host with a deploy user, a key, the guest agent, nginx installed and configured, and root SSH login disabled:

```
#cloud-config
hostname: web-01
manage_etc_hosts: true

users:
  - name: deploy
    groups: [sudo]
    sudo: ALL=(ALL) NOPASSWD:ALL
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... you@laptop

ssh_pwauth: false
disable_root: true

package_update: true
packages:
  - nginx
  - qemu-guest-agent

write_files:
  - path: /var/www/html/index.html
    content: "<h1>web-01 is live</h1>\n"

runcmd:
  - systemctl enable --now qemu-guest-agent
  - systemctl enable --now nginx
```

## The vendor-data angle (and Proxmox snippets)

There are two places config can come from: user-data (per-VM, what you write) and vendor-data (supplied by the platform). Proxmox lets you attach a custom snippet as vendor-data via the `cicustom` option, pointing at a file on a snippet-enabled storage:

```
qm set 9000 --cicustom "vendor=local:snippets/vendor.yaml"
```

This is how you apply a baseline to *every* VM (install the guest agent, set a timezone, add a monitoring agent) without repeating it in each VM's user-data. NexoVirt uses exactly this mechanism: it delivers a `qemu-guest-agent` vendor snippet so provisioned VMs report their IP and shut down cleanly, while leaving your per-VM cloud-init credentials intact.

## Gotchas worth knowing

- **It runs once.** Editing the cloud-init fields after a VM has already booted won't re-apply them. Cloud-init has marked itself done. To force a clean re-run you must clean its state (`cloud-init clean`) or, more reliably, reinstall the VM.
- **YAML is strict.** A single bad indent silently disables a whole section. Keep two-space indentation and validate before you ship a snippet.
- **The image must include cloud-init.** Official "cloud" images do; a plain ISO install does not. Always start from the distro's cloud image.
- **Passwords vs keys.** Prefer SSH keys; set `ssh_pwauth: false` to disable password login once a key is in place.

## How NexoVirt handles it

Doing this by hand for every VM: download the right cloud image, import it, attach a cloud-init drive, write user-data, deliver a vendor snippet, resize and boot, is a lot of repeated, fragile steps. NexoVirt wraps the whole flow: you pick an OS template and a plan, set a password or paste an SSH key, and the panel provisions the VM from the cloud image with cloud-init configured, delivers the guest-agent snippet, resizes the disk and starts it. The [OS template catalog](https://nexovirt.com/os-templates) is the list of images it can deploy this way. You get cloud-init's automation without hand-assembling any of it.

Do this in NexoVirt

Pick an OS template and a plan; NexoVirt builds the VM from the cloud image with cloud-init set up. Free on up to 2 nodes. [Browse the OS templates](https://nexovirt.com/os-templates)

[Install free](https://docs.nexovirt.com/getting-started/installation/)

## Keep reading

- [ Proxmox VPS hosting panel Provision cloud-init VMs from plans and templates. ](https://nexovirt.com/proxmox-vps-panel)
- [ Proxmox alerts in Discord, Google Chat and email ](https://nexovirt.com/blog/proxmox-discord-alerts)
- [ Proxmox snapshot vs backup: when to use which (vzdump & PBS) ](https://nexovirt.com/blog/proxmox-snapshots-and-backups)

## Start running NexoVirt today

Free for homelabs with unlimited VMs on up to 2 nodes. Running a hosting business? Upgrade in place when you grow, no migration.

[Install free](https://docs.nexovirt.com/getting-started/installation/) [See pricing](https://nexovirt.com/pricing)
