Packer is an open-source tool by HashiCorp that automates the creation of machine images for multiple platforms from a single source template. Instead of manually configuring a server, taking a snapshot, and hoping you documented every step, Packer codifies the entire process. You write a template, run packer build, and get identical images for AWS, Azure, Docker, and VMware — all from one definition.

This guide covers Packer installation, HCL template syntax, builders, provisioners, multi-platform builds, and integrating Packer into CI/CD pipelines for automated image delivery.

Prerequisites

  • Packer installed (v1.9+)
  • An account on at least one target platform (AWS, Azure, Docker, or VirtualBox)
  • AWS CLI configured (if building AMIs)
  • Basic understanding of machine images and server provisioning
  • Familiarity with HCL syntax (similar to Terraform)

Installing Packer

Install from the HashiCorp repository for automatic updates:

# Ubuntu/Debian
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 update && sudo apt install packer

# macOS
brew install packer

# Verify installation
packer version

Your First Packer Template

Packer uses HCL (HashiCorp Configuration Language) templates. Here is a minimal example that builds a Docker image:

# docker-nginx.pkr.hcl

packer {
  required_plugins {
    docker = {
      version = ">= 1.0.0"
      source  = "github.com/hashicorp/docker"
    }
  }
}

source "docker" "nginx" {
  image  = "ubuntu:22.04"
  commit = true
}

build {
  name = "custom-nginx"

  sources = ["source.docker.nginx"]

  provisioner "shell" {
    inline = [
      "apt-get update",
      "apt-get install -y nginx curl",
      "rm -rf /var/lib/apt/lists/*"
    ]
  }

  post-processor "docker-tag" {
    repository = "myorg/custom-nginx"
    tags       = ["latest", "1.0"]
  }
}

Build it:

# Initialize plugins
packer init docker-nginx.pkr.hcl

# Validate the template
packer validate docker-nginx.pkr.hcl

# Build the image
packer build docker-nginx.pkr.hcl

Building AWS AMIs with Packer

The most common Packer use case is building Amazon Machine Images (AMIs):

# aws-webserver.pkr.hcl

packer {
  required_plugins {
    amazon = {
      version = ">= 1.2.0"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "app_version" {
  type    = string
  default = "1.0.0"
}

source "amazon-ebs" "webserver" {
  ami_name      = "webserver-{{timestamp}}"
  instance_type = "t3.micro"
  region        = var.aws_region

  source_ami_filter {
    filters = {
      name                = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    owners      = ["099720109477"]  # Canonical
    most_recent = true
  }

  ssh_username = "ubuntu"

  tags = {
    Name        = "WebServer"
    Version     = var.app_version
    Builder     = "Packer"
    Environment = "production"
  }
}

build {
  sources = ["source.amazon-ebs.webserver"]

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx nodejs npm certbot",
      "sudo systemctl enable nginx"
    ]
  }

  provisioner "file" {
    source      = "config/nginx.conf"
    destination = "/tmp/nginx.conf"
  }

  provisioner "shell" {
    inline = [
      "sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf",
      "sudo nginx -t",
      "sudo systemctl restart nginx"
    ]
  }
}
# Build with variable overrides
packer build -var "aws_region=eu-west-1" -var "app_version=2.1.0" aws-webserver.pkr.hcl

Provisioner Types

Packer supports multiple provisioners that run inside the temporary build instance:

Shell Provisioner

provisioner "shell" {
  scripts = [
    "scripts/base-setup.sh",
    "scripts/install-app.sh",
    "scripts/harden.sh"
  ]
  environment_vars = [
    "APP_ENV=production",
    "DEBIAN_FRONTEND=noninteractive"
  ]
}

Ansible Provisioner

provisioner "ansible" {
  playbook_file = "ansible/playbook.yml"
  extra_arguments = [
    "--extra-vars", "app_version=${var.app_version}",
    "--tags", "setup,deploy"
  ]
  ansible_env_vars = [
    "ANSIBLE_HOST_KEY_CHECKING=False"
  ]
}

File Provisioner

provisioner "file" {
  source      = "app/dist/"
  destination = "/opt/app/"
}

Real-world scenario: Your company runs 50 EC2 instances behind a load balancer. Each time you deploy, you build a new AMI with Packer containing the latest application code, Nginx configuration, and security patches. Terraform then performs a rolling update — launching new instances with the fresh AMI and draining old ones. If the new image has issues, you roll back to the previous AMI in minutes. No SSH-ing into servers, no configuration drift, no snowflake servers.

Multi-Platform Builds

Build images for multiple platforms from one template:

source "amazon-ebs" "aws" {
  ami_name      = "myapp-aws-{{timestamp}}"
  instance_type = "t3.micro"
  region        = "us-east-1"
  source_ami_filter {
    filters = { name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*" }
    owners  = ["099720109477"]
    most_recent = true
  }
  ssh_username = "ubuntu"
}

source "docker" "container" {
  image  = "ubuntu:22.04"
  commit = true
}

source "virtualbox-iso" "local" {
  iso_url      = "https://releases.ubuntu.com/22.04/ubuntu-22.04.3-live-server-amd64.iso"
  iso_checksum = "sha256:abcdef..."
  ssh_username = "vagrant"
  ssh_password = "vagrant"
  # ... additional VirtualBox config
}

build {
  sources = [
    "source.amazon-ebs.aws",
    "source.docker.container",
  ]

  provisioner "shell" {
    inline = [
      "sudo apt-get update",
      "sudo apt-get install -y nginx",
    ]
  }
}

Run packer build . and Packer builds both images in parallel.

Packer vs Alternatives Comparison

FeaturePackerDockerfileAWS Image BuilderAnsible (images)
Multi-platformYes (any cloud + local)Docker onlyAWS onlyYes (with plugins)
Config LanguageHCLDockerfile syntaxJSON/YAMLYAML
Layer CachingNo (full rebuild)Yes (layers)PartialNo
Build SpeedMinutesSeconds-minutesMinutesMinutes
Provisioner SupportShell, Ansible, Chef, PuppetRUN commandsSSM/AWSTOENative
CI/CD IntegrationExcellentExcellentAWS-onlyGood
Immutable OutputYes (snapshot)Yes (image)Yes (AMI)Depends
Learning CurveMediumLowMediumMedium

When to choose Packer: Multi-cloud environments, VM-based deployments, golden image pipelines, or when you need the same image on AWS and VMware simultaneously.

Gotchas and Edge Cases

  • Stale source AMI filters: If your source_ami_filter matches multiple AMIs, most_recent = true ensures you get the latest. Without it, Packer picks arbitrarily and your builds become non-deterministic.

  • Build timeouts: Packer waits for SSH connectivity after launching the build instance. Slow cloud regions or restrictive security groups cause timeouts. Increase ssh_timeout from the default 5 minutes if needed.

  • Provisioner ordering matters: Provisioners run in the order they appear in the template. A file provisioner that copies to /opt/app/ will fail if the directory does not exist yet — add a shell provisioner first to create it.

  • IAM permissions for AWS builds: Packer needs ec2:RunInstances, ec2:CreateImage, ec2:DescribeImages, and several more. Use the minimal Packer IAM policy rather than giving it full admin access.

  • Deregistering old AMIs: Packer creates new AMIs but never deletes old ones. Without cleanup, you accumulate hundreds of unused AMIs. Use a lifecycle policy or a scheduled script to deregister images older than N days.

  • Disk space during builds: Temporary build instances use the default root volume size. If your provisioning installs many packages, the disk fills up. Add launch_block_device_mappings to increase the root volume.

Troubleshooting

ProblemCauseSolution
”Timeout waiting for SSH”Security group blocks SSH or instance slow to bootCheck SG allows port 22 from builder IP; increase ssh_timeout
”AccessDenied” during AMI creationIAM policy missing ec2:CreateImageAdd required EC2 and EBS permissions to IAM role
Provisioner scripts failScript runs before packages updateAdd apt-get update as first step; use DEBIAN_FRONTEND=noninteractive
Build succeeds but AMI missingWrong region or accountCheck region in source block; use aws ec2 describe-images to verify
Packer build slowNo parallel builds configuredUse -parallel-builds=N or separate source blocks for parallelism

Summary

  • Packer automates machine image creation from code, eliminating manual server configuration and ensuring reproducible builds
  • HCL templates define sources (where to build), provisioners (what to install), and post-processors (what to do with the output)
  • Multi-platform builds produce identical images for AWS, Docker, and VMware from a single template
  • Ansible provisioner integrates existing configuration management into Packer builds for complex setups
  • CI/CD integration enables automatic image builds on every infrastructure change, creating an immutable deployment pipeline
  • Pair with Terraform for a complete workflow — Packer builds the image, Terraform deploys it