Once again, despite my efforts to keep this blog updated and create something valuable to give back to the community, I fell short.

So, let’s shake it off and move forward. Lately, I’ve been exploring new topics to learn, and I decided it was time to level up my DevOps skills. Today, we’re going to look at something I really enjoyed building: a home lab with Proxmox, using Terraform to provision virtual machines. At this point, you might be asking yourself, “What is he planning to do with that?” If your guess is a Kubernetes cluster, you’re absolutely right. My goal is to build a Kubernetes cluster for a home lab where I can experiment, test ideas, and learn in a more practical way, without constantly creating and destroying VMs on cloud providers for simple tasks. This setup will not only give me a better space to explore, but it will also help me make more informed decisions and save money when it’s time to move workloads to the cloud. And of course, having a bit of on-premises infrastructure never hurt anybody.
In this post
- A Proxmox-based home lab
- A reusable VM template
- A Terraform configuration to provision:
- 3 control plane nodes
- 3 worker nodes
- A generated Ansible inventory for future automation
Prerequisites
- Basic Linux knowledge
- Proxmox VE installed
- Terraform installed
- SSH key pair
What you will do?
Here is the diagram of what we are trying to achieve.

Let’s start
I’ll start by describing the setup I’m using to build this. This is simply my current setup, and you can adapt it to match your own situation.
I have a gaming desktop that I originally built for gaming—but, ironically, gaming is the one thing I’m not using it for. So, I bought a 1 TB SATA SSD, which is more than enough for this purpose. You could easily do the same with a 500 GB drive, so don’t worry if you have less storage available.
If you’re in a more comfortable financial position, you could also buy a Mini PC or even build a dedicated home server. That part is entirely up to you.
At a minimum, you’ll need a computer that supports virtualization in order to create this home lab.
What is Proxmox VE?
Proxmox VE (Virtual Environment) is a comprehensive, open-source server management platform designed for enterprise virtualization.
It enables you to create, deploy, and manage virtual machines through an intuitive web-based interface. This makes it easy to handle your virtual infrastructure efficiently, especially in clustered environments. Take a look at the image below, which was taken from the official Proxmox website. It is well worth exploring further.

Why Proxmox vs Cloud
Using cloud providers like AWS or Azure is incredibly convenient, but for this kind of experimentation, I wanted something more predictable and cost-efficient.
Running a home lab with Proxmox gives me:
- Full control over the infrastructure
- No ongoing costs for spinning resources up and down
- A stable environment where I can break things without worrying about billing
In the cloud, even simple experiments can become expensive if you forget to tear resources down. With Proxmox, once the hardware is in place, I can experiment freely.
That said, I still see cloud as essential — this setup is not a replacement, but a complement for learning and testing before moving workloads to the cloud.
Why Terraform vs Ansible-only?
Ansible is great for configuration management, but when it comes to infrastructure provisioning, Terraform provides a more structured and predictable approach.
With Terraform, I get:
- Declarative infrastructure (desired state)
- Idempotency out of the box
- A clear execution plan (
terraform plan) before applying changes
While Ansible can create VMs, it’s not its primary strength. Terraform is purpose-built for provisioning infrastructure, while Ansible shines when configuring what runs inside those machines.
In this setup:
- Terraform → creates the VMs
- Ansible → configures the cluster (next step 👀)
Why Static IP vs DHCP Reservation?
For this lab, I chose to assign static IPs directly via cloud-init instead of relying on DHCP reservations.
The main reasons:
- Predictability: IPs are defined in code
- Portability: the setup works independently of router configuration
- Reproducibility: I can recreate the same environment anywhere
DHCP reservations work well, but they introduce an external dependency (your router configuration), which makes the setup less portable.
By defining IPs in Terraform, everything becomes part of the infrastructure code.
Why not just use the Cloud?
This is a fair question — and honestly, for many scenarios, the cloud is the right answer.
However, for learning and experimentation:
- Costs can add up quickly
- You depend on internet connectivity
- You may hesitate to experiment freely
With a home lab:
- You have a safe environment to break things
- No cost anxiety
- Faster iteration for small tests
That said, I still plan to mirror this setup in the cloud later, which is where the real value comes in — validating ideas locally before scaling them.
Why not use k3s instead of full Kubernetes?
k3s is an excellent lightweight Kubernetes distribution and is often the better choice for edge environments or low-resource setups.
However, my goal here is to:
- Learn Kubernetes as close to production as possible
- Understand the full control plane components
- Gain hands-on experience with tools like
kubeadm
Using full Kubernetes gives me deeper insight into:
- Cluster bootstrapping
- Networking
- Control plane behavior
k3s abstracts some of that complexity, which is great in practice, but for learning purposes, I want to understand what’s happening under the hood.
What did I do?
I’ve downloaded the Proxmox VE ISO and created a bootable USB drive using balenaEtcher so I could boot my PC from it.
One important thing to keep in mind is:
Proxmox VE is built on Debian Linux, so when you install it, you are essentially installing a Debian-based Linux distribution with Proxmox VE on top.
The installation process is quite straightforward. You can watch a YouTube tutorial or simply try it yourself and explore as you go. I’ll leave that up to you. For now, here are a few tips that may be helpful:
- You should reserve a range of IP addresses on your router (DHCP server).
- Your router assigns IP addresses in your local network through DHCP server.
- In most modern routers, there is a feature to reserve some ip addresses to not be used by the DHCP servers.
- Example: on my personal network the DHCP server can only assign automatically the IPs from 192.168.0.100 to 192.168.0.250… 150 Ip addresses are more than enough for my personal devices and gadget.
So, on the Proxmox VE installation you will have the chance to set the FIXED IP address for your Proxmox VE. (In my case I’ve used 192.168.0.50). Follow up, the installation process and good luck.
If you are using a MiniPC, or another device to install Proxmox and will access it over your local network there is a thing you can do to make it cool.
Go on your hosts file and point the Proxmox server IP to an address, for example:
192.168.0.50 myproxmox.lab
Every time you type on your browser http://myproxmox.lab:8006 you will access the Proxmox VE interface (as shown on the image at the beginning)
Great…. what now?
Now, we will see where things get real.
What can I do now?
With Proxmox you can create Virtual Machines in the same way as we create on VMWare or VirtualBox. But, I don’t think that is a way of handling VMs when we are talking about things in production (even though we are doing thing on a home lab)
So, what can we do for this? How to work in a more productive way?
Well, we can use templates. So we will have a base line that will be used create new VM as we need.
This template can be created with qcow2 image.
A QCOW2 (QEMU Copy-On-Write) is a versatile virtual disk image format used primarily by the QEMU emulator and KVM virtualization to store virtual machine (VM) data. It supports thin provisioning (only uses space actually written to), snapshots, compression, and AES encryption, making it more efficient and flexible than raw disk images.
I took the chance to create a script to make this automated for me, feel free to copy, make your changes, don’t forget to share what improvements you made.
Basically the script above creates a template and add it to Proxmox.
I copied this script to my proxmox and ran it as root like this:./create-debian12-template.sh 9000 local-lvm
Remember I create with Debian because I like it. You can use any distro you like which has qcow2 image. So, use your creativity.
Once this template is shown at your Proxmox VE interface you are able to use it to create your VMs with a baseline.
And now we will use it with terraform.
Creating VMS with terraform on proxmox ve
Since this is not a Linux, Shell Script not Terraform course I will talk about briefely what my script does. The full repo is here
#main.tf
terraform {
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.69"
}
}
}
provider "proxmox" {
endpoint = var.proxmox_host_address
api_token = "${var.pm_api_token_id}=${var.pm_api_token_secret}"
insecure = true
}
Basically I am setting the Terraform required provider to use the “bpg/proxmox”, which more can be found here in the Terraform Registry
While on the provider proxmox to configure the endpoint as my Proxmox host, the api_token you can generate using the UI followint the steps below
- Log in to the Proxmox web UI
- Go to Datacenter > Permissions > API Tokens
- Click Add
- Select a user (or create a dedicated user first)
- Set a Token ID
- Uncheck “Privilege Separation” if you want the token to inherit the user’s permissions
- Copy the token value (it is shown only once)
The API token format is <user>@<realm>!<tokenid>=<secret>
You can see the option insecure=true this is because the self-signed TLS certificate is in use: To solve that you can either:
- Add a Promox certificate to Trusted Store OR
- Use a valid TLS (Let’s Encrypt)
NOTE: You may have noticed that I am not concerned with security at in this article, and you are right! This is for a home lab, and I am focusing on get things done, of course security is a must, but for this one let’s keep it simple and fun, we are not creating things for production environment with sensitive stuff, we are having fun! So for this setup, I’m intentionally keeping things simple. Also, it’s is also a great opportunity for you to dig into security and do it yourself in your own way
# control-planes.tf
resource "proxmox_virtual_environment_vm" "control_planes" {
count = var.control_plane_nr
name = format("%s-%d", var.control_plane_naming, count.index + 1)
node_name = var.target_node
vm_id = var.control_plane_id_range + count.index + 1
on_boot = true
started = true # Explicitly start VM after creation so cloud-init runs
clone {
vm_id = var.template_vmid
full = true
datastore_id = "local-lvm"
}
agent {
enabled = true # The template must have qemu-guest-agent installed and enabled, otherwise this will not work correctly.
}
cpu {
cores = var.control_plane_cores
sockets = var.control_plane_sockets
}
memory {
dedicated = var.control_plane_memory
}
disk {
datastore_id = "local-lvm"
file_format = "raw"
interface = "scsi0"
size = var.control_plane_disksize
}
network_device {
bridge = var.bridge_network
model = "virtio"
# firewall is omitted (defaults to false) -- no Proxmox firewall on this NIC
}
initialization {
datastore_id = "local-lvm"
interface = "ide2"
user_account {
username = var.ciuser
keys = [var.ssh_keys]
}
ip_config {
ipv4 {
address = "${cidrhost(var.bridge_cidr_range, var.control_plane_network_range + count.index)}/24"
gateway = cidrhost(var.bridge_cidr_range, 1)
}
}
}
# Proxmox may report network info changes after cloud-init runs;
# ignore those to prevent unnecessary plan diffs.
lifecycle {
ignore_changes = [network_device]
}
tags = ["masters"]
}
In this file, we are setting up how the control planes should be created. This will use the templates that we created previously as baseline and here we are customizing the way we like.
The workers will be created in the same way you can check it out on repo that I provided above.
I want to show you the variables.tf file below, this is where the configuration shines.
We can pass all the parameters we need in this file, take some time and read it so you will understand it better.
variable "pm_api_token_id" {
type = string
default = "<generated_token_id_on_proxmox_ui>" #Example: "terraform@pve!terraform-token"
}
variable "pm_api_token_secret" {
type = string
default = "<generated_token_secret_on_proxmox_ui>" #Example: "abc123def456ghi789jkl012mno345pq"
}
#### Global Parameters ####
variable "bridge_network" {
type = string
default = "vmbr0" #Update with your bridge network name if different (e.g. vmbr1)
}
variable "bridge_cidr_range" {
type = string
default = "<your_bridge_cidr_range>" #Example: "192.168.1.0/24"
}
variable "ssh_keys" {
type = string
default = "<your_ssh_public_keys>" #Example: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3... user@hostname"
}
variable "ciuser" {
type = string
default = "proxuser" #Update with your desired cloud-init username
}
variable "target_node" {
type = string
default = "proxmox-node1" #Update with the name of the Proxmox node where you want to create the VMs
}
variable "template_vmid" {
type = number
default = 9000 #if you ran the template creation script, this should be 9000, otherwise update with the VMID of your cloud-init template
}
#### Control Plane Parameters ###
variable "control_plane_nr" {
type = number
default = 3 #Update with the number of control plane nodes you want to create
}
variable "control_plane_id_range" {
type = number
default = 400 #Update with the range of IDs for control plane nodes this will generate something like 400, 401, 402 for 3 control plane nodes. Make sure this range does not overlap with any existing VMIDs in your Proxmox cluster.
}
variable "control_plane_network_range" {
type = number
default = 60 #The IP for three control planes will end with .60, .61, .62 if you use the default bridge_cidr_range of 192.168.1.0/24
}
variable "control_plane_naming" {
type = string
default = "cp" #This will generate VM names like cp-400, cp-401, cp-402 for the control plane nodes. Update if you want a different naming convention.
}
variable "control_plane_cores" {
type = number
default = 4 #Update with the number of CPU cores you want to allocate to each control plane node
}
variable "control_plane_sockets" {
type = number
default = 1 #Update with the number of CPU sockets you want to allocate to each control plane node
}
variable "control_plane_memory" {
type = number
default = 8192 #Update with the amount of RAM in MB you want to allocate to each control plane node (e.g. 8192 for 8GB) Feel free to use less
}
variable "control_plane_disksize" {
type = number
default = 30 #Update with the disk size in GB you want to allocate to each control plane node (e.g. 30 for 30GB) Feel free to use less
}
#### Worker Node Parameters ###
variable "worker_nr" {
type = number
default = 3 #Update with the number of worker nodes you want to create
}
variable "worker_id_range" {
type = number
default = 500 #Update with the range of IDs for worker nodes this will generate something like 500, 501, 502 for 3 worker nodes. Make sure this range does not overlap with any existing VMIDs in your Proxmox cluster.
}
variable "worker_network_range" {
type = number
default = 70 #The IP for three worker nodes will end with .70, .71, .72 if you use the default bridge_cidr_range of 192.168.1.0/24
}
variable "worker_naming" {
type = string
default = "worker" #This will generate VM names like worker-500, worker-501, worker-502 for the worker nodes. Update if you want a different naming convention.
}
variable "worker_cores" {
type = number
default = 2 #Update with the number of CPU cores you want to allocate to each worker node
}
variable "worker_sockets" {
type = number
default = 1 #Update with the number of CPU sockets you want to allocate to each worker node
}
variable "worker_memory" {
type = number
default = 4096 #Update with the amount of RAM in MB you want to allocate to each worker node (e.g. 4096 for 4GB) Feel free to use less
}
variable "worker_disksize" {
type = number
default = 20 #Update with the disk size in GB you want to allocate to each worker node (e.g. 20 for 20GB) Feel free to use less
}
variable "proxmox_host_address" {
type = string
description = "Proxmox VE API endpoint URL (e.g. https://192.168.0.50:8006/)"
default = "https://192.168.0.50:8006/" #Update with your Proxmox VE API endpoint
}
After you have configured it the way you like you just go and run
terraform validateterraform planterraform apply– either with--auto-approveor not, you choose.
And see the magic, follow it up on Proxmox VE UI and see your VMs being created for your Kubernetes Cluster in the future.
But wait there is a inventory.tf file?! What is it?
Well seen, Watson!
This file right below is responsible to get the information we provided and create an Static Inventory file with a template named inventory.tmpl, so we will be able to use it with ANSIBLE. Cool ain’t it?!
resource "local_file" "ansible_inventory" {
content = templatefile("inventory.tmpl",
{
control_plane = {
index = range(var.control_plane_nr)
ip_address = [for i in range(var.control_plane_nr) : cidrhost(var.bridge_cidr_range, var.control_plane_network_range + i)]
user = [for i in range(var.control_plane_nr) : var.ciuser]
vm_name = proxmox_virtual_environment_vm.control_planes[*].name
}
worker = {
index = range(var.worker_nr)
ip_address = [for i in range(var.worker_nr) : cidrhost(var.bridge_cidr_range, var.worker_network_range + i)]
user = [for i in range(var.worker_nr) : var.ciuser]
vm_name = proxmox_virtual_environment_vm.workers[*].name
}
}
)
filename = "inventory.ini"
file_permission = "0600"
}
Well, that’s it!
Nothing much to add at this stage, as it is part of a series posts, in the next post, I’ll bootstrap Kubernetes using kubeadm and configure networking with CNI.
Meanwhile you can get check the full repo here.
👉 Full source code: https://github.com/PellizzoniCode/proxmox_terraform
This is just the beginning of the journey.
In the next post, I’ll bootstrap a Kubernetes cluster using kubeadm and configure networking with a CNI plugin.
If you have any questions or suggestions, feel free to reach out — I’d love to hear your feedback.
See you in the next one =)