Proxmox VM Templates with Cloud-Init and OpenTofu
Define VMs as code and spin up a fully configured, ready-to-SSH-into machine in minutes from one cloud-init template -- instead of clicking through the same install wizard again.
What you'll build and why
A cloud-init-enabled VM template turns "install an OS" into "clone a template and let it configure itself on first boot" -- hostname, network, and an SSH key all set automatically, no install wizard. OpenTofu then lets you describe the VM itself (CPU, memory, disk, which template, which network) as a file you can version-control and re-apply, instead of a one-off manual clone you'd have to remember how you configured.
Don't build this if: you only ever create a VM once in a while and don't mind the manual process -- the payoff here is real for repeated or scripted VM creation, less so for a single VM you'll set up once and never touch again.
Important note on the provider: this guide uses the bpg/proxmox provider, the most widely used OpenTofu/Terraform provider for Proxmox -- but it's community-maintained, not an official Proxmox product. It's actively developed and broadly relied on in the Proxmox community, but treat it accordingly: check its own release notes when upgrading, and don't assume Proxmox's official support channels cover issues specific to it.
How it works
Cloud image (e.g. Debian/Ubuntu cloud .img)
│
▼
Proxmox VM template (built once, with cloud-init
drive attached -- this is what gets cloned)
│
│ OpenTofu, via the bpg/proxmox provider,
│ talks to Proxmox's API
▼
New VM -- cloned from the template, cloud-init
applies your OpenTofu-defined hostname/
network/SSH key on first boot
Cloud-init runs once on first boot, reads the configuration Proxmox hands it (via the cloud-init drive OpenTofu configures), and applies it -- this is the same mechanism cloud providers use for instance initialization, running locally on your own hardware.
Before you start
Decision: which cloud image as your base? Debian and Ubuntu both publish official cloud images built for exactly this use case (minimal, cloud-init-ready). This guide uses Debian's; the process is nearly identical for Ubuntu's.
Steps
Step 1: Download a cloud image and build the template
On the Proxmox host, via SSH (see the CLI reference guide for command background):
$ wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2
$ qm create 9000 --name debian-12-cloudinit-template --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
$ qm importdisk 9000 debian-12-generic-amd64.qcow2 local-lvm
$ qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
$ qm set 9000 --ide2 local-lvm:cloudinit
$ qm set 9000 --boot c --bootdisk scsi0
$ qm set 9000 --serial0 socket --vga serial0
$ qm template 9000
Expected output: VM ID 9000 now exists as a template (visible in the Proxmox UI with a distinct template icon) -- this is what every future VM in this guide clones from, never booted directly itself.
Step 2: Install OpenTofu on your control machine
$ curl --proto '=https' --tlsv1.2 -fsSL https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
$ chmod +x install-opentofu.sh
$ ./install-opentofu.sh --install-method standalone
(The official installer script -- check opentofu.org for current recommended install methods if this has changed.)
Step 3: Configure the provider
main.tf:
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "0.<current-major>.x"
}
}
}
provider "proxmox" {
endpoint = "https://<proxmox-host>:8006/"
api_token = "<your-proxmox-api-token>"
insecure = false
}
Generate an API token in Proxmox first (Datacenter → Permissions → API Tokens) rather than using your own account's password here -- scope it to only what OpenTofu needs.
Step 4: Define a VM cloned from the template
resource "proxmox_virtual_environment_vm" "test_vm" {
name = "test-vm-01"
node_name = "<proxmox-node-name>"
clone {
vm_id = 9000
}
cpu {
cores = 2
}
memory {
dedicated = 2048
}
initialization {
ip_config {
ipv4 {
address = "<10.0.20.50>/24"
gateway = "<10.0.20.1>"
}
}
user_account {
username = "<your-username>"
keys = [file("~/.ssh/id_ed25519.pub")]
}
}
}
Step 5: Apply
$ tofu init
$ tofu plan
$ tofu apply
Expected output: tofu plan shows the VM to be created before anything happens; tofu apply (after confirming) creates it, and within a minute or two of boot, cloud-init applies the configured network and SSH key.
Verify it works
-
tofu planafter a successfulapplyshows no changes -- confirms the real state matches what's defined, not drifted - The new VM appears in the Proxmox UI, cloned from the template
-
ssh <your-username>@<the-configured-ip>succeeds using the key set ininitialization.user_account, with no manual login step - Failure test: change a value in
main.tf(e.g.memory.dedicated), runtofu planagain, and confirm it correctly shows only that one change -- not a full VM recreation, unless the changed attribute genuinely requires one (some do; the plan output tells you which)
Secure it
- API token scope: the token from Step 3 should have only the Proxmox permissions OpenTofu actually needs (VM creation/management), not full admin -- least privilege applies to automation credentials as much as human ones.
- Secrets in
.tffiles: never commit the API token directly into a committed.tffile -- use a.tfvarsfile excluded from git, or environment variables, the same principle as any infrastructure-as-code secret. - SSH key distribution: the guide's
user_account.keysbakes a public key into every VM created this way -- fine for your own key, but be deliberate if this template is ever shared or used by others. - Community provider risk: since
bpg/proxmoxisn't an official Proxmox product, pin a specific provider version (theversionconstraint in Step 3) rather than always taking latest, so a breaking provider update doesn't surprise a futuretofu apply.
Back it up and maintain it
What matters: your .tf files and the template VM itself -- the .tf files belong in git; the template is small and easy to regenerate from Step 1 if lost, but note down the base image version you used.
Update cadence: update the cloud image periodically (a fresh template picks up base-OS security patches at clone time, reducing what needs patching post-boot); update the OpenTofu provider deliberately, reading its changelog first given it's community-maintained.
What to monitor: nothing runs continuously -- OpenTofu only acts when you run it. Periodically run tofu plan against your existing VMs to catch drift (manual changes made outside OpenTofu that your state no longer reflects).
Troubleshooting
Logs: OpenTofu's own CLI output; Proxmox's task log (Datacenter → Tasks) for the actual clone/provisioning operations.
| Symptom | Likely cause | Diagnostic | Fix |
|---|---|---|---|
tofu apply fails to authenticate |
API token invalid, expired, or insufficiently scoped | Test the token directly against Proxmox's API with curl, isolating whether it's an OpenTofu-side or Proxmox-side problem |
Regenerate the token with correct permissions |
| VM clones but cloud-init never applies (default hostname/no network) | The cloud-init drive wasn't attached to the template correctly (Step 1), or the base image isn't cloud-init-enabled | Check qm config <vmid> on the cloned VM for the cloud-init drive; confirm the source image was a genuine cloud image, not a standard installer ISO |
Redo Step 1 with a confirmed cloud-init-ready image |
tofu plan shows constant drift even with no manual changes |
State file doesn't match reality (partial apply failure, or manual edits in the Proxmox UI) | tofu state show <resource> vs. actual VM config in Proxmox |
Reconcile manually, or tofu refresh to resync state with real infrastructure |
| VM created but unreachable over SSH | Network config in initialization.ip_config doesn't match your actual LAN, or the SSH key wasn't applied |
Check the VM's console directly in Proxmox for cloud-init's own boot log | Fix the IP/gateway values; confirm the public key path in user_account.keys is correct |
Provider version mismatch error on tofu init |
Provider version constraint too strict for what's actually available, or a breaking change in a new major version | Check the exact error against the provider's changelog | Adjust the version constraint deliberately, reading what changed |
Undo
$ tofu destroy
Removes every resource OpenTofu created and tracks in state -- this deletes the VM, confirm before running against anything with real data. Removing the template itself is a manual qm destroy 9000 on the Proxmox host, separate from anything OpenTofu manages.
Go further
- Automating the Homelab with Ansible -- pair with this guide directly: OpenTofu provisions the VM, Ansible configures what's inside it, a standard combined pattern
- Parameterize the VM resource with OpenTofu variables and
count/for_eachto provision multiple similar VMs from one definition - Store OpenTofu state remotely (rather than a local file) if more than one person or machine will ever run
tofu applyagainst the same infrastructure
Resources
Official documentation:
- OpenTofu documentation -- verified this session
- bpg/proxmox provider documentation -- community-maintained, verified this session
- Proxmox VE cloud-init documentation
Source and releases:
Community:
Related DaemonPress projects:
Last verified: 2026-09-21, checked against official OpenTofu and bpg/proxmox provider documentation.