{"url":"https://nexovirt.com/blog/cloud-init-on-proxmox","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.","markdown_url":"https://nexovirt.com/blog/cloud-init-on-proxmox.md","sections":[{"heading":"Cloud-init on Proxmox: auto-install packages on boot","content":"- By NexoVirt Team\n- June 26, 2026\n- Updated October 14, 2026\n- 9 min read\n\nShort answer\n\nProxmox 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:\n\n```\nqm set 9000 --cicustom \"user=local:snippets/web-01.yaml\"\n```\n\nStart from a distribution's cloud image, not an installer ISO: only cloud images ship with cloud-init.\n\nA 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."},{"heading":"What cloud-init actually is","content":"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.\n\nThe payload is split into a few well-known documents:\n\n- **meta-data**: identity: instance ID, hostname.\n- **user-data**: the main config you write: users, packages, commands, files. Usually a `#cloud-config` YAML document.\n- **network-config**: interfaces, addresses, gateways, DNS.\n- **vendor-data**: config supplied by the platform/host (Proxmox), merged with your user-data."},{"heading":"How Proxmox delivers cloud-init","content":"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.\n\nThe 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:\n\n```"},{"heading":"1. Create a VM shell","content":"qm create 9000 --name ubuntu-cloud --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0"},{"heading":"2. Import the downloaded cloud image as the VM's disk","content":"qm set 9000 --scsi0 local-lvm:0,import-from=/var/lib/vz/template/iso/noble-server-cloudimg-amd64.img\nqm set 9000 --scsihw virtio-scsi-pci --boot order=scsi0"},{"heading":"3. Add the cloud-init drive","content":"qm set 9000 --ide2 local-lvm:cloudinit"},{"heading":"4. Set the cloud-init fields","content":"qm set 9000 --ciuser admin --cipassword 'changeme' \\\n    --sshkeys ~/.ssh/id_ed25519.pub \\\n    --ipconfig0 ip=dhcp"},{"heading":"5. (optional) grow the disk and boot","content":"qm resize 9000 scsi0 +18G\nqm start 9000\n```\n\nThose `--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."},{"heading":"How do you pass your own user-data to a Proxmox VM?","content":"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):\n\n```"},{"heading":"allow snippets on the \"local\" storage (directory: /var/lib/vz/snippets)","content":"pvesm set local --content iso,vztmpl,backup,snippets"},{"heading":"save the cloud-config there, then attach it as the VM's user-data","content":"nano /var/lib/vz/snippets/web-01.yaml\nqm set 9000 --cicustom \"user=local:snippets/web-01.yaml\""},{"heading":"check what cloud-init will see","content":"qm cloudinit dump 9000 user\n```\n\nA 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."},{"heading":"Installing packages on first boot","content":"Package installation lives in user-data, in a `#cloud-config` document. The relevant keys are `package_update`, `package_upgrade` and `packages`:\n\n```\n#cloud-config\npackage_update: true\npackage_upgrade: true\npackages:\n  - nginx\n  - git\n  - htop\n  - qemu-guest-agent\n```\n\nOn 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."},{"heading":"Running commands: runcmd and bootcmd","content":"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:\n\n```\n#cloud-config\nruncmd:\n  - systemctl enable --now nginx\n  - mkdir -p /var/www/app\n  - [ sh, -c, \"echo 'deploy ok' > /var/www/html/index.html\" ]\n```\n\nEach 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."},{"heading":"Writing files","content":"`write_files` drops configuration onto disk before `runcmd` runs, so you can write a config and then reload the service that uses it:\n\n```\n#cloud-config\nwrite_files:\n  - path: /etc/nginx/sites-available/app.conf\n    permissions: '0644'\n    content: |\n      server {\n        listen 80;\n        root /var/www/app;\n        index index.html;\n      }\nruncmd:\n  - ln -sf /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/app.conf\n  - nginx -t && systemctl reload nginx\n```"},{"heading":"A complete, realistic example","content":"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:\n\n```\n#cloud-config\nhostname: web-01\nmanage_etc_hosts: true\n\nusers:\n  - name: deploy\n    groups: [sudo]\n    sudo: ALL=(ALL) NOPASSWD:ALL\n    shell: /bin/bash\n    ssh_authorized_keys:\n      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... you@laptop\n\nssh_pwauth: false\ndisable_root: true\n\npackage_update: true\npackages:\n  - nginx\n  - qemu-guest-agent\n\nwrite_files:\n  - path: /var/www/html/index.html\n    content: \"<h1>web-01 is live</h1>\\n\"\n\nruncmd:\n  - systemctl enable --now qemu-guest-agent\n  - systemctl enable --now nginx\n```"},{"heading":"The vendor-data angle (and Proxmox snippets)","content":"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:\n\n```\nqm set 9000 --cicustom \"vendor=local:snippets/vendor.yaml\"\n```\n\nThis 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."},{"heading":"Gotchas worth knowing","content":"- **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.\n- **YAML is strict.** A single bad indent silently disables a whole section. Keep two-space indentation and validate before you ship a snippet.\n- **The image must include cloud-init.** Official \"cloud\" images do; a plain ISO install does not. Always start from the distro's cloud image.\n- **Passwords vs keys.** Prefer SSH keys; set `ssh_pwauth: false` to disable password login once a key is in place."},{"heading":"How NexoVirt handles it","content":"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.\n\nDo this in NexoVirt\n\nPick 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)\n\n[Install free](https://docs.nexovirt.com/getting-started/installation/)"},{"heading":"Keep reading","content":"- [ Proxmox VPS hosting panel Provision cloud-init VMs from plans and templates. ](https://nexovirt.com/proxmox-vps-panel)\n- [ Proxmox alerts in Discord, Google Chat and email ](https://nexovirt.com/blog/proxmox-discord-alerts)\n- [ Proxmox snapshot vs backup: when to use which (vzdump & PBS) ](https://nexovirt.com/blog/proxmox-snapshots-and-backups)"},{"heading":"Start running NexoVirt today","content":"Free for homelabs with unlimited VMs on up to 2 nodes. Running a hosting business? Upgrade in place when you grow, no migration.\n\n[Install free](https://docs.nexovirt.com/getting-started/installation/) [See pricing](https://nexovirt.com/pricing)"}],"faq":[]}