Rahul Reddy

I build AI platforms, Cross-Platform Apps, and the Infra they run on.

Building in the open, writing about what breaks, and sharing the occasional opinion.

hi [at] rahulmx.com
GoTypeScriptSwiftPythoniOSSwiftUIReact NativeNext.jsReactNode.jsFastAPILLM OrchestrationAgentic SystemsLiteLLMRAGPrompt EngineeringPostgreSQLBigQuerySnowflakeRedisMongoDBGCPDockerTerraformProxmoxGitHub ActionsCloudflare WorkersCloudflare R2Cloudflare D1Cloudflare TunnelDistributed SystemsAPI DesignData PipelinesPlatform ArchitectureGoTypeScriptSwiftPythoniOSSwiftUIReact NativeNext.jsReactNode.jsFastAPILLM OrchestrationAgentic SystemsLiteLLMRAGPrompt EngineeringPostgreSQLBigQuerySnowflakeRedisMongoDBGCPDockerTerraformProxmoxGitHub ActionsCloudflare WorkersCloudflare R2Cloudflare D1Cloudflare TunnelDistributed SystemsAPI DesignData PipelinesPlatform Architecture

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.home resolves 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