I don’t run Terraform against my Proxmox box day to day — my containers get created directly with pct, and that’s honestly the right tool for a homelab of ~30 LXCs I mostly manage by hand. But “would Terraform make sense here” is a fair question, so I set aside an afternoon, wrote the smallest config that actually provisions something, and ran it against my real host. It broke twice before it worked, and both breaks are worth knowing about before you try this yourself.
The Setup
The bpg/proxmox provider (not the older, now-unmaintained Telmate one) talks to the Proxmox API directly — no SSH, no agent. It needs an API token, which on my box meant:
pveum user token add root@pam terraform-test --privsep 0--privsep 0 means the token inherits the user’s full permissions instead of needing its own ACL — fine for a throwaway test, not what I’d use for anything that sticks around (a scoped role with just VM.Allocate/VM.Config.*/Datastore.AllocateSpace is the real answer, and I deleted this token the moment I was done).
provider "proxmox" {
endpoint = "https://127.0.0.1:8006"
api_token = "root@pam!terraform-test=<secret>"
insecure = true # self-signed cert, internal network only
}
resource "proxmox_virtual_environment_container" "test" {
node_name = "proxmox"
vm_id = 997
operating_system {
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
type = "debian"
}
initialization {
hostname = "tf-test"
ip_config {
ipv4 { address = "dhcp" }
}
}
network_interface {
name = "eth0"
bridge = "vmbr0"
}
disk {
datastore_id = "local-lvm"
size = 4
}
cpu { cores = 1 }
memory { dedicated = 256 }
unprivileged = true
started = true
}An LXC container, not a QEMU VM — the provider does both, but on a box that already runs everything as LXC, matching that made more sense than introducing a second guest type just for this test. The initialization block is the provider’s equivalent of cloud-init for containers: hostname and network config injected at first boot, no template surgery required.
Break One: “Apply Failed” for a Container That Actually Got Created
terraform apply ran, then errored:
Error: error waiting for container created: task "...:vzcreate:997:..." failed
to complete with exit code: WARNINGS: 1Checking the actual Proxmox task log told a different story — the container had been created and started fine:
WARN: Systemd 257 detected. You may need to enable nesting.
TASK WARNINGS: 1The provider treats any non-empty warning count from the Proxmox task API as a failure and refuses to save the resource into state — even though the underlying operation succeeded. That leaves an orphan: a real container Terraform doesn’t know about. Running apply again just fails harder:
Error: error creating container: received an HTTP 500 response
- Reason: CT 997 already exists on node 'proxmox'The fix in the moment was manual — pct destroy 997 --purge 1, clear the local state file, apply clean. The fix that actually matters is upstream of that: don’t ignore the warning, act on it.
Break Two: the Warning Was Real
Checking the fresh container confirmed the warning wasn’t noise:
$ pct exec 997 -- systemctl is-system-running
degraded
$ pct exec 997 -- systemctl --failed
dev-mqueue.mount failed POSIX Message Queue File System
run-lock.mount failed Legacy Locks Directory /run/lock
tmp.mount failed Temporary Directory /tmpDebian 13’s systemd (257) tries to mount these inside the container at boot, and an unprivileged LXC without nesting enabled blocks exactly that class of mount. This is the same constraint that trips people up running Docker or k3s in an LXC — it’s not specific to Terraform, but Terraform’s default container resource doesn’t turn it on for you. One block fixes it:
resource "proxmox_virtual_environment_container" "test" {
# ...
features {
nesting = true
}
}Reapplied clean, and confirmed properly this time instead of trusting the exit code:
$ pct exec 997 -- systemctl is-system-running
running
$ pct exec 997 -- systemctl --failed
0 loaded units listed.Destroy Was the Easy Part
$ terraform destroy -auto-approve
proxmox_virtual_environment_container.test: Destroying... [id=997]
proxmox_virtual_environment_container.test: Destruction complete after 4sFour seconds, clean, no surprises — which is exactly the asymmetry that makes Terraform worth it for anything you’ll create and destroy more than once: the failure modes are all on the apply side, and once the config is right, destroy just works.
What I Actually Wanted to Know: Can You Import Instead of Recreate?
Everything above assumes Terraform creates the container from nothing. The more realistic question for a homelab that already has ~30 hand-built LXCs is the opposite one: could I bring an existing, already-running pct-created container under Terraform’s management without destroying and rebuilding it? I made a plain container by hand, the normal way, and tried:
$ pct create 993 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst
--hostname import-test --cores 1 --memory 256
--net0 name=eth0,bridge=vmbr0,ip=dhcp --rootfs local-lvm:4
--unprivileged 1 --features nesting=1
$ terraform import proxmox_virtual_environment_container.imported proxmox/993
Import successful!That part worked cleanly — the ID format is <node>/<vmid>, and the provider pulled the container’s real state in without complaint. If the story ended there, importing would look like a free lunch. It doesn’t end there.
Break Three: “Import successful” Doesn’t Mean “Plan Shows No Changes”
I ran terraform plan immediately after, with a .tf resource block that I’d written to describe the container — same node, same vmid, a disk block, a features block — without having first gone and copied every real attribute out of the actual running container. The result:
Plan: 1 to add, 0 to change, 1 to destroy.Destroy and recreate. Not “no changes,” which is what a successful import feels like it should mean. Reading through the diff, most of it was attributes I simply hadn’t declared — memory { dedicated = 256 }, the initialization block, the exact features set — which Terraform reasonably wants to reset to the provider’s defaults since my config didn’t say otherwise. Those are fixable by copying values across carefully. The one that actually forces replacement, and would have destroyed a real container if I’d run apply without reading the plan first, was subtler:
~ operating_system {
+ template_file_id = "local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst" # forces replacement
}The provider doesn’t read the origin template back out of an existing container — Proxmox itself doesn’t store “which template this was cloned from” as part of a container’s live config, so there’s nothing for import to populate that field with. It comes back empty in state no matter what, and the moment your .tf file specifies a real value for it — which every guide, this post included, tells you to do — Terraform sees a diff on an attribute that requires destroying and rebuilding the resource to change. There’s no clean fix for this one; it’s a genuine gap between what pct-created containers expose and what the provider’s schema wants for that specific field.
The Actual Rule for Adopting Existing Containers
terraform import puts a resource in your state file. It does not verify your config matches reality, and “the import command didn’t error” is not the same signal as “your config is correct” — the only command that tells you that is plan, read carefully, before apply ever runs. For a container you actually care about, that means: import, then plan, then go fix every drifted attribute in your .tf file one at a time until plan reports zero changes — and for template_file_id specifically, either leave it unset if your provider version tolerates that, or accept that this one field may never round-trip cleanly for a container Terraform didn’t originally create. Never run apply on a freshly-imported resource without reading the plan in full; “destroy and recreate” reads identically to “no changes” in a terminal you’re not paying attention to.
Would I Actually Use This?
For the throwaway container in this post — no, pct is three lines and doesn’t need a state file. Where I can see it paying for itself: anything I’d want to spin up and tear down repeatedly (a CI runner, a test environment for a specific change), or the day I have more than one Proxmox node and want one config describing all of them instead of remembering what I ran by hand on each. The two breaks above are exactly what I’d want to know before that day, instead of on it.
The Checklist
- Use
bpg/proxmox, not the archived Telmate provider. - Scope the API token’s permissions for anything that isn’t a five-minute test —
--privsep 0is a shortcut, not a default. - Don’t trust a Terraform error at face value against Proxmox — check the actual task log; “failed” sometimes means “succeeded with a warning the provider didn’t like.”
- Add
features { nesting = true }for any container that’ll run Docker, k3s, or anything else that wants its own cgroups — and verify withsystemctl is-system-running, not just “it started.”
