Cloud-Init has become the de-facto standard in the Proxmox world for turning freshly cloned VMs into productive workloads without manual post-processing. The GUI conveniently handles hostname, IP, SSH keys and the initial password — but anything beyond that, such as packages, services or firewall rules, quickly ends up in hand-maintained YAML files that get copied, forgotten and ultimately drift out of sync. This is exactly where the Proxmox VE Snippets feature comes in: it lets you store arbitrary cloud-init building blocks centrally on a storage and reference them at clone time with a single config switch.
In this article we show how to enable a snippets storage, how to build a reusable web server template and how to cleanly attach it to new VMs. We use Proxmox VE 8.4 with Cloud-Init 24.x as the baseline — the concepts apply to older 8.x releases as well.
What are Proxmox Snippets exactly?
Snippets are a dedicated content type on a Proxmox storage, comparable to iso, vztmpl or backup. While ISO images cover installation and container templates serve LXC, snippets provide small text files consumed by other components — in practice mostly Cloud-Init and hook scripts. The files live in the normal filesystem under /var/lib/vz/snippets/ (for storage type dir) or on a CephFS/NFS mount, which makes them reachable from all cluster nodes provided the storage is shared.
Cloud-Init knows three classic input files that map nicely to snippets:
- user-data: packages, commands, users, SSH keys,
runcmdblocks - network-config: VLANs, bonds, static routes beyond what the GUI offers
- meta-data: instance-id, hostname, occasionally cloud-specific fields
Proxmox normally generates these files itself from the GUI inputs. With snippets you selectively override that behaviour for individual VMs while still being able to use the GUI values as a fallback.
Enabling snippets storage
For a storage to accept snippets, the snippets content type must be enabled. On a single node or small cluster the existing local storage is often fine; in larger environments a CephFS or NFS share is recommended, otherwise snippets have to be synced manually across nodes.
In the GUI open Datacenter -> Storage, pick the storage and tick “Snippets” under “Content”. On the command line it is faster — a look at the central configuration file pve-storage.cfg shows the principle:
# /etc/pve/storage.cfg
dir: local
path /var/lib/vz
content iso,vztmpl,backup,snippets
shared 0
cephfs: cephfs-snippets
path /mnt/pve/cephfs-snippets
content snippets,iso
fs-name cephfs
Or set it directly via CLI:
pvesm set local --content iso,vztmpl,backup,snippets
After enabling, create the directory and check permissions:
mkdir -p /var/lib/vz/snippets
ls -ld /var/lib/vz/snippets
The files should be owned by root and readable and writable by root — nothing more is needed, since qemu-server runs as root.
Building a reusable web server template
Now for the concrete example: a user-data that installs nginx, ships a default page and opens ports 80/443 in the local nftables firewall. We name the file webserver-user-data.yaml and place it under /var/lib/vz/snippets/:
#cloud-config
package_update: true
package_upgrade: true
packages:
- nginx
- nftables
- curl
- htop
write_files:
- path: /etc/nginx/sites-available/default
permissions: '0644'
content: |
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html;
server_name _;
location / { try_files $uri $uri/ =404; }
}
- path: /etc/nftables.conf
permissions: '0755'
content: |
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
tcp dport { 22, 80, 443 } accept
ip protocol icmp accept
}
}
runcmd:
- systemctl enable --now nginx
- systemctl enable --now nftables
- nft -f /etc/nftables.conf
- echo "Provisioned by DATAZONE on $(date)" > /var/www/html/index.html
The #cloud-config line at the very top is mandatory — without that marker, cloud-init will not recognise the file. Also make sure indentation is clean and uses spaces; tabs cause hard-to-spot parser errors.
Attaching the snippet to a VM
With the local storage and the webserver-user-data.yaml file in place, reference the snippet via qm set like this:
# Replace cloud-init user-data
qm set 9001 --cicustom "user=local:snippets/webserver-user-data.yaml"
# Regenerate the cloud-init drive
qm cloudinit update 9001
The --cicustom parameter understands three keys that you can set individually or combined:
| Key | Snippet | Typical content |
|---|---|---|
user | user-data | packages, runcmd, write_files |
network | network-config | VLAN tagging, bonds, multiple NICs |
meta | meta-data | instance-id, hostname logic |
Multiple keys are passed comma-separated:
qm set 9001 --cicustom "user=local:snippets/webserver-user-data.yaml,network=local:snippets/dmz-network.yaml"
The GUI values for SSH key, password and IP address stay active and are merged with the snippet — so you do not have to rebuild everything in YAML.
Cluster workflow: templates, clones, pipeline
In practice we combine snippets with classic VM templates. A sensible pipeline looks like this:
- Golden image as a template (Debian 12 or Ubuntu 24.04 with the cloud-init package).
- Snippet library: one snippet per role (web, db, monitoring, jumphost).
- Clone:
qm clone 9000 1234 --name web01 --full - Assign snippet:
qm set 1234 --cicustom "user=local:snippets/webserver-user-data.yaml" - Start VM:
qm start 1234— the rest happens on first boot.
If you wrap this in an Ansible or Terraform pipeline, you get a reproducible provisioning path without external config-management tooling. For pure container workloads Kubernetes is often overkill — a handful of snippets plus a clear naming convention go a long way.
Real-world pitfalls
Three issues come up over and over in customer projects:
- Snippet does not run: almost always the
#cloud-configline is missing or cloud-init was disabled in the image. Inside the guestcloud-init status --longreveals the status. - Changes to the snippet do not apply: cloud-init only runs on first boot. For testing, run
cloud-init clean --logsinside the guest and reboot, or regenerate the cloud-init drive in the GUI. - Snippet on node A, VM on node B: live migration requires the storage to be shared, otherwise the target node will not find the file. CephFS, NFS or a
cron-based sync solve this.
For version control of your snippets, a Git repository synced into /var/lib/vz/snippets/ via cron or webhook is highly recommended. That way changes can be reviewed, rolled back and documented — an important step towards Infrastructure as Code without immediately adopting the full tooling stack.
For more background on cluster architecture and storage choices see our Proxmox consulting and the overview of suitable shared storage setups in our TrueNAS section.
Conclusion
Proxmox Snippets are an unspectacular but extremely useful feature: they close the gap between “the GUI is no longer enough” and “fully automated config management”. If you regularly clone VMs and keep repeating the same post-clone steps, two or three well-maintained snippets buy you back time immediately — and reduce drift between systems as a side effect.
DATAZONE supports you in building reusable Proxmox provisioning — from the snippet catalogue to cluster storage to integration with your existing backup. Get in touch: Contact.
More on these topics:
More articles
Proxmox Live Migration Between Clusters: Moving VMs Without Downtime
Cross-cluster live migration with Proxmox VE 8: how to use qm remote-migrate, prepare API tokens and certificates, and move VMs without any downtime.
Proxmox GPU Passthrough on Mid-Range Servers: Which Cards Are Worth It?
Proxmox GPU passthrough in 2026 for SMB: NVIDIA L4, L40S, AMD Instinct and used Tesla T4 compared. IOMMU, vfio-pci, AI inference and vGPU setup.
Proxmox 3-Node Cluster Without Shared Storage: ZFS Replication Instead
Proxmox HA cluster without Ceph or SAN: How ZFS replication, QDevice quorum and local storage enable a cost-effective 3-node cluster for SMB.