· 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.

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 proxmox Packer plugin (installed automatically via packer 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 packer

Verify the installation:

packer --version

Step 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:

  1. Go to Datacenter → Permissions → Users → Add a user: packer@pve
  2. Go to Datacenter → Permissions → API Tokens → Add a token for packer@pve, name it packer — uncheck Privilege Separation so it inherits the user’s permissions
  3. Copy the token secret — it is only shown once

Now assign the required permissions. In Datacenter → Permissions → Add → User Permission:

PathUserRole
/packer@pvePVEVMAdmin
/storage/<your-storage>packer@pvePVEDatastoreAdmin

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 Packer

Step 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.iso

Or 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-template

The 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 empty

Step 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-data

Create 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: false

Important: Replace the password value 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-agent so Proxmox can communicate with the VM
  • Grants passwordless sudo to the ubuntu user
  • Removes SSH host keys so each clone generates its own
  • Clears machine-id so 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_id format is <user>@<realm>!<token-name> — in this case packer@pve!packer.

Add the variables file to .gitignore:

echo "variables.pkrvars.hcl" >> .gitignore

Step 8: Initialize and Validate

Initialize Packer to download the Proxmox plugin:

packer init ubuntu-2404.pkr.hcl

Validate the configuration — this catches syntax errors and invalid values before you run the build:

packer validate -var-file=variables.pkrvars.hcl ubuntu-2404.pkr.hcl

You should see:

The configuration is valid.

Step 9: Run the Build

packer build -var-file=variables.pkrvars.hcl ubuntu-2404.pkr.hcl

Packer will:

  1. Create a new VM in Proxmox with the specified ID and settings
  2. Boot the Ubuntu installer ISO
  3. Serve the http/user-data autoinstall config from a local HTTP server
  4. Wait for Ubuntu to install and the VM to reboot
  5. Connect via SSH and run the provisioner shell scripts
  6. Clean up the image (reset cloud-init, remove SSH keys, clear history)
  7. 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 100

For 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.1

Useful Packer Commands

CommandDescription
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:

  1. Created a Proxmox API token with the minimum required permissions
  2. Uploaded the Ubuntu 24.04 ISO to Proxmox storage
  3. Wrote a cloud-init autoinstall user-data for a fully unattended install
  4. Defined the full VM spec in a Packer HCL file — hardware, network, disk, boot command
  5. Cleaned and generalized the image with shell provisioners
  6. Built and converted the VM to a reusable Proxmox template
  7. 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
Share:
Back to Blog

Related Posts

View All Posts »
How to Install Docker on Ubuntu 24.04 LTS

How to Install Docker on Ubuntu 24.04 LTS

A complete step-by-step guide to installing Docker Engine on Ubuntu 24.04 LTS (Noble Numbat) — from adding the official repository to running your first container and setting up Docker Compose.