Windows Server 2025 on Proxmox with Packer: Fully Automated Template Build

📅 Published: October 29, 2026 ✍️ By Khurram Shahzad 🏷️ Proxmox VE · Packer · Windows Server 2025 · Infrastructure as Code ⏱️ 19 min read
Windows ISO + VirtIO + autounattend Packer build ✓ unattended✓ VirtIO + agent ✓ updates✓ sysprep Proxmox template ready to clone INFRASTRUCTURE AS CODE Windows Server 2025 Proxmox + Packer Fully automated template build autounattend VirtIO sysprep ☁ VMoreCloud Learn | Build | Share
Versions and scope (checked October 29, 2026): this guide uses the current boot_iso block syntax of the HashiCorp Proxmox plugin for Packer, targets Proxmox VE 8.x and 9.x, and builds a UEFI Windows Server 2025 template. Older tutorials use the removed top-level iso_file setting, so copy-pasting them into a recent plugin version fails validation. Test the whole build in a lab before you rely on it, and adjust names, IDs and storage to your environment.

Clicking through a Windows Server installation, installing the VirtIO drivers by hand and running Windows Update gets old quickly. Packer turns all of it into one repeatable command that ends with a patched, generalized template on Proxmox VE.

This guide builds that pipeline from scratch. Packer creates the VM, attaches your Windows ISO, the VirtIO driver ISO and a CD it generates on the fly containing the answer file, so you never have to remaster the Windows ISO. Windows installs unattended, a bootstrap script installs the VirtIO drivers and QEMU guest agent and enables WinRM, Packer runs Windows Update, and sysprep generalizes the image before Packer converts it to a template. Each piece is explained so you can adapt it to Server Core, Datacenter or your own baseline.

⚡ Quick Summary

  • Tooling: Packer with the proxmox-iso builder and the windows-update plugin, authenticating to Proxmox with an API token.
  • No ISO remastering: the answer file and bootstrap script are delivered on a CD that Packer generates (cd_content), and the VirtIO ISO is attached as a second CD.
  • Hardware: UEFI (OVMF), q35, VirtIO SCSI and network, and the QEMU guest agent, which Packer needs to discover the VM’s IP address.
  • Secrets stay out of Git: passwords are injected into the answer files by templatefile() from environment variables.
  • Clean output: sysprep generalizes the image and undoes the build-time WinRM settings on every clone.

How the pipeline works

#StageWhat happensCommon failure
1VM creationPacker creates a UEFI VM and attaches three CDs: Windows ISO, VirtIO ISO and a generated answer-file CDAPI token privileges, storage names
2BootPacker taps the space bar to answer “Press any key to boot from CD”Key press missed, so UEFI shell or no boot
3Windows SetupSetup finds autounattend.xml, loads the VirtIO storage driver, partitions the disk and installs the chosen imageWrong driver path, wrong image index
4First logonThe bootstrap script installs VirtIO tools and the guest agent, then enables WinRMGuest agent not running, no IP
5ProvisioningPacker connects over WinRM, runs Windows Update, cleans up and uploads the sysprep answer fileWinRM timeout, update loops
6GeneralizeThe shutdown command runs sysprep, which generalizes Windows and powers offSysprep fails or times out
7TemplatePacker converts the stopped VM to a Proxmox templateTask timeout

Prerequisites

  • Proxmox VE 8.x or 9.x, with ISO storage (for example local) and VM disk storage (for example local-lvm or ZFS).
  • A machine to run Packer with network access to the Proxmox API (port 8006) and to the VM while it builds. Packer starts a small HTTP server there for some workflows, and your firewall must allow the VM to reach it if you use one.
  • Packer 1.10 or newer. Packer is distributed under a source-available license, so check that it fits your policy.
  • An ISO-creation tool on the Packer machine. Generating the answer-file CD needs xorriso, mkisofs (or genisoimage), hdiutil on macOS, or oscdimg on Windows.
  • Windows Server 2025 ISO. The Microsoft Evaluation Center ISO is fine for a lab. Production images need your licensed media.
  • VirtIO driver ISO (virtio-win.iso), from the stable download location.
💡 Install the tools: on Debian or Ubuntu, sudo apt install xorriso covers the ISO tool. Install Packer from HashiCorp’s package repository or a binary download, then confirm with packer version.

Step 1: Proxmox API token and ISOs

Create a dedicated user and role for Packer instead of using root@pam. Packer then has only the permissions it needs to create VMs, upload the generated CD and convert the VM to a template.

# Run as root on the Proxmox node
pveum user add packer@pve --comment "Packer image builds"

pveum role add PackerBuild --privs "Datastore.AllocateSpace Datastore.AllocateTemplate Datastore.Audit Pool.Audit SDN.Use Sys.Audit Sys.Console VM.Allocate VM.Audit 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.Console VM.PowerMgmt"

pveum acl modify / --users packer@pve --roles PackerBuild

# Token without privilege separation: it inherits the user's permissions
pveum user token add packer@pve build --privsep 0
# Save the secret that is printed. It is shown only once.
# The token ID you will use in Packer is: packer@pve!build
⚠️ Privileges vary by Proxmox version. If pveum role add rejects a privilege name, remove it. For a quick lab you can use the built-in PVEAdmin role instead, and tighten later. Keep the token secret out of Git.

Next, put the ISOs on the Proxmox node and find your image index. The index chooses the edition inside the ISO, for example Standard or Datacenter, with or without Desktop Experience.

# On the Proxmox node: put both ISOs in the "local" ISO storage
cd /var/lib/vz/template/iso
wget -O virtio-win.iso https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso

# Upload your Windows Server 2025 ISO (UI: local > ISO Images > Upload), then verify it
sha256sum windows-server-2025.iso

# Find the image index you want inside the ISO
apt install -y wimtools
mkdir -p /mnt/win && mount -o loop,ro /var/lib/vz/template/iso/windows-server-2025.iso /mnt/win
wiminfo /mnt/win/sources/install.wim | grep -E "^(Index|Name|Description)"
umount /mnt/win

The wiminfo output lists each edition with its index. On typical Windows Server media the Desktop Experience editions are the second and fourth entries, but the order depends on your ISO, so always check it.

Step 2: Project layout and variables

packer-proxmox-ws2025/
├── ws2025.pkr.hcl                  # plugins, source, build
├── variables.pkr.hcl               # variable definitions
├── ws2025.pkrvars.hcl              # non-secret values (safe to commit)
├── answer/
│   ├── autounattend.pkrtpl.hcl     # Windows Setup answer file (templated)
│   └── sysprep-unattend.pkrtpl.hcl # answer file used by sysprep
├── scripts/
│   ├── bootstrap.ps1               # runs at first logon: drivers, agent, WinRM
│   └── cleanup.ps1                 # runs last: RDP, cleanup, TRIM
└── .gitignore                      # *.auto.pkrvars.hcl, packer.log, secrets

variables.pkr.hcl

# variables.pkr.hcl
variable "proxmox_url" {
  type        = string
  description = "Proxmox API endpoint, for example https://pve01.example.lan:8006/api2/json"
}
variable "proxmox_token_id" {
  type        = string
  description = "API token ID, for example packer@pve!build"
}
variable "proxmox_token_secret" {
  type      = string
  sensitive = true
}
variable "proxmox_node" {
  type = string
}
variable "insecure_tls" {
  type    = bool
  default = false
}

variable "iso_storage" {
  type    = string
  default = "local"
}
variable "vm_storage" {
  type    = string
  default = "local-lvm"
}
variable "bridge" {
  type    = string
  default = "vmbr0"
}
variable "template_vm_id" {
  type    = number
  default = 9025
}

variable "windows_iso" {
  type        = string
  description = "File name of the Windows Server 2025 ISO in the ISO storage"
}
variable "windows_iso_checksum" {
  type        = string
  default     = "none"
  description = "sha256:<hash> is recommended"
}
variable "virtio_iso" {
  type    = string
  default = "virtio-win.iso"
}
variable "image_index" {
  type        = number
  default     = 2
  description = "Image index inside install.wim (check with wiminfo or DISM)"
}

variable "admin_password" {
  type      = string
  sensitive = true
}

Values and secrets

Keep non-secret values in ws2025.pkrvars.hcl, and pass secrets through PKR_VAR_ environment variables so they never enter version control.

# ws2025.pkrvars.hcl (non-secret)
proxmox_url      = "https://pve01.example.lan:8006/api2/json"
proxmox_token_id = "packer@pve!build"
proxmox_node     = "pve01"
insecure_tls     = false   # set true only for a lab with a self-signed certificate

windows_iso          = "windows-server-2025.iso"
windows_iso_checksum = "sha256:REPLACE_WITH_YOUR_ISO_HASH"
image_index          = 2

iso_storage = "local"
vm_storage  = "local-lvm"
bridge      = "vmbr0"
# Linux or macOS
export PKR_VAR_proxmox_token_secret='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
export PKR_VAR_admin_password='UseAL0ng!Random-BuildPassw0rd'

# Windows PowerShell
$env:PKR_VAR_proxmox_token_secret = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
$env:PKR_VAR_admin_password       = 'UseAL0ng!Random-BuildPassw0rd' 
⚠️ Passwords end up in an answer file. Windows Setup needs the administrator password in plain text inside autounattend.xml, so use a long, random, build-only password and change it when you deploy clones (see the security checklist). Add *.auto.pkrvars.hcl, packer.log and any secrets file to .gitignore.

Step 3: The Windows answer file

The answer file drives all three setup passes. This version is written as a Packer template, so ${image_index} and ${admin_password} are filled in at build time by templatefile().

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend"
          xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">

  <!-- PASS 1: Windows PE. Load the VirtIO storage driver, partition the disk, install the image. -->
  <settings pass="windowsPE">
    <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <SetupUILanguage><UILanguage>en-US</UILanguage></SetupUILanguage>
      <InputLocale>en-US</InputLocale>
      <SystemLocale>en-US</SystemLocale>
      <UILanguage>en-US</UILanguage>
      <UserLocale>en-US</UserLocale>
    </component>

    <component name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <DriverPaths>
        <!-- The VirtIO ISO drive letter varies, so list the likely ones. Missing paths are ignored. -->
        <PathAndCredentials wcm:action="add" wcm:keyValue="1"><Path>D:\vioscsi\2k25\amd64</Path></PathAndCredentials>
        <PathAndCredentials wcm:action="add" wcm:keyValue="2"><Path>E:\vioscsi\2k25\amd64</Path></PathAndCredentials>
        <PathAndCredentials wcm:action="add" wcm:keyValue="3"><Path>F:\vioscsi\2k25\amd64</Path></PathAndCredentials>
        <PathAndCredentials wcm:action="add" wcm:keyValue="4"><Path>G:\vioscsi\2k25\amd64</Path></PathAndCredentials>
      </DriverPaths>
    </component>

    <component name="Microsoft-Windows-Setup" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <DiskConfiguration>
        <Disk wcm:action="add">
          <DiskID>0</DiskID>
          <WillWipeDisk>true</WillWipeDisk>
          <CreatePartitions>
            <CreatePartition wcm:action="add"><Order>1</Order><Type>EFI</Type><Size>300</Size></CreatePartition>
            <CreatePartition wcm:action="add"><Order>2</Order><Type>MSR</Type><Size>16</Size></CreatePartition>
            <CreatePartition wcm:action="add"><Order>3</Order><Type>Primary</Type><Extend>true</Extend></CreatePartition>
          </CreatePartitions>
          <ModifyPartitions>
            <ModifyPartition wcm:action="add"><Order>1</Order><PartitionID>1</PartitionID><Format>FAT32</Format><Label>System</Label></ModifyPartition>
            <ModifyPartition wcm:action="add"><Order>2</Order><PartitionID>2</PartitionID></ModifyPartition>
            <ModifyPartition wcm:action="add"><Order>3</Order><PartitionID>3</PartitionID><Format>NTFS</Format><Label>Windows</Label><Letter>C</Letter></ModifyPartition>
          </ModifyPartitions>
        </Disk>
      </DiskConfiguration>

      <ImageInstall>
        <OSImage>
          <InstallFrom>
            <MetaData wcm:action="add"><Key>/IMAGE/INDEX</Key><Value>${image_index}</Value></MetaData>
          </InstallFrom>
          <InstallTo><DiskID>0</DiskID><PartitionID>3</PartitionID></InstallTo>
          <WillShowUI>OnError</WillShowUI>
        </OSImage>
      </ImageInstall>

      <UserData>
        <AcceptEula>true</AcceptEula>
        <FullName>Administrator</FullName>
        <Organization>Lab</Organization>
        <!-- No ProductKey: fine for evaluation and volume-licensed media. Retail media needs a key. -->
      </UserData>
    </component>
  </settings>

  <!-- PASS 2: specialize -->
  <settings pass="specialize">
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <ComputerName>*</ComputerName>
      <TimeZone>UTC</TimeZone>
    </component>
  </settings>

  <!-- PASS 3: first boot. Create the admin password, auto-logon, and hand over to the bootstrap script. -->
  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <OOBE>
        <HideEULAPage>true</HideEULAPage>
        <HideLocalAccountScreen>true</HideLocalAccountScreen>
        <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
        <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <ProtectYourPC>3</ProtectYourPC>
        <NetworkLocation>Work</NetworkLocation>
      </OOBE>
      <UserAccounts>
        <AdministratorPassword><Value>${admin_password}</Value><PlainText>true</PlainText></AdministratorPassword>
      </UserAccounts>
      <AutoLogon>
        <Enabled>true</Enabled>
        <LogonCount>3</LogonCount>
        <Username>Administrator</Username>
        <Password><Value>${admin_password}</Value><PlainText>true</PlainText></Password>
      </AutoLogon>
      <FirstLogonCommands>
        <SynchronousCommand wcm:action="add">
          <Order>1</Order>
          <Description>Run the Packer bootstrap script from the answer-file CD</Description>
          <CommandLine>cmd /c "for %i in (D E F G H) do if exist %i:\bootstrap.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File %i:\bootstrap.ps1"</CommandLine>
        </SynchronousCommand>
      </FirstLogonCommands>
    </component>
  </settings>
</unattend>

Why it is written this way

  • Driver paths for D to G. Windows PE assigns drive letters to the three CDs in an order you do not control precisely, so the VirtIO storage driver path is listed under several letters. Without that driver Setup cannot see the VirtIO disk.
  • GPT layout for UEFI. EFI system, MSR and Windows partitions match the OVMF firmware used by the VM.
  • Image index, not image name. Names differ between evaluation and volume-license media, while the index is stable for a given ISO.
  • No product key. Evaluation and volume-licensed media accept this. For retail media, add a key from Microsoft’s KMS client setup keys page in the UserData section.
  • Bootstrap by searching drives. The first-logon command looks for bootstrap.ps1 on drives D to H, so it does not depend on one specific letter.

Step 4: Bootstrap and cleanup scripts

scripts/bootstrap.ps1

This runs once at first logon. Its order matters: drivers and the guest agent first (Packer needs the agent to learn the VM’s IP), then wait for a network address, then WinRM.

# scripts/bootstrap.ps1  (runs once, at first logon, during the Packer build)
$ErrorActionPreference = 'Stop'
Start-Transcript -Path 'C:\Windows\Temp\bootstrap.log' -Force

# 1. Install VirtIO drivers and the QEMU guest agent from the attached VirtIO ISO.
#    Packer needs the guest agent to learn the VM's IP address.
$tools = Get-PSDrive -PSProvider FileSystem |
    ForEach-Object { Join-Path $_.Root 'virtio-win-guest-tools.exe' } |
    Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $tools) { throw 'virtio-win-guest-tools.exe not found. Is the VirtIO ISO attached?' }

$p = Start-Process -FilePath $tools -ArgumentList '/install','/quiet','/norestart' -Wait -PassThru
if ($p.ExitCode -notin 0, 3010) { throw "VirtIO guest tools failed with exit code $($p.ExitCode)" }

# 2. Wait for the VirtIO network adapter to get an address.
$deadline = (Get-Date).AddMinutes(5)
do {
    Start-Sleep -Seconds 5
    $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
          Where-Object { $_.IPAddress -notlike '169.254.*' -and $_.InterfaceAlias -notlike 'Loopback*' }
} until ($ip -or (Get-Date) -gt $deadline)
if (-not $ip) { throw 'No IPv4 address after installing the VirtIO network driver.' }

# 3. WinRM so Packer can connect. This is for the build only and is locked down again by sysprep.
Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
Enable-PSRemoting -Force -SkipNetworkProfileCheck
Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value $true
Set-Item WSMan:\localhost\Service\Auth\Basic -Value $true
New-NetFirewallRule -Name 'WinRM-HTTP-Packer' -DisplayName 'WinRM HTTP (Packer build only)' `
    -Direction Inbound -Protocol TCP -LocalPort 5985 -Action Allow -Profile Any | Out-Null
Set-Service WinRM -StartupType Automatic
Restart-Service WinRM

Stop-Transcript

scripts/cleanup.ps1

This runs after the updates, just before sysprep.

# scripts/cleanup.ps1  (runs after all updates, before sysprep)
$ErrorActionPreference = 'Continue'

# Allow RDP for administrators
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop'

# Shrink the component store without /ResetBase, so updates stay uninstallable
Dism.exe /Online /Cleanup-Image /StartComponentCleanup

# Clear the Windows Update download cache and temp files
Stop-Service wuauserv -Force -ErrorAction SilentlyContinue
Remove-Item "$env:windir\SoftwareDistribution\Download\*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$env:TEMP\*" -Recurse -Force -ErrorAction SilentlyContinue

# Clear event logs so clones start clean
wevtutil el | ForEach-Object { wevtutil cl $_ 2>$null }

# Return free space to thin-provisioned storage (disk has discard enabled)
Optimize-Volume -DriveLetter C -ReTrim -Verbose

Step 5: The sysprep answer file

The shutdown command runs sysprep with this file. It does two jobs: it sets the post-generalize first-boot behavior, and it reverses the build-time WinRM shortcuts (Basic authentication, unencrypted traffic and the firewall rule) on every VM you clone.

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend"
          xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">

  <settings pass="specialize">
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <ComputerName>*</ComputerName>
      <TimeZone>UTC</TimeZone>
    </component>

    <!-- Undo the build-time WinRM shortcuts on every VM cloned from this template -->
    <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <RunSynchronous>
        <RunSynchronousCommand wcm:action="add">
          <Order>1</Order>
          <Description>Disable WinRM Basic auth</Description>
          <Path>reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" /v AllowBasic /t REG_DWORD /d 0 /f</Path>
        </RunSynchronousCommand>
        <RunSynchronousCommand wcm:action="add">
          <Order>2</Order>
          <Description>Disable unencrypted WinRM</Description>
          <Path>reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service" /v AllowUnencryptedTraffic /t REG_DWORD /d 0 /f</Path>
        </RunSynchronousCommand>
        <RunSynchronousCommand wcm:action="add">
          <Order>3</Order>
          <Description>Disable the build-only WinRM firewall rule</Description>
          <Path>netsh advfirewall firewall set rule name="WinRM HTTP (Packer build only)" new enable=no</Path>
        </RunSynchronousCommand>
      </RunSynchronous>
    </component>
  </settings>

  <settings pass="oobeSystem">
    <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
               publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
      <OOBE>
        <HideEULAPage>true</HideEULAPage>
        <HideLocalAccountScreen>true</HideLocalAccountScreen>
        <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
        <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
        <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
        <ProtectYourPC>3</ProtectYourPC>
      </OOBE>
      <UserAccounts>
        <AdministratorPassword><Value>${admin_password}</Value><PlainText>true</PlainText></AdministratorPassword>
      </UserAccounts>
    </component>
  </settings>
</unattend>
💡 Clones start in OOBE. After sysprep, the first boot of each clone runs the specialize and OOBE passes and applies the administrator password from this file. Plan to change that password on first use, or add an in-guest tool such as Cloudbase-Init for cloud-init style configuration.

Step 6: The Packer template

This is the heart of the build. Read the comments, because each block ties back to a design decision described later.

# ws2025.pkr.hcl
packer {
  required_version = ">= 1.10.0"
  required_plugins {
    proxmox = {
      version = ">= 1.2.0"
      source  = "github.com/hashicorp/proxmox"
    }
    windows-update = {
      version = ">= 0.16.8"
      source  = "github.com/rgl/windows-update"
    }
  }
}

locals {
  build_date = formatdate("YYYY-MM-DD", timestamp())
}

source "proxmox-iso" "ws2025" {
  # ---- Proxmox connection ----
  proxmox_url              = var.proxmox_url
  username                 = var.proxmox_token_id
  token                    = var.proxmox_token_secret
  insecure_skip_tls_verify = var.insecure_tls
  node                     = var.proxmox_node
  task_timeout             = "20m"

  # ---- Template identity ----
  vm_id                = var.template_vm_id
  vm_name              = "ws2025-std-build"
  template_name        = "tpl-ws2025-std"
  template_description = "Windows Server 2025 Standard (Desktop Experience), built ${local.build_date} with Packer"
  tags                 = "windows;ws2025;packer"

  # ---- Hardware ----
  os              = "win11"             # Proxmox OS type that also covers Windows Server 2022 and 2025
  bios            = "ovmf"              # UEFI
  machine         = "q35"
  cpu_type        = "host"              # use a baseline model such as x86-64-v2-AES if hosts differ
  cores           = 4
  memory          = 8192
  scsi_controller = "virtio-scsi-single"
  qemu_agent      = true

  efi_config {
    efi_storage_pool  = var.vm_storage
    efi_type          = "4m"
    pre_enrolled_keys = true
  }

  disks {
    type         = "scsi"
    disk_size    = "60G"
    storage_pool = var.vm_storage
    format       = "raw"
    io_thread    = true
    discard      = true
    cache_mode   = "none"
  }

  network_adapters {
    model    = "virtio"
    bridge   = var.bridge
    firewall = false
  }

  # ---- Install media: Windows ISO, VirtIO ISO, and a generated answer-file CD ----
  boot_iso {
    type         = "sata"
    index        = "0"
    iso_file     = "${var.iso_storage}:iso/${var.windows_iso}"
    iso_checksum = var.windows_iso_checksum
    unmount      = true
  }

  additional_iso_files {
    type         = "sata"
    index        = "1"
    iso_file     = "${var.iso_storage}:iso/${var.virtio_iso}"
    iso_checksum = "none"
    unmount      = true
  }

  additional_iso_files {
    type             = "sata"
    index            = "2"
    cd_label         = "UNATTEND"
    iso_storage_pool = var.iso_storage
    unmount          = true
    cd_content = {
      "autounattend.xml" = templatefile("${path.root}/answer/autounattend.pkrtpl.hcl", {
        admin_password = var.admin_password
        image_index    = var.image_index
      })
      "bootstrap.ps1" = file("${path.root}/scripts/bootstrap.ps1")
    }
  }

  # "Press any key to boot from CD or DVD" appears briefly on UEFI. Tap space a few times.
  boot_wait    = "3s"
  boot_command = ["<spacebar><wait2><spacebar><wait2><spacebar><wait2><spacebar>"]

  # ---- Connection after first logon ----
  communicator   = "winrm"
  winrm_username = "Administrator"
  winrm_password = var.admin_password
  winrm_timeout  = "2h"
  winrm_use_ssl  = false
  winrm_insecure = true

  # ---- Generalize and power off. Packer then converts the VM to a template. ----
  shutdown_command = "C:\\Windows\\System32\\Sysprep\\sysprep.exe /oobe /generalize /quiet /shutdown /unattend:C:\\Windows\\Temp\\sysprep-unattend.xml"
  shutdown_timeout = "30m"
}

build {
  name    = "ws2025"
  sources = ["source.proxmox-iso.ws2025"]

  # Two update passes catch cumulative updates that appear after the first reboot
  provisioner "windows-update" {
    search_criteria = "IsInstalled=0"
    filters = [
      "exclude:$_.Title -like '*Preview*'",
      "include:$true",
    ]
    update_limit = 100
  }

  provisioner "windows-update" {
    search_criteria = "IsInstalled=0"
    filters = [
      "exclude:$_.Title -like '*Preview*'",
      "include:$true",
    ]
    update_limit = 100
  }

  provisioner "powershell" {
    script = "${path.root}/scripts/cleanup.ps1"
  }

  provisioner "file" {
    content = templatefile("${path.root}/answer/sysprep-unattend.pkrtpl.hcl", {
      admin_password = var.admin_password
    })
    destination = "C:\\Windows\\Temp\\sysprep-unattend.xml"
  }
}

Step 7: Run the build

cd packer-proxmox-ws2025

packer init .                                   # downloads the Proxmox and windows-update plugins
packer fmt -recursive .
packer validate -var-file=ws2025.pkrvars.hcl .

PACKER_LOG=1 PACKER_LOG_PATH=packer.log \
  packer build -var-file=ws2025.pkrvars.hcl .

Watch the VM console in the Proxmox web interface for the first run. You should see, in order: the boot prompt being answered, Windows Setup running without prompts, a first logon that runs the bootstrap script, Packer connecting over WinRM, update reboots, then sysprep shutting the VM down. When Packer reports the build finished, a new template appears in the Proxmox tree.

💡 Expect it to take a while. A fresh Windows Server image can have many updates to install across several reboots, so the first build often takes well over an hour depending on hardware and network speed.

Step 8: Test a clone

# Full clone of the template (VM ID 9025) into a new VM 301
qm clone 9025 301 --name ws2025-test --full 1 --storage local-lvm
qm start 301

# The guest agent reports the IP once Windows has finished its first boot
qm guest cmd 301 network-get-interfaces
  1. Clone the template to a new VM ID and start it.
  2. Wait for OOBE to finish. The first boot of a generalized image takes a few minutes.
  3. Check the guest agent reports an IP, then connect with RDP or the Proxmox console using the administrator account.
  4. Confirm the lock-down: WinRM should refuse Basic authentication and unencrypted traffic, and the build-only firewall rule should be disabled.
  5. Check Windows Update reports the machine as current.

Design choices explained

ChoiceReason
Generated CD instead of a remastered ISOThe plugin can build an ISO from cd_content or cd_files and attach it. Your Windows ISO stays untouched, so you can swap in a new Microsoft ISO or verify its checksum without redoing any manual steps
UEFI (OVMF) with q35Matches how modern Windows Server is normally installed and supports Secure Boot capable firmware. The CDs use SATA, which works with q35 and keeps the drive order predictable
virtio-scsi-single with iothread and discardGood performance, one controller per disk for an I/O thread, and TRIM so thin storage can reclaim space
os = "win11"Proxmox’s recommended OS type setting for current Windows client and server releases
QEMU guest agentPacker uses it to learn the VM’s IP. Without it the build waits for an address that it never discovers
Two update passesSome cumulative updates only become available after the first set installs and the machine restarts
Sysprep through shutdown_commandPacker runs it as the last action and waits for the VM to power off, then converts the stopped VM to a template
Secrets through templatefile()The repository contains no passwords. Values arrive at build time from environment variables

Customizing the build

  • Server Core or Datacenter: change image_index to the matching entry from wiminfo. Remove the RDP lines in the cleanup script if you do not want them.
  • More VirtIO drivers: add paths for other drivers (for example the balloon driver) to the answer file, or rely on the guest tools installer, which installs the driver set.
  • Different hardware profile: change cores, memory and the disk size in the source block. Use a baseline CPU type if your cluster mixes CPU generations and you plan live migration.
  • TPM and Secure Boot: Windows Server 2025 does not require a TPM. If your security policy does, add a TPM state to the VM and keep pre-enrolled keys on.
  • Cloud-style configuration: add Cloudbase-Init to the image and enable Proxmox cloud-init support, if you want clones to receive hostnames and addresses automatically.
  • CI/CD: run packer build from a self-hosted CI runner on a schedule so you get a fresh patched template every month. Add a naming scheme with the build date so you can keep or prune old templates.
  • Build several images: turn the source into a variable-driven definition and add more source blocks for Core, Datacenter or Windows 11.

Troubleshooting

SymptomLikely cause and fix
Windows Setup shows “We couldn’t find any drives”The VirtIO storage driver was not loaded. Check the driver path folder name on your VirtIO ISO (for example vioscsi\2k25\amd64), and that the VirtIO ISO is attached
VM boots to the UEFI shell or a black screenThe “Press any key” prompt was missed. Increase boot_wait, or add more space presses in boot_command
Setup stops and asks which image to installWrong image_index, or a retail ISO that needs a product key. Re-check with wiminfo
Packer waits forever for an IP or WinRMThe guest agent or network driver was not installed. Open the VM console and read C:\Windows\Temp\bootstrap.log. Check the VirtIO ISO has virtio-win-guest-tools.exe
WinRM connects, then authentication failsThe password in the answer file does not match winrm_password, or it does not meet Windows complexity rules
Packer cannot upload the generated CDMissing Datastore.AllocateTemplate privilege on the ISO storage, or the storage does not allow ISO content
“No ISO tool found” when creating the CDInstall xorriso, mkisofs or genisoimage on the Packer machine
TLS certificate error to ProxmoxUse a trusted certificate, or set insecure_tls = true in a lab only
Update provisioner loops or times outRaise timeouts, check internet access from the VM, and limit update_limit. Unreachable update sources are a common cause
Sysprep fails or the VM never powers offRead C:\Windows\System32\Sysprep\Panther\setuperr.log. Common causes are installed apps that block generalize or a reboot pending from updates
Template shows no network on clonesConfirm the VirtIO network driver installed, and that the clone is attached to a bridge with DHCP

Security checklist

  • Least-privilege token. Use a dedicated user and role, and rotate the token if it ever appears in a log or ticket.
  • Secrets out of Git. Use environment variables or a secrets manager, and review packer.log before sharing it.
  • Build-only WinRM. Basic authentication and unencrypted WinRM exist only during the build. The sysprep file disables them on clones. Run the build on an isolated VLAN if you can.
  • Change the baked-in password when you deploy clones, using your normal secret process or a configuration tool.
  • Verify ISOs. Set windows_iso_checksum to a real SHA-256 value and download the VirtIO ISO only from the official source.
  • Patch regularly. Rebuild the template monthly so new VMs do not start months behind on updates.
  • Audit templates. Remove old templates you no longer support, and record the build date in the template description.

If you also manage Windows Server, Hyper-V and hybrid infrastructure, more practical guides are on VMoreCloud.

Frequently asked questions

Do I need to modify the Windows Server ISO for Packer on Proxmox?

No. The Proxmox plugin can generate a small CD from cd_content or cd_files and attach it alongside the original ISO and the VirtIO ISO. You only need an ISO creation tool on the Packer machine.

Why does Packer need the QEMU guest agent?

The plugin uses the guest agent to learn the VM’s IP address so it can open the WinRM connection. Without the agent, the build waits for an address it cannot discover.

How do I choose Standard, Datacenter or Core?

Set image_index to the index of that edition in install.wim, which you can list with wiminfo or DISM. The index differs between ISOs, so check yours.

Can I use an evaluation ISO?

Yes, for labs. Evaluation media installs without a product key. The resulting template is an evaluation image, so do not use it for production workloads without proper licensing.

Why is WinRM enabled with Basic authentication?

It simplifies the build, because Packer connects before any certificate or domain exists. The sysprep answer file turns it off again for every clone. Run builds on an isolated network.

How often should I rebuild the template?

Monthly is a practical default, aligned with Patch Tuesday, so new VMs begin close to fully patched. Automate it from a scheduled CI job.

Sources and further reading

This guide is based on HashiCorp, Proxmox and Microsoft documentation and community write-ups available on October 29, 2026. Plugin settings, Proxmox privileges and Windows media details change between releases, so confirm them against the documentation for your versions. Use evaluation media for labs only, and follow your licensing terms for production images.

Khurram Shahzad, Hybrid Cloud & Virtualization Engineer and author at VMoreCloud
👤 About the Author
Khurram Shahzad
Hybrid Cloud & Virtualization Engineer
Hybrid Cloud & Virtualization Engineer specializing in VMware (VCF, vSAN, Aria), Microsoft Azure, AWS, Windows Server, Microsoft 365, Entra ID, Intune, Linux, Nutanix, Proxmox, networking, backup & disaster recovery, Cisco and FortiGate. I share practical technical guides, infrastructure projects and insights to help IT professionals build and manage modern IT environments through VMoreCloud.
🖥️ VMware🔷 Azure☁️ AWS🪟 Windows Server🐧 Linux🗄️ Virtualization🛡️ Cybersecurity🔗 Networking💾 Backup & DR✖️ Nutanix🟠 Proxmox
📄
120+
Articles Published
👥
10K+
Readers
🎓
Learning & Sharing
A Stronger Tech Community
View all articles →
“Learn  |  Build  |  Share”
☁️ VMoreCloud|Tech Knowledge for a Better Tomorrow
IT  •  Cloud  •  Virtualization  •  Security  •  Open Source

Leave a Reply

Your email address will not be published. Required fields are marked *