Automated LXC Container Provisioning with OpenTofu and Proxmox
Lightweight infrastructure automation with fully configured Docker-ready containers
While VMs provide robust isolation, LXC containers offer a lightweight alternative for workloads that don’t require separate kernels or operating systems. This guide extends our VM provisioning workflow to include automated container deployment, complete with Docker installation, user configuration, and SSH key management—all executed automatically through OpenTofu provisioners.
By the end of this tutorial, you’ll deploy production-ready Docker hosts in under 3 minutes with a single command.
Containers vs Cloud Images: Choosing the Right Tool
When to Use LXC Containers
- Docker/container hosts
- Web servers and APIs
- Development environments
- High-density deployments
- Fast boot times required
- Nested virtualization (with nesting feature)
When to Use Cloud Images
- Standardized VM provisioning
- Different kernel versions needed
- Maximum security isolation
- GPU passthrough required
- Kernel module loading needed
Performance Comparison
Prerequisites
- Completed VM provisioning setup (OpenTofu installed, API tokens configured)
- Proxmox VE 8.0+ (LXC support required)
- SSH key pair for authentication
- Basic understanding of Linux containers
Part 1: Ubuntu LXC Template Setup
Unlike VM cloud images, LXC templates are pre-configured root filesystem archives. Proxmox provides official templates for various Linux distributions.
Step 1: Download Ubuntu Container Template
Proxmox provides official LXC templates for various Linux distributions. You can download them through the GUI or command line.
Option 1: Proxmox Web GUI (Recommended)
- Navigate to your shared storage (e.g., “shared-nfs”) → CT Templates
- Click the “Templates” button
- Select “ubuntu-24.04-standard_24.04-2_amd64.tar.zst”
- Click “Download” and wait for completion
Option 2: Command Line
Alternatively, SSH into your Proxmox node:
# Navigate to template storage directory
cd /var/lib/vz/template/cache
# Download Ubuntu 24.04 LTS container template
wget http://download.proxmox.com/images/system/ubuntu-24.04-standard_24.04-2_amd64.tar.zst
# Verify download
ls -lh ubuntu-24.04-standard_24.04-2_amd64.tar.zst
# Expected output:
# -rw-r--r-- 1 root root 128M Nov 15 10:30 ubuntu-24.04-standard_24.04-2_amd64.tar.zst
📁 Shared Storage for Templates
Store templates on shared storage (NFS, Ceph, etc.) so all Proxmox nodes can access them. In OpenTofu, reference the template using your shared storage name:
ostemplate = "shared-nfs:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst"
- Replace
shared-nfswith your actual shared storage name vztmplis Proxmox’s internal reference (filesystem path istemplate/cache/)
Step 2: Available Template Options
Proxmox offers templates for various distributions. View available templates:
# List available container templates
pveam available --section system
# Common templates:
# ubuntu-24.04-standard_24.04-2_amd64.tar.zst (Ubuntu 24.04 LTS)
# ubuntu-22.04-standard_22.04-1_amd64.tar.zst (Ubuntu 22.04 LTS)
# debian-12-standard_12.2-1_amd64.tar.zst (Debian 12)
# alpine-3.19-default_20240207_amd64.tar.xz (Alpine Linux)
# rockylinux-9-default_20221109_amd64.tar.xz (Rocky Linux 9)
Part 2: OpenTofu Configuration for Containers
Understanding Container Configuration
Unlike VMs that use cloud-init for configuration, LXC containers require a different approach. We use OpenTofu provisioners to execute commands via SSH immediately after container creation. This allows us to install software, create users, and configure services automatically.
Container Configuration Workflow
↓
Container boots (5-10 seconds)
↓
SSH becomes available
↓
Provisioner connects with root password
↓
Execute configuration commands
↓
Fully configured container ready
Create Container Configuration Files
In your existing ~/infrastructure/proxmox directory, create the following files:
lxc-main.tf – Container Resource Definition
resource "proxmox_lxc" "container" {
target_node = var.target_node
hostname = var.container_name
ostemplate = "shared-nfs:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst"
vmid = var.container_id
cores = var.container_cores
memory = var.container_memory
# Root credentials for initial access
password = var.root_password
ssh_public_keys = file(pathexpand(var.ssh_key_file))
# Root filesystem configuration
rootfs {
storage = var.target_node == "pve1" ? "local-lvm" : "local"
size = "${var.container_disk_size}G"
}
# Network configuration with optional VLAN
network {
name = "eth0"
bridge = "vmbr0"
ip = var.use_dhcp ? "dhcp" : "${var.container_ip}/24"
gw = "10.0.0.1"
tag = var.vlan_tag
}
# DNS configuration
nameserver = "8.8.8.8 1.1.1.1"
searchdomain = "local"
# Container settings
unprivileged = true
start = true
# Post-creation configuration via SSH
provisioner "remote-exec" {
inline = [
# Wait for container to fully boot
"sleep 30",
# Enable root SSH access with keys
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config",
"sed -i 's/PermitRootLogin no/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config",
"systemctl restart sshd",
# Install prerequisites
"apt update",
"apt install -y ca-certificates curl gnupg",
# Add Docker's official GPG key
"install -m 0755 -d /etc/apt/keyrings",
"curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc",
"chmod a+r /etc/apt/keyrings/docker.asc",
# Add Docker repository
"echo 'Types: deb' > /etc/apt/sources.list.d/docker.sources",
"echo 'URIs: https://download.docker.com/linux/ubuntu' >> /etc/apt/sources.list.d/docker.sources",
"echo 'Suites: noble' >> /etc/apt/sources.list.d/docker.sources",
"echo 'Components: stable' >> /etc/apt/sources.list.d/docker.sources",
"echo 'Signed-By: /etc/apt/keyrings/docker.asc' >> /etc/apt/sources.list.d/docker.sources",
# Install Docker and Docker Compose
"apt update",
"apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
"systemctl enable docker",
"systemctl start docker",
# Create admin user with Docker access
"useradd -m -s /bin/bash -G docker,sudo admin",
"echo 'admin:${var.user_password}' | chpasswd",
"mkdir -p /home/admin/.ssh",
"echo '${var.admin_ssh_key}' > /home/admin/.ssh/authorized_keys",
"chown -R admin:admin /home/admin/.ssh",
"chmod 700 /home/admin/.ssh",
"chmod 600 /home/admin/.ssh/authorized_keys",
# Create ansible automation user
"useradd -m -s /bin/bash -G docker,sudo ansible",
"echo 'ansible:${var.user_password}' | chpasswd",
"mkdir -p /home/ansible/.ssh",
"echo '${var.ansible_ssh_key}' > /home/ansible/.ssh/authorized_keys",
"chown -R ansible:ansible /home/ansible/.ssh",
"chmod 700 /home/ansible/.ssh",
"chmod 600 /home/ansible/.ssh/authorized_keys",
# Configure passwordless sudo
"echo 'admin ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/admin",
"echo 'ansible ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/ansible",
"chmod 0440 /etc/sudoers.d/admin",
"chmod 0440 /etc/sudoers.d/ansible"
]
# SSH connection configuration
connection {
type = "ssh"
user = "root"
host = var.container_ip
private_key = file(pathexpand(var.ssh_private_key_file))
timeout = "10m"
}
}
}
lxc-variables.tf – Variable Definitions
variable "container_name" {
description = "Container hostname"
type = string
default = "test-lxc"
}
variable "container_id" {
description = "Container ID (100-999999)"
type = number
default = 500
}
variable "container_cores" {
description = "Number of CPU cores"
type = number
default = 2
}
variable "container_memory" {
description = "RAM in MB"
type = number
default = 1024
}
variable "container_disk_size" {
description = "Root filesystem size in GB"
type = number
default = 8
}
variable "container_ip" {
description = "Static IP address for container"
type = string
default = "10.0.0.100"
}
variable "vlan_tag" {
description = "VLAN tag (null for default/native VLAN)"
type = number
default = null
}
variable "use_dhcp" {
description = "Use DHCP instead of static IP"
type = bool
default = false
}
variable "root_password" {
description = "Root password for container creation"
type = string
sensitive = true
}
variable "ssh_key_file" {
description = "Path to SSH public key file for root"
type = string
default = "~/.ssh/id_rsa.pub"
}
variable "ssh_private_key_file" {
description = "Path to SSH private key for provisioning"
type = string
default = "~/.ssh/id_rsa"
}
variable "user_password" {
description = "Password for admin and ansible users"
type = string
sensitive = true
default = ""
}
variable "admin_ssh_key" {
description = "SSH public key for admin user"
type = string
default = ""
}
variable "ansible_ssh_key" {
description = "SSH public key for ansible user"
type = string
default = ""
}
Update terraform.tfvars (Add Container Variables)
Add these lines to your existing terraform.tfvars file:
# Container-specific credentials
root_password = "SecureR00tP@ssw0rd!"
user_password = "SecureUs3rP@ssw0rd!"
# SSH public keys for container users
admin_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBcD8kx... admin@workstation"
ansible_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGpQ3rY... ansible@automation"
Part 3: Deploying Containers with OpenTofu
Initialize and Deploy
# If not already initialized, run:
tofu init
# Preview what will be created
tofu plan
# Deploy container
tofu apply
Real-World Deployment Scenarios
Example Deployments
# Scenario 1: Docker host for microservices
tofu apply \
-var="container_name=docker-01" \
-var="container_id=501" \
-var="container_ip=10.0.0.151" \
-var="target_node=pve1"
# Scenario 2: High-performance development environment
tofu apply \
-var="container_name=dev-web" \
-var="container_id=502" \
-var="container_ip=10.0.0.152" \
-var="container_cores=4" \
-var="container_memory=4096" \
-var="container_disk_size=50"
# Scenario 3: Database container with VLAN isolation
tofu apply \
-var="container_name=postgres-01" \
-var="container_id=503" \
-var="container_ip=10.10.0.201" \
-var="vlan_tag=10" \
-var="container_memory=8192" \
-var="container_disk_size=100"
# Scenario 4: DHCP-enabled container for dynamic environments
tofu apply \
-var="container_name=test-env" \
-var="container_id=504" \
-var="use_dhcp=true" \
-var="container_memory=2048"
# Scenario 5: Minimal web server (1 core, 512MB RAM)
tofu apply \
-var="container_name=nginx-01" \
-var="container_id=505" \
-var="container_ip=10.0.0.155" \
-var="container_cores=1" \
-var="container_memory=512" \
-var="container_disk_size=8"
Lightweight Infrastructure at Scale
You’ve extended your infrastructure-as-code workflow to include lightweight LXC containers. Combined with your VM provisioning capabilities, you can now deploy the right tool for each workload—VMs for isolation and different operating systems, containers for efficient, high-density deployments.
What You’ve Accomplished
- Automated container deployment with Docker pre-installed
- Implemented automated user and SSH key management
- Created production-ready containers in under 3 minutes
- Established consistent configuration across containers
- Reduced resource overhead compared to VMs by 80-90%
