Note: This is the same Terraform Phoenix Cycle, but replace miniblue.io with Proxmox.
This article focuses on transitioning our infrastructure design from local emulation layers to dedicated bare-metal server hardware using Proxmox VE.
Unlike localized development tools, Proxmox allows your automated infrastructure to scale seamlessly across physical hardware via Proxmox clustering. By utilizing the native Proxmox Terraform provider (bpg/proxmox), we can replace hacky shell scripts with structured API calls to dynamically spin up, orchestrate, and collapse lightweight Linux Containers (LXC) or full Virtual Machines (VMs).
We call this the Provision-Orchestrate-Collapse-Destroy cycle.
1. The Provisioning Phase
We interact directly with the Proxmox hypervisor layer. Instead of requiring administrative root passwords, Terraform connects securely using an unprivileged API token configured with targeted role permissions.
We define a native proxmox_virtual_environment_container (LXC) to act as our compute canvas. Proxmox maps a clean, minimal operating system image template directly from local storage pools (local-lvm or local-zfs) into the new container instance.
2. The Orchestration Phase
Once the hypervisor allocates the hardware resources, the instance must join the broader workspace network. By map the container’s network block to a physical or virtual network bridge (vmbr0). This instantly grants the container a real, routable IP address on the local network interface.
Then execute our targeted deployment payload inside the isolated guest environment—such as configuring an automated Nginx reverse proxy gateway.
3. The Condition
Because our environment runs across real network interfaces rather than a local loopback device, the orchestration layer evaluates system health using automated network verification logic. The pipeline actively listens for a successful state validation or task-completion signal over the network interface.
4. The Teardown
Once validation finishes, we return the physical host back to its baseline. Terraform signals the Proxmox API to terminate the active container processes. Proxmox safely detaches the storage volumes and scrubs the local disk blocks.
The cluster map updates, returning 100% of the allocated CPU cores and RAM segments back to the hypervisor pool, leaving the physical hardware entirely pristine.
Example Implementation
Create Proxmox Container (LXC)
↓
Inject Base Operating System
↓
Deploy Nginx Reverse Proxy Services
↓
Validate Gateway Network Health
↓
Collapse Guest Workloads
↓
Destroy Proxmox Container Allocation
↓
Reclaim Bare-Metal System Resources
Directory Layout
Phoenix-Proxmox-ReverseProxy/
├── main.tf
├── providers.tf
├── variables.tf
├── versions.tf
│
├── config/
│ └── nginx-default.conf
│
└── scripts/
├── wait_proxy_ready.sh
├── validate_destroy.sh
├── collapse_workloads.sh
└── destroy_everything.sh
Version Configuration
filename: versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.106.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.2"
}
}
}
Provider Configuration
filename: ‘providers.tf’
provider "proxmox" {
endpoint = var.pm_api_url
api_token = var.pm_api_token
insecure = true # Set to false if utilizing verified SSL certificates
}
provider "null" {}
Variables
filename: variables.tf
variable "pm_api_url" {
type = string
description = "The Proxmox VE API endpoint URL (e.g., https://192.168.1.50:8006/api2/json)"
}
variable "pm_api_token" {
type = string
secret = true
description = "The unprivileged API Token secret string (UserID!TokenID=SecretUUID)"
}
variable "target_node" {
type = string
default = "pve-host"
}
variable "container_ip" {
type = string
default = "192.168.1.50"
}
Main
filename: main.tf
##########################################################
# STEP 1: Provision
# Provision the bare-metal LXC Container via Proxmox API
##########################################################
resource "proxmox_virtual_environment_container" "proxy_gateway" {
node_name = var.target_node
vm_id = 9000
hostname = "nginx-phoenix-gateway"
initialization {
architecture = "amd64"
os_template = "local:vztmpl/debian-12-standard.tar.zst"
ip_config {
ipv4 {
address = "${var.container_ip}/24"
gateway = "192.168.1.1"
}
}
}
cpu {
cores = 2
}
memory {
dedicated = 1024
}
}
##########################################################
# STEP 2: Orchestrate
# Inject Nginx configurations and deploy the network service
##########################################################
resource "null_resource" "deploy_proxy_config" {
depends_on = [proxmox_virtual_environment_container.proxy_gateway]
connection {
type = "ssh"
user = "root"
host = var.container_ip
private_key = file("~/.ssh/id_rsa")
}
provisioner "file" {
source = "config/nginx-default.conf"
destination = "/etc/nginx/conf.d/default.conf"
}
provisioner "remote-exec" {
inline = [
"apt-get update && apt-get install -y nginx",
"systemctl restart nginx"
]
}
}
##########################################################
# STEP 3: Verification Loop
# Wait for remote HTTP port readiness
##########################################################
resource "null_resource" "wait_proxy_ready" {
depends_on = [null_resource.deploy_proxy_config]
provisioner "local-exec" {
command = "bash scripts/wait_proxy_ready.sh ${var.container_ip}"
}
}
##########################################################
# STEP 4: Pre-Collapse Validation
# Validate state consistency before triggering destruction
##########################################################
resource "null_resource" "validate_destroy" {
depends_on = [null_resource.wait_proxy_ready]
provisioner "local-exec" {
command = "bash scripts/validate_destroy.sh ${var.container_ip}"
}
}
##########################################################
# STEP 5: Collapse Guest Workloads
# Stop active tasks inside the container prior to hardware deletion
##########################################################
resource "null_resource" "collapse_workloads" {
depends_on = [null_resource.validate_destroy]
provisioner "local-exec" {
command = "bash scripts/collapse_workloads.sh ${var.container_ip}"
}
}
Nginx Configuration payload
filename: config/nginx-default.conf
server {
listen 80;
server_name localhost;
location / {
root /var/www/html;
index index.html;
add_header X-Phoenix-Cycle "Active" always;
}
}
Automation Scripts
filename: scripts/wait_proxy_ready.sh
#!/usr/bin/env bash
set -e
TARGET_IP=$1
echo "[STEP] Verifying network path to Proxmox guest instance..."
MAX_RETRY=20
COUNT=0
while true; do
if curl -s -I "http://${TARGET_IP}:80" | grep -q "200 OK"; then
echo "[OK] Nginx service reachable on target IP."
exit 0
fi
COUNT=$((COUNT+1))
if [ "$COUNT" -ge "$MAX_RETRY" ]; then
echo "[ERROR] Target proxy failed to stabilize within window."
exit 1
fi
sleep 3
done
filename: scripts/validate_destroy.sh
#!/usr/bin/env bash
set -e
TARGET_IP=$1
echo "[STEP] Running infrastructure verification pre-collapse..."
# CHECK 1: Ensure custom header signature is active
HTTP_HEADER=$(curl -s -I "http://${TARGET_IP}:80" | grep -i "X-Phoenix-Cycle")
if [ -z "$HTTP_HEADER" ]; then
echo "[ERROR] Validation failed: Integrity token mismatch."
exit 1
fi
echo "[OK] Integrity validation successful."
filename: scripts/collapse_workloads.sh
#!/usr/bin/env bash
set -e
TARGET_IP=$1
echo "[STEP] Gracefully collapsing active guest connections..."
# Terminate internal daemons gracefully before hypervisor destruction
ssh -o StrictHostKeyChecking=no root@${TARGET_IP} "systemctl stop nginx"
echo "[DONE] Workloads collapsed."
filename: scripts/destroy_everything.sh
#!/usr/bin/env bash
set -e
echo "[STEP] Requesting hypervisor asset reclamation via Terraform..."
terraform destroy -auto-approve
echo "[DONE] Bare-metal environment returned to baseline."
Execution Instructions
Initialize the Proxmox provider schema:
terraform init
Execute the full bare-metal provisioning and verification pipeline:
terraform apply -auto-approve
Collapse workloads and reclaim resources back to the Proxmox pool:
bash scripts/destroy_everything.sh
Special Note
In our configuration, the destruction sequence runs exactly in reverse order of the defined dependency chain. Because the collapse_workloads resource relies explicitly on validate_destroy, Terraform guarantees the software processes running inside the guest OS are safely stopped before it initiates the destruction of the underlying hypervisor resource container.
While null_resource execution scripts are paired here alongside native resources to handle remote orchestration tasks smoothly, this establishes a scalable bare-metal automation template that entirely removes dependency on local workstation loops.