My homelab runs on Proxmox and OpenTofu, here is how I set it up
I have been running a homelab for a while. It started as a few manual Docker installs, then a pile of shell scripts, and eventually something that was too painful to rebuild from scratch. Moving to Proxmox with OpenTofu as the provisioner fixed that. Now rebuilding the whole thing is one tofu apply.
Why Proxmox
Proxmox is a bare-metal hypervisor that runs both full VMs and LXC containers. The web UI is functional, the API is well-documented, and it does not cost anything. The real reason I picked it over something like ESXi is that it has a proper community and tooling around it. The Terraform/OpenTofu provider for Proxmox is actively maintained and covers everything I need.
Why OpenTofu instead of Terraform
HashiCorp changed the Terraform license to BSL in 2023. OpenTofu is the open-source fork that kept the MPL license. The syntax is identical. If you have existing Terraform configs, they work without changes. For a homelab there is no practical difference, but I would rather not build on something that can change its license again.
What gets provisioned
The setup provisions four LXC containers:
- Reverse proxy: Caddy running in a Debian LXC. Handles TLS termination and routing for all internal services. Wildcard certs from Let's Encrypt via DNS challenge.
- DNS server: AdGuard Home in its own container. Local DNS records point internal hostnames to the reverse proxy IP. Split DNS means
service.homeresolves internally without going out to the internet. - Docker host: A Debian container with Docker installed. Most services run here as Compose stacks. Keeping Docker inside an LXC rather than directly on the Proxmox host makes it easier to snapshot and restore.
- NAS: A container with a Samba share and a mounted ZFS dataset from the host. Used for media storage and backups.
The OpenTofu config
Each container is a proxmox_lxc resource. The provider talks to the Proxmox API over HTTPS. Auth is an API token scoped to what the provider needs.
resource "proxmox_lxc" "reverse_proxy" {
target_node = "pve"
hostname = "caddy"
ostemplate = "local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst"
unprivileged = true
cores = 1
memory = 512
rootfs {
storage = "local-lvm"
size = "4G"
}
network {
name = "eth0"
bridge = "vmbr0"
ip = "192.168.1.10/24"
gw = "192.168.1.1"
}
}
Static IPs are assigned in the config rather than relying on DHCP. This matters because the DNS server needs to know where the reverse proxy is before DHCP assignments are stable.
Provisioning vs configuration
OpenTofu handles provisioning: create the container, set the CPU/memory, assign the IP. It does not configure what runs inside. For that I use Ansible. OpenTofu creates the container, outputs the IP, and Ansible picks it up from there to install packages and drop config files.
Keeping these two concerns separate makes each tool do what it is good at. OpenTofu is declarative infrastructure state. Ansible is imperative configuration. Mixing them creates configs that are hard to read and harder to debug.
What actually works well
The whole setup is in the labmox repo. Destroying and recreating any container is a tofu destroy -target followed by tofu apply. Snapshots on the Proxmox side give a rollback option before doing anything risky. The combination of IaC for provisioning and container-level snapshots covers most failure modes.
The thing I get most out of this is not automation for its own sake. It is that I know exactly what is running and why. Six months from now I can open the config and understand the setup without having to reverse-engineer it from a running system.
Links
- Github repo: labmox