OpenTofu on the Proxmox box. The homelab gets a plan phase.
Everything in my homelab is managed except the thing everything runs on. The guests have Ansible, the configs are in git, and the hypervisor itself has forty hand-built VMs, a naming convention that drifted in 2023, and no plan phase. Infrastructure as code stops at the hypervisor door for most homelabs I have seen, usually because the tools feel enterprise-shaped. They are not, and the evening I spent proving it to myself is written up here.
Why OpenTofu
OpenTofu is the Linux Foundation fork of Terraform — drop-in CLI (tofu init / plan / apply), state-compatible, and its registry mirrors the provider ecosystem, so anything written for Terraform resolves by the same name. I compared the fork and upstream in March; for a homelab the decision is even simpler, because the licence question that motivates most migrations does not even apply to a box nobody is selling. You can use either. I use OpenTofu everywhere, so I used it here.
The provider
Two Proxmox providers exist, and choosing wrong is the first mistake half the tutorials will make for you: bpg/proxmox is the actively maintained one, and the older telmate/proxmox still ranks well in search results despite years of stagnation. Use bpg. Your .terraform-lock.hcl and your future self will both be simpler.
The prep — the actual project
Three steps, and only one of them is hard:
- API token. Datacenter → Permissions → API Tokens: a dedicated user, a token with privilege separation, and a role like PVEVMAdmin rather than root. Write it down as
user@pam!tokenname=secret— that exact string is what the provider wants. - The certificate decision. A self-signed host cert means
insecure = truein the provider block, which is exactly as sketchy as it sounds on a LAN you control. Putting a real cert on the host takes ten minutes and removes the flag; do that if you can. - One cloud-init VM template. This is the evening. Build a template once — a minimal Debian or Ubuntu image with QEMU guest agent and cloud-init enabled, via the
qmCLI or Packer — and every VM you declare afterwards is a clone of it. The plan phase is minutes; the template is the work, and it is the same work regardless of which IaC tool sits on top.
The code
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.70"
}
}
}
provider "proxmox" {
api_url = "https://pve.lan:8006/"
api_token = var.pve_token
}
resource "proxmox_virtual_environment_vm" "docker" {
name = "docker01"
node_name = "pve1"
clone {
vm_id = 9000 # the cloud-init template
full = true
}
cpu {
cores = 4
}
memory {
dedicated = 8192
}
disk {
datastore_id = "local-lvm"
interface = "virtio0"
size = 64
}
network_device {
bridge = "vmbr0"
}
initialization {
ip_config {
ipv4 {
address = "dhcp"
}
}
ssh_keys = [var.ssh_pubkey]
}
}Same shape for LXC containers (proxmox_virtual_environment_ct) and for uploading cloud-init snippets (proxmox_virtual_environment_file). tofu plan against a hypervisor has the same texture as against any cloud: diff first, then commit the diff.
The honest trade-offs
- Template privilege separation produces
PermissionDeniederrors that point at nothing in particular. When the token works in the UI but not the provider, re-read the privilege separation checkbox before you re-read the provider docs. - State now matters on a box you also reboot casually. Remote state or, at minimum, a cron that runs
tofu state pullinto a backup. Losing the state of a hypervisor is losing the inventory of everything you thought was managed. - Linked clones are cheap and tie every VM to the template’s storage; full clones cost space and buy independence. Homelab disks being what they are, I clone full.
- The provider tracks Proxmox releases quickly, which cuts both ways. Pin the version; upgrade deliberately, not incidentally.
- Importing the forty existing snowmen is possible (
tofu import) and tedious. I did it for the three VMs I touch weekly and let the rest retire naturally. Plans that lie are worse than plans that cover nothing.
Where I landed
New VMs are reviewed diffs instead of clicks in a web UI, and “what is running on pve1?” has an answer in git. The homelab does not need enterprise process, but it benefits from the same property every production system has after its first real incident: a place where the intended state is written down, and a command that shows the difference.