Windows Server 2025 on Proxmox with Packer: Fully Automated Template Build
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-isobuilder and thewindows-updateplugin, 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
- Prerequisites
- Step 1: Proxmox API token and ISOs
- Step 2: Project layout and variables
- Step 3: The Windows answer file
- Step 4: Bootstrap and cleanup scripts
- Step 5: The sysprep answer file
- Step 6: The Packer template
- Step 7: Run the build
- Step 8: Test a clone
- Design choices explained
- Customizing the build
- Troubleshooting
- Security checklist
- FAQ
- Sources
How the pipeline works
| # | Stage | What happens | Common failure |
|---|---|---|---|
| 1 | VM creation | Packer creates a UEFI VM and attaches three CDs: Windows ISO, VirtIO ISO and a generated answer-file CD | API token privileges, storage names |
| 2 | Boot | Packer taps the space bar to answer “Press any key to boot from CD” | Key press missed, so UEFI shell or no boot |
| 3 | Windows Setup | Setup finds autounattend.xml, loads the VirtIO storage driver, partitions the disk and installs the chosen image | Wrong driver path, wrong image index |
| 4 | First logon | The bootstrap script installs VirtIO tools and the guest agent, then enables WinRM | Guest agent not running, no IP |
| 5 | Provisioning | Packer connects over WinRM, runs Windows Update, cleans up and uploads the sysprep answer file | WinRM timeout, update loops |
| 6 | Generalize | The shutdown command runs sysprep, which generalizes Windows and powers off | Sysprep fails or times out |
| 7 | Template | Packer converts the stopped VM to a Proxmox template | Task timeout |
Prerequisites
- Proxmox VE 8.x or 9.x, with ISO storage (for example
local) and VM disk storage (for examplelocal-lvmor 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(orgenisoimage),hdiutilon macOS, oroscdimgon 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.
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
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'
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
UserDatasection. - Bootstrap by searching drives. The first-logon command looks for
bootstrap.ps1on 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>
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.
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
- Clone the template to a new VM ID and start it.
- Wait for OOBE to finish. The first boot of a generalized image takes a few minutes.
- Check the guest agent reports an IP, then connect with RDP or the Proxmox console using the administrator account.
- Confirm the lock-down: WinRM should refuse Basic authentication and unencrypted traffic, and the build-only firewall rule should be disabled.
- Check Windows Update reports the machine as current.
Design choices explained
| Choice | Reason |
|---|---|
| Generated CD instead of a remastered ISO | The 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 q35 | Matches 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 discard | Good 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 agent | Packer uses it to learn the VM’s IP. Without it the build waits for an address that it never discovers |
| Two update passes | Some cumulative updates only become available after the first set installs and the machine restarts |
Sysprep through shutdown_command | Packer 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_indexto the matching entry fromwiminfo. 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 buildfrom 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
sourceblocks for Core, Datacenter or Windows 11.
Troubleshooting
| Symptom | Likely 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 screen | The “Press any key” prompt was missed. Increase boot_wait, or add more space presses in boot_command |
| Setup stops and asks which image to install | Wrong image_index, or a retail ISO that needs a product key. Re-check with wiminfo |
| Packer waits forever for an IP or WinRM | The 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 fails | The password in the answer file does not match winrm_password, or it does not meet Windows complexity rules |
| Packer cannot upload the generated CD | Missing Datastore.AllocateTemplate privilege on the ISO storage, or the storage does not allow ISO content |
| “No ISO tool found” when creating the CD | Install xorriso, mkisofs or genisoimage on the Packer machine |
| TLS certificate error to Proxmox | Use a trusted certificate, or set insecure_tls = true in a lab only |
| Update provisioner loops or times out | Raise 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 off | Read 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 clones | Confirm 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.logbefore 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_checksumto 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
- HashiCorp: Packer Proxmox ISO builder documentation
- GitHub: hashicorp/packer-plugin-proxmox
- GitHub: rgl/packer-plugin-windows-update
- Proxmox wiki: Windows VirtIO drivers
- Proxmox wiki: Windows guest best practices
- Microsoft Learn: Unattended Windows Setup reference
- Microsoft Learn: Sysprep (generalize) a Windows installation
- Microsoft Learn: KMS client activation keys
- ITSpyWorld: an earlier Windows Server 2025 Proxmox Packer build using a modified ISO
- Michael Waterman: Building secure Windows images with Packer on Proxmox
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.
