How to Build Custom Windows Server Images with Packer on Windows Server 2025
Maintaining a library of consistent, pre-hardened server images dramatically reduces provisioning time and eliminates configuration drift between environments. HashiCorp Packer automates the process of building these “golden images” — fully patched, configured, and sysprepped virtual machine images that serve as the authoritative starting point for every new server deployment. On Windows Server 2025, Packer’s HCL2 template format, combined with its PowerShell provisioner and WinRM communicator, provides a reliable pipeline from ISO or base image to a deployment-ready VHDX or Azure Managed Image. This tutorial covers installing Packer, authoring an HCL2 template targeting both Hyper-V and Azure ARM builders, running Windows Update non-interactively during the build, and producing an artifact ready for your image library.
Prerequisites
- A Windows Server 2025 build host (physical or VM) with Hyper-V role enabled, or an Azure subscription for cloud builds.
- Packer 1.10 or later (HCL2 support).
- Internet access from the build host to download Windows Updates and Packer plugins.
- A Windows Server 2025 ISO (evaluation ISO available from Microsoft Evaluation Center).
- WinRM enabled on the template (handled by the autounattend.xml).
- PowerShell 5.1 or later on the build host.
- For Azure builds: Azure CLI authenticated (
az login) and a resource group pre-created.
Step 1: Install Packer
Packer can be installed via winget, Chocolatey, or by downloading the binary directly from HashiCorp. Using a package manager ensures future upgrades are a single command.
# Option A: winget (recommended — built into Windows 10/11 and Server 2025)
winget install --id HashiCorp.Packer --exact --accept-package-agreements
# Option B: Chocolatey
Set-ExecutionPolicy Bypass -Scope Process -Force
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
choco install packer -y
# Verify installation
packer version
# Expected output: Packer v1.11.x
# Add Packer to PATH permanently if winget did not (open a new shell after this)
$env:PATH += ";$env:ProgramFilesHashiCorpPacker"
[System.Environment]::SetEnvironmentVariable("PATH", $env:PATH, [System.EnvironmentVariableTarget]::Machine)
Step 2: Create the Directory Structure
New-Item -ItemType Directory -Path "C:Packerws2025-golden" -Force
New-Item -ItemType Directory -Path "C:Packerws2025-goldenhttp" -Force
New-Item -ItemType Directory -Path "C:Packerws2025-goldenscripts" -Force
Set-Location "C:Packerws2025-golden"
Place your autounattend.xml inside the http folder. Packer spins up a temporary HTTP server during Hyper-V builds so the VM can fetch the answer file during Windows Setup. The answer file should set a local administrator password, enable WinRM, and skip the OOBE screens.
Step 3: Write the HCL2 Packer Template
Create ws2025.pkr.hcl in the project root. The file defines variables, source blocks for Hyper-V and Azure, and a shared build block that runs provisioners against both builders.
# Create the template file
@'
packer {
required_plugins {
hyperv = {
version = ">= 1.1.3"
source = "github.com/hashicorp/hyperv"
}
azure = {
version = ">= 2.1.0"
source = "github.com/hashicorp/azure"
}
}
}
variable "winrm_password" {
type = string
sensitive = true
default = "P@cker2025!"
}
variable "azure_subscription_id" {
type = string
default = ""
}
variable "azure_resource_group" {
type = string
default = "rg-packer-images"
}
# ──────────────────────────────────────────────
# Hyper-V ISO builder
# ──────────────────────────────────────────────
source "hyperv-iso" "ws2025" {
iso_url = "C:/ISOs/WindowsServer2025.iso"
iso_checksum = "sha256:YOUR_ISO_SHA256_HASH_HERE"
vm_name = "ws2025-golden"
disk_size = 61440 # 60 GB in MB
ram_size = 4096
cpus = 4
generation = 2
enable_secure_boot = false # set true for production; needs signed bootloader
guest_additions_mode = "none"
http_directory = "http"
boot_wait = "5s"
boot_command = [""] # UEFI picks up autounattend.xml from HTTP
communicator = "winrm"
winrm_username = "Administrator"
winrm_password = var.winrm_password
winrm_use_ssl = false
winrm_insecure = true
winrm_timeout = "60m"
shutdown_command = "C:/Windows/System32/Sysprep/sysprep.exe /oobe /generalize /quiet /shutdown"
shutdown_timeout = "30m"
output_directory = "C:/Packer/output/ws2025-hyperv"
}
# ──────────────────────────────────────────────
# Azure ARM builder
# ──────────────────────────────────────────────
source "azure-arm" "ws2025" {
subscription_id = var.azure_subscription_id
managed_image_resource_group_name = var.azure_resource_group
managed_image_name = "ws2025-golden-{{isotime "2006-01-02"}}"
location = "eastus"
os_type = "Windows"
image_publisher = "MicrosoftWindowsServer"
image_offer = "WindowsServer"
image_sku = "2025-datacenter-azure-edition"
image_version = "latest"
vm_size = "Standard_D4s_v5"
communicator = "winrm"
winrm_use_ssl = true
winrm_insecure = true
winrm_timeout = "60m"
winrm_username = "packer"
winrm_password = var.winrm_password
azure_tags = {
environment = "golden"
os = "ws2025"
built_by = "packer"
}
}
# ──────────────────────────────────────────────
# Build block — shared provisioners
# ──────────────────────────────────────────────
build {
name = "ws2025-golden"
sources = [
"source.hyperv-iso.ws2025",
"source.azure-arm.ws2025"
]
# 1. Install all pending Windows Updates
provisioner "powershell" {
script = "scripts/windows-update.ps1"
}
# 2. Install required Windows features
provisioner "powershell" {
script = "scripts/install-features.ps1"
}
# 3. Apply baseline hardening (CIS or custom)
provisioner "powershell" {
script = "scripts/harden-baseline.ps1"
}
# 4. Run Sysprep (Azure builder handles this automatically; needed for Hyper-V)
provisioner "powershell" {
only = ["hyperv-iso.ws2025"]
inline = [
"Write-Host 'Sysprep will run via shutdown_command after provisioners complete.'"
]
}
}
'@ | Set-Content -Path "C:Packerws2025-goldenws2025.pkr.hcl" -Encoding UTF8
Step 4: Write the Provisioner Scripts
Create the three PowerShell scripts referenced in the build block. These run inside the VM being built.
# scripts/windows-update.ps1 — install Windows Updates non-interactively
@'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Install PSWindowsUpdate module if not present
if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Install-Module -Name PSWindowsUpdate -Force -Confirm:$false
}
Import-Module PSWindowsUpdate
# Install all available updates; reboot if needed and continue
Install-WindowsUpdate -AcceptAll -AutoReboot -IgnoreReboot -Confirm:$false
Write-Host "Windows Update complete."
'@ | Set-Content -Path "C:Packerws2025-goldenscriptswindows-update.ps1" -Encoding UTF8
# scripts/install-features.ps1 — install common roles/features for your org
@'
$features = @(
"NET-Framework-Core",
"NET-Framework-45-Core",
"RSAT-AD-PowerShell",
"Windows-Defender",
"PowerShell-V2"
)
foreach ($f in $features) {
Install-WindowsFeature -Name $f -IncludeManagementTools -ErrorAction SilentlyContinue
}
Write-Host "Feature installation complete."
'@ | Set-Content -Path "C:Packerws2025-goldenscriptsinstall-features.ps1" -Encoding UTF8
# scripts/harden-baseline.ps1 — apply security baseline settings
@'
# Disable SMBv1
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force
# Disable unnecessary services
$disableServices = @("XblGameSave","XboxNetApiSvc","WMPNetworkSvc","Fax")
foreach ($svc in $disableServices) {
$service = Get-Service -Name $svc -ErrorAction SilentlyContinue
if ($service) { Set-Service -Name $svc -StartupType Disabled }
}
# Enable audit policies
auditpol /set /subcategory:"Logon" /success:enable /failure:enable
auditpol /set /subcategory:"Account Logon" /success:enable /failure:enable
# Set minimum password length
net accounts /minpwlen:14 /maxpwage:90
Write-Host "Baseline hardening complete."
'@ | Set-Content -Path "C:Packerws2025-goldenscriptsharden-baseline.ps1" -Encoding UTF8
Step 5: Initialize and Build
Set-Location "C:Packerws2025-golden"
# Download required plugins declared in the packer block
packer init ws2025.pkr.hcl
# Validate template syntax before committing to a build
packer validate ws2025.pkr.hcl
# Build Hyper-V image only (set -only to target a specific source)
packer build -only="hyperv-iso.ws2025" `
-var "winrm_password=YourSecurePassword!" `
ws2025.pkr.hcl
# Build Azure image only
packer build -only="azure-arm.ws2025" `
-var "azure_subscription_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
-var "winrm_password=YourSecurePassword!" `
ws2025.pkr.hcl
# Build both simultaneously
packer build `
-var "azure_subscription_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
-var "winrm_password=YourSecurePassword!" `
ws2025.pkr.hcl
The Hyper-V build outputs a VHDX at C:Packeroutputws2025-hyperv. The Azure build registers a Managed Image in your resource group named ws2025-golden-YYYY-MM-DD, ready to be referenced in ARM templates or Terraform.
Step 6: Use the Golden Image
# Deploy a new VM from the Hyper-V VHDX
$vhdxPath = "C:Packeroutputws2025-hypervws2025-goldenVirtual Hard Disksws2025-golden.vhdx"
New-VM -Name "SRV-NEW01" -Generation 2 -MemoryStartupBytes 4GB `
-VHDPath $vhdxPath -SwitchName "External Switch"
Start-VM -Name "SRV-NEW01"
# In Azure — deploy from the managed image via Azure CLI
az vm create `
--resource-group "rg-production" `
--name "srv-new01" `
--image "/subscriptions/xxxx/resourceGroups/rg-packer-images/providers/Microsoft.Compute/images/ws2025-golden-2026-05-17" `
--admin-username "azureadmin" `
--generate-ssh-keys `
--size Standard_D4s_v5
Conclusion
HashiCorp Packer transforms the traditionally manual and error-prone process of building Windows Server images into a reproducible, version-controlled pipeline. By combining Packer’s HCL2 templates with PowerShell provisioners for Windows Update, feature installation, and security hardening, you produce a sysprepped golden image that is identical every time the build runs. Targeting both Hyper-V and Azure ARM from the same template ensures consistency across on-premises and cloud environments. Storing your Packer templates in source control and triggering builds in a CI/CD pipeline — such as Azure DevOps or GitHub Actions — gives you a fully automated image factory for Windows Server 2025, reducing new server provisioning time from hours to minutes while enforcing your organisation’s security baseline on every deployment.