Packer Runbook & Cheatsheet
Packer Runbook & Cheatsheet
Packer builds identical machine images for many platforms from one source template — the foundation of immutable infrastructure. Part one takes you from install to a built, provisioned image; part two is the reference: the CLI, template blocks, builders, provisioners, and post-processors.
Concept & target
Packer bakes a configured image once, so every machine boots identical. Pick the builder for where the image will run before you write anything.
| Target | Builder / plugin |
|---|---|
| Local containers (easiest to learn) | docker |
| Homelab VMs | qemu, virtualbox-iso, vmware/vsphere-iso, proxmox |
| AWS / Azure / GCP | amazon-ebs, azure-arm, googlecompute |
| No real build (test provisioners) | null |
Three moving parts A source (builder) creates a blank machine, provisioners configure it, and post-processors package the result. This runbook uses docker for a first build you can run anywhere, then shows how to swap the source for a VM or cloud target.
Install
# Debian / Ubuntu wget -O- https://apt.releases.hashicorp.com/gpg | \ sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [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 # macOS: brew tap hashicorp/tap && brew install hashicorp/tap/packer # Windows: choco install packer (or winget install HashiCorp.Packer) packer version
Write a template
An HCL2 template names its plugins, defines a source, and wires a build. Files end in .pkr.hcl.
# Declare the plugin so 'packer init' can fetch it packer { required_plugins { docker = { version = ">= 1.0.0" source = "github.com/hashicorp/docker" } } } # SOURCE = the builder: a blank machine to configure source "docker" "ubuntu" { image = "ubuntu:24.04" commit = true } # BUILD = source + provisioners + post-processors build { name = "bubim-image" sources = ["source.docker.ubuntu"] provisioner "shell" { inline = [ "apt-get update", "apt-get install -y curl vim ca-certificates", ] } post-processor "docker-tag" { repository = "bubim/ubuntu" tags = ["latest"] } }
Swap the target To build a VM or cloud image instead, replace the source block (and its plugin) — e.g. source “qemu” “…” or source “amazon-ebs” “…”. The build block and provisioners stay the same.
Init, format, validate
packer init . # download plugins from required_plugins packer fmt . # canonical formatting packer validate . # check syntax + config before building
init first, always packer init reads the required_plugins block and installs matching binaries. It only works on HCL2 templates; run it before validate or build on any new or cloned template.
Build
packer build . # Build just one source when a template has several packer build -only='bubim-image.docker.ubuntu' . # Keep the machine on failure so you can inspect it packer build -on-error=ask .
Credentials Cloud and hypervisor builders need auth (env vars, a credentials file, an API token). Keep those out of the template — supply them via environment (see Phase 6), never commit them.
Provision
Provisioners run inside the temporary machine to install and configure. They execute top to bottom.
build {
sources = ["source.docker.ubuntu"]
# Copy a file in
provisioner "file" {
source = "files/motd"
destination = "/etc/motd"
}
# Run a script from disk
provisioner "shell" {
script = "scripts/harden.sh"
}
# Hand off to a config manager (ties into your Salt sheet)
provisioner "salt-masterless" {
local_state_tree = "salt/roots"
}
}Bake, do not boot-configure Put slow, stable setup (packages, hardening, agents) into the image with provisioners; leave per-host, per-boot config to your config manager or cloud-init. That split is the whole point of immutable images.
Variables & secrets
variable "image_tag" {
type = string
default = "latest"
}
variable "registry_user" {
type = string
sensitive = true # kept out of logs
default = env("REGISTRY_USER")
}
locals {
full_tag = "bubim/ubuntu:${var.image_tag}"
}# Supply values, in increasing precedence packer build -var 'image_tag=2026.07' . packer build -var-file='prod.pkrvars.hcl' . export PKR_VAR_image_tag=2026.07 && packer build . # Any *.auto.pkrvars.hcl in the dir is loaded automatically
Secrets via env Mark sensitive variables sensitive = true and feed them from the environment (PKR_VAR_* or env()), so tokens never land in the template, a var-file, or the build log.
Post-process & ship
build {
sources = ["source.docker.ubuntu"]
# Record every artifact to a JSON file (great for CI)
post-processor "manifest" {
output = "manifest.json"
}
# Tag then push to a registry
post-processors {
post-processor "docker-tag" {
repository = "bubim/ubuntu"
tags = ["latest", "2026.07"]
}
post-processor "docker-push" {}
}
}Chain vs parallel A single post-processor block runs independently; wrapping several in a post-processors (plural) block chains them, feeding each artifact into the next — which is how tag-then-push works.
Operate
# CI gate: fail the pipeline on unformatted or invalid templates packer fmt -check . && packer validate . # Manage plugins explicitly packer plugins installed packer init -upgrade . # bump within version constraints # Convert an old JSON template to HCL2 packer hcl2_upgrade old-template.json
Pin for reproducibility Constrain plugin versions in required_plugins (e.g. version = “~> 1.0”) and a core version with required_version, so a build in six months uses the same toolchain.
Command line
| Command | Does |
|---|---|
| packer init DIR | Install plugins from required_plugins (HCL2 only). |
| packer fmt DIR | Canonical formatting; -check for CI. |
| packer validate DIR | Syntax + config check without building. |
| packer build DIR | Run the build(s). |
| packer build -only / -except | Include / exclude specific sources. |
| packer build -var / -var-file | Pass variables inline or from a file. |
| packer build -on-error=ask | cleanup | abort | ask | run-cleanup-provisioner on failure. |
| packer build -debug | Step through the build interactively. |
| packer inspect DIR | Show variables, sources, and build steps. |
| packer console DIR | REPL for testing expressions and functions. |
| packer plugins installed / install | List / install plugins manually. |
| packer hcl2_upgrade FILE | Convert a legacy JSON template to HCL2. |
Template blocks
| Block | Role |
|---|---|
| packer { } | Core + plugin requirements (required_version, required_plugins). |
| variable “x” { } | Typed input; default, sensitive, validation. |
| locals { } | Computed values reused across the template. |
| source “type” “name” { } | A builder configuration (the blank machine). |
| build { } | Ties sources to provisioners and post-processors. |
| provisioner “type” { } | Configures the machine during the build. |
| post-processor “type” { } | Packages / publishes the artifact. |
| data “type” “name” { } | Look up external values at build time. |
Common builders
| Source type | Produces |
|---|---|
| docker | Container images (commit or export). |
| amazon-ebs | AWS AMIs. |
| azure-arm | Azure managed images. |
| googlecompute | GCP images. |
| qemu | KVM/QEMU disk images (qcow2) — homelab-friendly. |
| virtualbox-iso | VirtualBox VMs / OVA. |
| vmware-iso, vsphere-iso | VMware / vSphere VMs and templates. |
| proxmox-iso, proxmox-clone | Proxmox VE templates. |
| null | No machine; run provisioners against an existing target. |
Each is a plugin Declare it in required_plugins with its source (e.g. github.com/hashicorp/qemu) and let packer init fetch it.
Provisioners
| Provisioner | Does |
|---|---|
| shell | Run inline commands or a script (Linux/Unix). |
| file | Upload files/dirs into the machine. |
| ansible / ansible-local | Run a playbook from the host / inside the guest. |
| salt-masterless | Apply a Salt state tree with no master. |
| powershell / windows-shell | Windows scripting. |
| windows-restart | Reboot and wait (Windows). |
| breakpoint | Pause the build for debugging. |
Post-processors
| Post-processor | Does |
|---|---|
| manifest | Write build artifacts to JSON. |
| vagrant | Package as a Vagrant box. |
| compress | Gzip/zip the artifact. |
| checksum | Emit checksum files. |
| docker-tag / docker-push | Tag and publish a container image. |
| shell-local | Run a command on the build host afterward. |
Variables & precedence
| Source | Notes (lowest to highest precedence) |
|---|---|
| default in variable { } | Fallback value. |
| *.auto.pkrvars.hcl | Auto-loaded from the working directory. |
| PKR_VAR_name | Environment variable. |
| -var-file=FILE | Explicit var file. |
| -var ‘name=value’ | Highest precedence, per invocation. |
Sensitive Set sensitive = true to redact a value from output; combine with env() to keep secrets out of files entirely.
Optimize
| Area | Lever |
|---|---|
| Fast feedback | fmt -check + validate before every build; run both in CI. |
| Reproducibility | Pin plugin versions and required_version so builds are deterministic. |
| Scope | -only / -except to build a subset; parallel sources build at once. |
| Debug | -on-error=ask or -debug to inspect a failing machine instead of losing it. |
| Slim images | Clean package caches and temp files in a final provisioner; smaller images boot faster. |
| Layering | Bake stable setup into the image; leave per-host config to cloud-init / config management. |
| Secrets | Feed via PKR_VAR_* / env() with sensitive; never commit them. |
| Traceability | Emit a manifest and checksum so CI knows exactly what it produced. |
References
| Resource | Use | Link |
|---|---|---|
| Packer docs | Templates, commands, blocks | developer.hashicorp.com/packer |
| Plugin registry | Builders, provisioners, sources | packer/integrations |
| hcl2_upgrade | JSON to HCL2 migration | HCL2 upgrade tutorial |
| packer init | Plugin install workflow | init command reference |