Quick Start: Get Terraform + Proxmox Running in 5 Minutes
I run a homelab with Proxmox VE managing 12 VMs and containers — it’s my playground for testing everything before pushing to production. After 6 months of using Terraform to manage the entire infrastructure, I never want to manually click through the UI to create VMs again. Everything is code: reviewable, rollbackable, and reproducible in minutes.
Before diving into the details, let’s try it out first:
Step 1: Install Terraform
# Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform version
Step 2: Create an API Token on Proxmox
Go to Proxmox Web UI → Datacenter → Permissions → API Tokens → Add. Create a token for user root@pam and uncheck “Privilege Separation” so the token has full permissions. Copy the token string — it’s only shown once.
Step 3: Minimal Configuration File
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.60"
}
}
}
provider "proxmox" {
endpoint = "https://192.168.1.100:8006/"
api_token = "root@pam!terraform=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
insecure = true # remove if you have a valid SSL cert
}
resource "proxmox_virtual_environment_vm" "ubuntu_vm" {
name = "ubuntu-test"
node_name = "pve"
vm_id = 200
cpu {
cores = 2
}
memory {
dedicated = 2048
}
disk {
datastore_id = "local-lvm"
file_id = "local:iso/ubuntu-22.04-live-server-amd64.iso"
interface = "virtio0"
size = 20
}
network_device {
bridge = "vmbr0"
}
}
terraform init
terraform plan
terraform apply
Once done, the ubuntu-test VM appears on the Proxmox dashboard immediately — no clicking required.
Deep Dive: How the bpg/proxmox Provider Works
There are currently two popular providers for Proxmox: Telmate/proxmox (older, less maintained) and bpg/proxmox (newer, actively maintained, supports more resources). I tried both and switched to bpg after Telmate failed to handle Cloud-Init correctly on Ubuntu 22.04.
Main Resource Types
proxmox_virtual_environment_vm— create and manage VMsproxmox_virtual_environment_container— LXC containersproxmox_virtual_environment_download_file— download ISO or cloud imagesproxmox_virtual_environment_pool— create resource poolsproxmox_virtual_environment_user— manage Proxmox users
Real-World Workflow with Cloud-Init
Cloning from a Cloud-Init template is what I do every day. The VM is SSH-ready as soon as it boots — no manual installation needed.
resource "proxmox_virtual_environment_vm" "web_server" {
name = "web-prod-01"
node_name = "pve"
vm_id = 201
clone {
vm_id = 9000 # ID of the Ubuntu Cloud-Init template
full = true # full clone, independent of the template
}
cpu {
cores = 4
type = "x86-64-v2-AES"
}
memory {
dedicated = 4096
}
network_device {
bridge = "vmbr0"
model = "virtio"
}
initialization {
ip_config {
ipv4 {
address = "192.168.1.201/24"
gateway = "192.168.1.1"
}
}
user_account {
username = "ubuntu"
keys = [file("~/.ssh/id_ed25519.pub")]
}
dns {
servers = ["1.1.1.1", "8.8.8.8"]
}
}
lifecycle {
ignore_changes = [
initialization, # prevent re-apply on every plan
]
}
}
Advanced: Managing Multiple VMs with for_each and Modules
As the number of VMs grows — I have 12 — copy-pasting resource blocks becomes unmaintainable. Using for_each combined with locals solves this cleanly:
locals {
vms = {
"web-01" = { cores = 2, memory = 2048, ip = "192.168.1.201", vm_id = 201 }
"web-02" = { cores = 2, memory = 2048, ip = "192.168.1.202", vm_id = 202 }
"db-01" = { cores = 4, memory = 8192, ip = "192.168.1.210", vm_id = 210 }
"cache" = { cores = 2, memory = 4096, ip = "192.168.1.220", vm_id = 220 }
}
}
resource "proxmox_virtual_environment_vm" "vms" {
for_each = local.vms
name = each.key
node_name = "pve"
vm_id = each.value.vm_id
clone {
vm_id = 9000
full = true
}
cpu {
cores = each.value.cores
}
memory {
dedicated = each.value.memory
}
initialization {
ip_config {
ipv4 {
address = "${each.value.ip}/24"
gateway = "192.168.1.1"
}
}
user_account {
username = "ubuntu"
keys = [file("~/.ssh/id_ed25519.pub")]
}
}
}
Adding a new VM is just one line in locals.vms. To remove one, delete that line and run terraform apply.
Output for Ansible Integration
Once Terraform creates the VMs, Ansible handles the internal provisioning. Combining these two tools is the workflow I use in production:
output "vm_ips" {
value = {
for k, v in proxmox_virtual_environment_vm.vms :
k => v.initialization[0].ip_config[0].ipv4[0].address
}
}
# Get IPs to build a dynamic Ansible inventory
terraform output -json vm_ips | jq -r 'to_entries[] | "\(.key) ansible_host=\(.value | split("/")[0])"'
Practical Tips After 6 Months in Production
1. Create a Proper Cloud-Init Template — The Most Important Step
The template must be created from a cloud image, not a manually installed ISO. This is what tripped me up the most at the beginning:
# Run on the Proxmox host
wget https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img
qm create 9000 --name "ubuntu-22.04-cloud" --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
qm importdisk 9000 jammy-server-cloudimg-amd64.img local-lvm
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0
qm set 9000 --boot c --bootdisk scsi0
qm set 9000 --ide2 local-lvm:cloudinit
qm set 9000 --serial0 socket --vga serial0
qm set 9000 --agent enabled=1
qm template 9000
2. Safely Store the State File with a Remote Backend
The state file (terraform.tfstate) is what Terraform uses to track which VMs exist. Lose this file and you lose track of everything. Using self-hosted MinIO as an S3 backend is the ideal solution for a homelab:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "proxmox/homelab.tfstate"
region = "main"
endpoint = "http://192.168.1.50:9000"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
}
}
3. Always Review the Plan Before Applying
# Save the plan to a file, review it carefully, then apply
terraform plan -out=tfplan
terraform show tfplan
terraform apply tfplan
# Destroy a specific VM (not all of them)
terraform destroy -target='proxmox_virtual_environment_vm.vms["web-01"]'
Common Errors and Fixes
- Cloud-Init not applying: Check that the template has a cloudinit IDE drive — you may have missed the
qm set 9000 --ide2 local-lvm:cloudinitstep - Permission denied when applying: The API token needs
VM.Allocate,VM.Clone,Datastore.AllocateSpace, andVM.Config.Networkpermissions - Terraform stuck at “waiting for VM to start”: QEMU Guest Agent is not installed in the template — missing the
qm set 9000 --agent enabled=1step - Destroy fails: If a VM has snapshots, Terraform cannot delete it automatically — you need to remove the snapshots manually in Proxmox first. I once spent 2 hours debugging this one
Once the entire homelab infrastructure is set up with Terraform, it takes less than 10 minutes to recreate everything from scratch if needed. That’s exactly why IaC with Proxmox is worth the upfront setup time.

