TL;DR — Quick Summary

Packer automates machine image creation for AWS, Azure, Docker, and VMware. Learn HCL templates, builders, provisioners, and CI/CD image pipelines.

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

Feature Packer Dockerfile AWS Image Builder Ansible (images)
Multi-platform Yes (any cloud + local) Docker only AWS only Yes (with plugins)
Config Language HCL Dockerfile syntax JSON/YAML YAML
Layer Caching No (full rebuild) Yes (layers) Partial No
Build Speed Minutes Seconds-minutes Minutes Minutes
Provisioner Support Shell, Ansible, Chef, Puppet RUN commands SSM/AWSTOE Native
CI/CD Integration Excellent Excellent AWS-only Good
Immutable Output Yes (snapshot) Yes (image) Yes (AMI) Depends
Learning Curve Medium Low Medium Medium

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

Problem Cause Solution
“Timeout waiting for SSH” Security group blocks SSH or instance slow to boot Check SG allows port 22 from builder IP; increase ssh_timeout
“AccessDenied” during AMI creation IAM policy missing ec2:CreateImage Add required EC2 and EBS permissions to IAM role
Provisioner scripts fail Script runs before packages update Add apt-get update as first step; use DEBIAN_FRONTEND=noninteractive
Build succeeds but AMI missing Wrong region or account Check region in source block; use aws ec2 describe-images to verify
Packer build slow No parallel builds configured Use -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