· Sysadmin.id · Infrastructure · 9 min read
How to Build a Proxmox Ubuntu 24.04 Template with Packer
A step-by-step guide to automating Proxmox VM template creation for Ubuntu 24.04 LTS using Packer — fully unattended cloud-init provisioning, repeatable builds, and production-ready image templates.
Manually creating VM templates in Proxmox is tedious and error-prone. Every time you need a fresh Ubuntu 24.04 base image, you click through the same installer steps, configure the same packages, and convert the VM to a template by hand.
Packer automates all of that. You define your template once in code, run one command, and get a fully configured, production-ready Proxmox VM template — every time, identically.
This guide walks through the complete setup: Proxmox API credentials, Packer HCL configuration, cloud-init autoinstall, and converting the finished VM into a reusable template.
Prerequisites
Before you start, make sure you have:
- A running Proxmox VE 7.x or 8.x node with API access
- Packer v1.10+ installed on your workstation or CI runner
- The
proxmoxPacker plugin (installed automatically viapacker init) - An Ubuntu 24.04 LTS ISO available on your Proxmox storage
- A Proxmox user with sufficient API permissions (covered below)
Note: All Packer commands in this guide are run from your workstation, not on the Proxmox host itself. Packer communicates with Proxmox entirely through the REST API.
Step 1: Install Packer
On Ubuntu or Debian:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install -y packerVerify the installation:
packer --versionStep 2: Create a Proxmox API Token
Packer needs API access to Proxmox. Using an API token is more secure than using your root password.
In the Proxmox web UI:
- Go to Datacenter → Permissions → Users → Add a user:
packer@pve - Go to Datacenter → Permissions → API Tokens → Add a token for
packer@pve, name itpacker— uncheck Privilege Separation so it inherits the user’s permissions - Copy the token secret — it is only shown once
Now assign the required permissions. In Datacenter → Permissions → Add → User Permission:
| Path | User | Role |
|---|---|---|
/ | packer@pve | PVEVMAdmin |
/storage/<your-storage> | packer@pve | PVEDatastoreAdmin |
Or via CLI on the Proxmox host:
pveum role add Packer -privs "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.Audit VM.PowerMgmt Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit"
pveum user add packer@pve --password ''
pveum aclmod / -user packer@pve -role PackerStep 3: Upload the Ubuntu 24.04 ISO to Proxmox
Download the Ubuntu 24.04 LTS server ISO and upload it to your Proxmox ISO storage:
# On the Proxmox host
wget -P /var/lib/vz/template/iso/ \
https://releases.ubuntu.com/24.04/ubuntu-24.04.2-live-server-amd64.isoOr upload it through the Proxmox web UI: Storage → ISO Images → Upload.
Note the storage name and ISO filename — you’ll need both in the Packer config.
Step 4: Project Structure
Create a working directory for your Packer project:
mkdir proxmox-ubuntu-template && cd proxmox-ubuntu-templateThe final structure will look like this:
proxmox-ubuntu-template/
ubuntu-2404.pkr.hcl # Main Packer build definition
variables.pkrvars.hcl # Your local variable values (gitignored)
http/
user-data # Cloud-init autoinstall config
meta-data # Required but can be emptyStep 5: Create the Cloud-Init Autoinstall Config
Packer boots the Ubuntu installer and feeds it an autoinstall configuration over HTTP. Create the http/ directory and both files:
mkdir http
touch http/meta-dataCreate http/user-data:
#cloud-config
autoinstall:
version: 1
locale: en_US.UTF-8
keyboard:
layout: us
network:
network:
version: 2
ethernets:
ens18:
dhcp4: true
identity:
hostname: ubuntu-template
username: ubuntu
password: "$6$rounds=4096$saltsaltsalt$hashedpassword"
ssh:
install-server: true
allow-pw: true
packages:
- qemu-guest-agent
- curl
- wget
- vim
- git
- htop
- unzip
- cloud-init
- cloud-initramfs-growroot
package_update: true
package_upgrade: true
storage:
layout:
name: direct
late-commands:
- systemctl enable qemu-guest-agent
- echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/ubuntu
- chmod 440 /target/etc/sudoers.d/ubuntu
- rm -f /target/etc/ssh/ssh_host_*
- truncate -s 0 /target/etc/machine-id
user-data:
disable_root: falseImportant: Replace the
passwordvalue with a real hashed password. Generate one with:python3 -c "import crypt; print(crypt.crypt('yourpassword', crypt.mksalt(crypt.METHOD_SHA512)))"
The late-commands at the bottom are critical for a clean template:
- Enables
qemu-guest-agentso Proxmox can communicate with the VM - Grants passwordless sudo to the
ubuntuuser - Removes SSH host keys so each clone generates its own
- Clears
machine-idso each clone gets a unique ID
Step 6: Write the Packer HCL Configuration
Create ubuntu-2404.pkr.hcl:
packer {
required_plugins {
proxmox = {
version = ">= 1.1.8"
source = "github.com/hashicorp/proxmox"
}
}
}
# ─── Variables ────────────────────────────────────────────────────────────────
variable "proxmox_url" {
type = string
default = "https://192.168.1.10:8006/api2/json"
}
variable "proxmox_node" {
type = string
default = "pve"
}
variable "proxmox_token_id" {
type = string
sensitive = true
}
variable "proxmox_token_secret" {
type = string
sensitive = true
}
variable "proxmox_storage" {
type = string
default = "local-lvm"
}
variable "proxmox_iso_storage" {
type = string
default = "local"
}
variable "iso_filename" {
type = string
default = "ubuntu-24.04.2-live-server-amd64.iso"
}
variable "vm_id" {
type = number
default = 9000
}
variable "vm_name" {
type = string
default = "ubuntu-2404-template"
}
variable "vm_cores" {
type = number
default = 2
}
variable "vm_memory" {
type = number
default = 2048
}
variable "disk_size" {
type = string
default = "20G"
}
variable "ssh_username" {
type = string
default = "ubuntu"
}
variable "ssh_password" {
type = string
sensitive = true
default = "yourpassword"
}
# ─── Build ────────────────────────────────────────────────────────────────────
source "proxmox-iso" "ubuntu-2404" {
# Proxmox connection
proxmox_url = var.proxmox_url
username = var.proxmox_token_id
token = var.proxmox_token_secret
insecure_skip_tls_verify = true
node = var.proxmox_node
# VM settings
vm_id = var.vm_id
vm_name = var.vm_name
tags = "ubuntu;template;2404"
# ISO
iso_file = "${var.proxmox_iso_storage}:iso/${var.iso_filename}"
iso_storage_pool = var.proxmox_iso_storage
unmount_iso = true
# Hardware
cores = var.vm_cores
memory = var.vm_memory
os = "l26"
cpu_type = "host"
# Network
network_adapters {
model = "virtio"
bridge = "vmbr0"
firewall = false
}
# Disk
disks {
disk_size = var.disk_size
format = "raw"
storage_pool = var.proxmox_storage
type = "virtio"
discard = true
io_thread = true
}
# Display
vga {
type = "serial0"
}
serials = ["socket"]
# Cloud-init drive — Proxmox will attach this after provisioning
cloud_init = true
cloud_init_storage_pool = var.proxmox_storage
# Boot command — triggers autoinstall via the HTTP server Packer spins up
boot_wait = "5s"
boot_command = [
"c<wait>",
"linux /casper/vmlinuz --- autoinstall ds='nocloud-net;s=http://{{ .HTTPIP }}:{{ .HTTPPort }}/'<enter><wait>",
"initrd /casper/initrd<enter><wait>",
"boot<enter>"
]
# HTTP server for cloud-init autoinstall
http_directory = "http"
http_port_min = 8802
http_port_max = 8802
# SSH connection — Packer waits for SSH to become available after install
communicator = "ssh"
ssh_username = var.ssh_username
ssh_password = var.ssh_password
ssh_timeout = "30m"
ssh_handshake_attempts = 50
# Convert to template when done
template_name = var.vm_name
template_description = "Ubuntu 24.04 LTS template — built with Packer on ${formatdate("YYYY-MM-DD", timestamp())}"
}
build {
name = "ubuntu-2404-template"
sources = ["source.proxmox-iso.ubuntu-2404"]
# Wait for cloud-init to finish
provisioner "shell" {
inline = [
"echo 'Waiting for cloud-init to complete...'",
"sudo cloud-init status --wait",
"echo 'Cloud-init complete.'"
]
}
# System cleanup before converting to template
provisioner "shell" {
inline = [
# Update and clean packages
"sudo apt-get update",
"sudo apt-get upgrade -y",
"sudo apt-get autoremove -y",
"sudo apt-get clean",
# Reset cloud-init so it runs fresh on each clone
"sudo cloud-init clean --logs",
"sudo truncate -s 0 /etc/machine-id",
"sudo rm -f /var/lib/dbus/machine-id",
"sudo ln -sf /etc/machine-id /var/lib/dbus/machine-id",
# Remove SSH host keys — each clone will regenerate them
"sudo rm -f /etc/ssh/ssh_host_*",
# Clear shell history
"sudo truncate -s 0 /root/.bash_history",
"truncate -s 0 ~/.bash_history",
# Remove temporary files
"sudo rm -rf /tmp/* /var/tmp/*",
"echo 'Template cleanup complete.'"
]
}
}Step 7: Create Your Variables File
Create variables.pkrvars.hcl with your actual values. Add this file to .gitignore — it contains credentials.
proxmox_url = "https://192.168.1.10:8006/api2/json"
proxmox_node = "pve"
proxmox_token_id = "packer@pve!packer"
proxmox_token_secret = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
proxmox_storage = "local-lvm"
proxmox_iso_storage = "local"
iso_filename = "ubuntu-24.04.2-live-server-amd64.iso"
vm_id = 9000
vm_name = "ubuntu-2404-template"
ssh_password = "yourpassword"Note: The
proxmox_token_idformat is<user>@<realm>!<token-name>— in this casepacker@pve!packer.
Add the variables file to .gitignore:
echo "variables.pkrvars.hcl" >> .gitignoreStep 8: Initialize and Validate
Initialize Packer to download the Proxmox plugin:
packer init ubuntu-2404.pkr.hclValidate the configuration — this catches syntax errors and invalid values before you run the build:
packer validate -var-file=variables.pkrvars.hcl ubuntu-2404.pkr.hclYou should see:
The configuration is valid.Step 9: Run the Build
packer build -var-file=variables.pkrvars.hcl ubuntu-2404.pkr.hclPacker will:
- Create a new VM in Proxmox with the specified ID and settings
- Boot the Ubuntu installer ISO
- Serve the
http/user-dataautoinstall config from a local HTTP server - Wait for Ubuntu to install and the VM to reboot
- Connect via SSH and run the provisioner shell scripts
- Clean up the image (reset cloud-init, remove SSH keys, clear history)
- Shut down the VM and convert it to a Proxmox template
The whole process takes around 10–15 minutes depending on your hardware and internet speed.
Successful output looks like:
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Creating VM
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Starting VM
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Waiting for SSH...
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Connected to SSH
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Provisioning...
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Stopping VM
==> ubuntu-2404-template.proxmox-iso.ubuntu-2404: Converting VM to template
Build 'ubuntu-2404-template.proxmox-iso.ubuntu-2404' finished after 12 minutes 34 seconds.Step 10: Clone the Template
Once the template is created, you can clone it from the Proxmox UI or via CLI:
# Clone template VM ID 9000 to a new VM ID 100
qm clone 9000 100 --name my-new-vm --full true
# Set cloud-init values on the clone
qm set 100 --ciuser ubuntu --cipassword yourpassword --ipconfig0 ip=dhcp
# Start the VM
qm start 100For a static IP on the clone:
qm set 100 \
--ciuser ubuntu \
--sshkeys ~/.ssh/id_rsa.pub \
--ipconfig0 ip=192.168.1.50/24,gw=192.168.1.1 \
--nameserver 1.1.1.1Useful Packer Commands
| Command | Description |
|---|---|
packer init <file> | Download required plugins |
packer validate -var-file=<vars> <file> | Validate config without building |
packer build -var-file=<vars> <file> | Run the full build |
packer build -debug <file> | Interactive step-by-step debug mode |
packer build -on-error=ask <file> | Pause on error instead of destroying the VM |
PACKER_LOG=1 packer build <file> | Enable verbose logging |
Common Issues and Fixes
Boot command not triggering autoinstall
The GRUB boot command is sensitive to timing. If the installer doesn’t pick up the autoinstall config, increase the boot_wait value:
boot_wait = "10s"SSH timeout — Packer can’t connect after install
Check that qemu-guest-agent is installed and the VM firewall isn’t blocking SSH. Also verify the password in user-data matches ssh_password in your variables.
TLS certificate error connecting to Proxmox API
Set insecure_skip_tls_verify = true if you’re using a self-signed certificate (the default for most Proxmox installs). For production, install a valid certificate and set it to false.
VM already exists with that ID
Either delete the existing VM in Proxmox first, or change the vm_id in your variables file. Packer will not overwrite an existing VM.
HTTP server not reachable from Proxmox
Your workstation’s HTTP server (default port 8802) must be reachable from the Proxmox host. If you’re behind a firewall, open that port or run Packer directly on a machine in the same network as Proxmox.
Summary
Here’s what you built:
- Created a Proxmox API token with the minimum required permissions
- Uploaded the Ubuntu 24.04 ISO to Proxmox storage
- Wrote a cloud-init autoinstall
user-datafor a fully unattended install - Defined the full VM spec in a Packer HCL file — hardware, network, disk, boot command
- Cleaned and generalized the image with shell provisioners
- Built and converted the VM to a reusable Proxmox template
- Cloned the template with custom cloud-init settings
Every time you need a fresh Ubuntu 24.04 base image, just run packer build again. You get an identical, clean, production-ready template in about 15 minutes — no clicking, no manual steps, no drift.
Need help setting up Packer, Proxmox, or your infrastructure automation pipeline? Get in touch — I’m happy to help.
Now let me save the file properly using the edit tool:- proxmox
- packer
- ubuntu
- linux
- infrastructure-as-code
- devops