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:

  1. 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.
  2. The certificate decision. A self-signed host cert means insecure = true in 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.
  3. 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 qm CLI 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

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.