Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Packer: Build Machine Images

By Kokil Thapa | Last reviewed: August 2026

Manually configuring servers after deployment is a primary source of drift, security gaps, and debugging nightmares in production web systems. Using Packer: Build Machine Images solves this by baking your application dependencies, security patches, and configuration into an immutable artifact before a single instance launches. This shifts setup time from minutes-per-deploy to zero, ensuring every environment from local development to production runs identical software stacks.

What Is Packer and Why Use It to Build Machine Images?

HashiCorp Packer is an open-source tool that automates the creation of machine images for multiple platforms from a single source configuration. Unlike configuration management tools that run on live servers, Packer operates during the build phase. For full-stack developers managing Laravel applications or legal-tech portals, this distinction matters because it separates "what the server needs" from "when the server starts." If you are exploring broader automation strategies, understanding DevOps automation for website reliability provides essential context for where image building fits.

In practice, Packer uses a declarative HashiCorp Configuration Language (HCL) format. You define a source block for the target platform (AWS EC2, DigitalOcean Droplet, Azure VM) and a build block containing provisioners. When you run packer build, it spins up a temporary instance, executes your provisioning scripts, validates the result, snapshots it into an image, and terminates the temporary resource. The output is a static artifact ID you reference in Terraform, Ansible, or cloud auto-scaling groups.

HCL TemplateSource + BuildVariablesBuilderLaunch Temp VMConnect SSH/WinRMProvisionerShell / AnsibleInstall & ConfigValidatorTest ServicesCheck PortsArtifactAMI / SnapshotImage ID
Packer build machine images pipeline: HCL definition triggers builder, provisioner configures temp VM, validator checks health, then produces immutable artifact

This workflow eliminates the "works on my machine" problem at the infrastructure level. When I deploy sister sites like notarykathmandu.com or translationnepal.com on shared EC2 infrastructure, they all boot from the same Packer-built AMI. This guarantees PHP versions, Nginx configs, and SSL libraries match exactly, reducing debugging time significantly compared to post-boot configuration.

How Do You Write HCL Templates for Packer Build Machine Images?

Modern Packer (v1.10+ in 2026) uses HCL2 as the default format. Legacy JSON templates still work but lack variable interpolation, modular blocks, and readability. A minimal viable template requires three components: a variable block for secrets, a source block defining the platform, and a build block orchestrating provisioners.

Defining Variables and Sources

Never hardcode API keys or region-specific values. Use variables with environment variable fallbacks for CI/CD compatibility:

variable "aws_access_key" {
  type    = string
  default = env("AWS_ACCESS_KEY_ID")
}

variable "aws_secret_key" {
  type      = string
  sensitive = true
  default   = env("AWS_SECRET_ACCESS_KEY")
}

source "amazon-ebs" "laravel_app" {
  access_key    = var.aws_access_key
  secret_key    = var.aws_secret_key
  region        = "ap-south-1"
  source_ami    = "ami-0c55b159cbfafe1f0" 
  instance_type = "t3.medium"
  ssh_username  = "ubuntu"
  
  ami_name      = "laravel-prod-${formatdate("YYYY-MM-DD-hhmm", timestamp())}"
  ami_description = "Ubuntu 24.04 + PHP 8.4 + Nginx for Laravel 12"
  
  tags = {
    Environment = "production"
    ManagedBy   = "packer"
    BaseOS      = "ubuntu-24.04"
  }
}

Structuring the Build Block

The build block references sources and chains provisioners sequentially. Order matters: system updates first, then language runtimes, then application-specific configs:

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

  provisioner "shell" {
    execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'"
    script          = "scripts/system-update.sh"
  }

  provisioner "shell" {
    environment_vars = [
      "PHP_VERSION=8.4",
      "NODE_VERSION=22"
    ]
    script = "scripts/install-runtime.sh"
  }

  provisioner "ansible" {
    playbook_file = "./ansible/laravel-hardening.yml"
    extra_arguments = [
      "--extra-vars", "deploy_user=www-data"
    ]
  }

  post-processor "manifest" {
    output     = "manifest.json"
    strip_path = true
  }
}

On real client projects, I separate provisioners into distinct scripts rather than one monolithic file. This makes debugging faster when a build fails at step 3 of 7—you know immediately whether it’s a system update issue or an application config problem. For teams integrating this with Laravel deployments, reviewing CI/CD pipeline setup practices helps align Packer builds with application release cycles.

variables.pkr.hclvariable "aws_access_key"env() fallbackvariable "php_version"default = "8.4"variable "region"ap-south-1sources.pkr.hclsource "amazon-ebs"instance_typesource_amisource "digitalocean"size = "s-2vcpu-4gb"image = "ubuntu-24-04"build.pkr.hclsources = [...]provisioner "shell"provisioner "ansible"post-processor
Modular HCL structure separating variables, multi-platform sources, and sequential build provisioners for maintainable Packer templates

Which Provisioners Work Best for Server Configuration?

Packer supports shell, Ansible, Chef, Puppet, Salt, PowerShell, and file upload provisioners. Choice depends on team expertise and complexity. Shell scripts suit simple, single-purpose images. Ansible excels when you already use it for application deployment or need idempotent, testable configurations.

ProvisionerBest ForLearning CurveIdempotencyDebugging
ShellSimple OS-level setup, single-purpose imagesLowManual (script design)Easy (stdout visible)
AnsibleComplex multi-service configs, existing Ansible teamsMediumBuilt-inModerate (verbose flags)
Chef/PuppetEnterprise policy compliance, large teamsHighBuilt-inComplex
PowerShellWindows Server images, .NET appsMediumManualModerate
FileConfig file uploads, certificate injectionNoneN/ATrivial

For Laravel and PHP applications, I typically combine shell and Ansible. Shell handles OS updates and runtime installation (PHP 8.4, Node 22 LTS, Composer 2.7). Ansible manages Nginx vhosts, PHP-FPM pools, firewall rules, and user permissions. This hybrid approach keeps fast-changing OS tasks in simple scripts while putting complex, interdependent service configs in testable Ansible roles.

A common mistake is running apt-get upgrade without pinning versions. On Ubuntu 24.04, an unattended upgrade during build can pull a newer PHP minor version than your application expects. Always pin critical packages:

# scripts/install-runtime.sh
#!/bin/bash
set -euo pipefail

export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get install -y --no-install-recommends \
  php8.4-fpm=8.4.* \
  php8.4-mysql=8.4.* \
  php8.4-redis=8.4.* \
  nginx=1.26.* \
  nodejs=22.*

systemctl enable php8.4-fpm nginx
systemctl disable apache2 || true

How Do You Validate and Test Built Images Before Deployment?

Building an image is only half the job. Validating it prevents deploying broken artifacts. Packer’s built-in validate command checks HCL syntax and provisioner connectivity, but not runtime correctness. For that, add explicit validation steps inside the build or use external testing frameworks.

In-Build Validation

Add a final shell provisioner that verifies critical services start and respond:

provisioner "shell" {
  inline = [
    "systemctl is-active php8.4-fpm || exit 1",
    "systemctl is-active nginx || exit 1",
    "curl -sf http://localhost/healthz || exit 1",
    "php -v | grep '8.4' || exit 1",
    "nginx -t 2>&1 | grep 'syntax is ok' || exit 1"
  ]
}

If any check fails, Packer aborts the build and deletes the temporary instance. This prevents bad images from ever being tagged or stored. On legal-tech portals handling sensitive documents, I also verify file permissions and SELinux/AppArmor status here—misconfigured permissions are a frequent post-deploy failure point.

External Testing with InSpec or Goss

For comprehensive validation, integrate HashiCorp InSpec or Goss as a post-build step. These tools run read-only tests against the finished image without modifying it:

# goss.yaml
package:
  php8.4-fpm:
    installed: true
    version: ["8.4"]
  nginx:
    installed: true

service:
  php8.4-fpm:
    enabled: true
    running: true
  nginx:
    enabled: true
    running: true

port:
  tcp:80:
    listening: true
  unix:/run/php/php8.4-fpm.sock:
    listening: true

file:
  /etc/nginx/sites-available/default:
    exists: true
    mode: "0644"
    owner: root

Run Goss inside Packer via a shell provisioner that installs the binary, copies the test file, and executes goss validate. Failures halt the build. This catches issues like missing PHP extensions, wrong socket paths, or disabled services that syntax checks miss.

HCL ValidateSyntax CheckPlugin VerifyVar ResolutionFAIL → AbortRuntime ChecksService ActivePort ListeningVersion MatchFAIL → Destroy VMGoss / InSpecPackage TestsFile PermsSecurity PolicyFAIL → No ArtifactValid ImageTaggedStored
Three-stage validation ensures only fully tested Packer build machine images reach production artifact storage

How Does Packer Compare to Manual Setup and Other IaC Tools?

Understanding where Packer fits prevents misuse. It complements rather than replaces configuration management or orchestration tools. Manual server setup remains common in Nepal’s SME sector due to perceived simplicity, but it introduces unreproducible environments. Configuration management (Ansible/Chef) applied at boot time adds 3–10 minutes per instance and risks partial failures. Packer moves this cost entirely to build time.

Terraform provisions infrastructure but doesn’t configure OS-level software. The optimal pattern pairs them: Packer builds the image, Terraform launches instances from that image. For Laravel applications, this means your Terraform aws_instance references data.aws_ami.latest_laravel.id instead of a generic Ubuntu AMI plus user_data scripts. If you’re evaluating infrastructure costs for Nepali businesses, comparing cloud hosting versus shared hosting trade-offs clarifies when immutable images justify their operational overhead.

Docker containers solve similar immutability problems but aren’t always appropriate. Legal-tech portals requiring specific kernel modules, systemd services, or hardware-bound licensing often need full VMs. Packer builds those VMs with the same reproducibility Docker offers for containers. For pure stateless web apps, containers may suffice; for stateful, regulated, or legacy-integrated systems, Packer-built VMs provide stronger isolation and compliance guarantees.

Conclusion

Adopting Packer: Build Machine Images transforms server provisioning from a fragile, manual ritual into a repeatable engineering process. Start small: pick one stable workload (a Laravel app server, a WordPress host, a legal portal backend), write an HCL template with shell provisioners, validate rigorously, and integrate the output into your existing deployment workflow. Resist over-engineering early; a working shell-scripted image beats a perfect Ansible playbook that never runs. As your confidence grows, layer in multi-platform sources, external testing, and CI-triggered builds. The goal isn’t theoretical purity—it’s servers that behave identically every time, so you spend less time debugging infrastructure and more time shipping features. Ready to implement immutable infrastructure for your next project? Contact me to discuss Packer integration for your Laravel, eCommerce, or legal-tech platform.

Frequently Asked Questions

Packer automates creation of identical machine images for multiple platforms from a single source configuration.

Packer builds immutable base images with pre-installed software, while Terraform provisions infrastructure and Ansible configures running servers. In my production deployments, I use Packer to bake PHP 8.4, Nginx, and security hardening into an AMI, then let Terraform launch instances from that image and Ansible handle only runtime secrets or environment-specific variables. This separation prevents configuration drift and reduces boot time significantly compared to provisioning everything at deploy time on a bare OS image.

Use HCL2 (.pkr.hcl) exclusively. The legacy JSON format was deprecated years ago and lacks support for modern features like dynamic blocks, locals, and improved variable validation. When maintaining older projects I have migrated several JSON templates to HCL2; the syntax is stricter but far more maintainable. Current stable Packer releases parse HCL2 natively without plugins. Avoid starting new work in JSON even if older tutorials still reference it, as community support and documentation now assume HCL2 throughout.

Yes. Define multiple source blocks in one template targeting amazon-ebs and virtualbox-iso simultaneously. Each source specifies its own communicator, ISO URL, or AMI base, while shared provisioners apply identical setup steps across platforms. I have used this pattern to create matching development and production environments where developers test locally on VirtualBox before CI builds the production AWS AMI. Ensure provisioner scripts are idempotent and platform-aware, using conditional logic or separate script files when OS-level commands differ between targets.

Never hardcode secrets in templates. Use environment variables referenced via env() functions, HashiCorp Vault integration, or cloud provider secret managers accessed through IAM roles. For Nepal-based clients using eSewa or Khalti API keys during image builds, I inject these at runtime through CI/CD pipeline variables rather than storing them in Git. Packer also supports var files excluded from version control. Always revoke temporary credentials immediately after build completion and audit logs to confirm no secrets leaked into image metadata or provisioner output logs.

Shell and Ansible provisioners cover most production needs. Shell scripts handle low-level OS tasks like kernel tuning, user creation, and package installation with precise control. Ansible excels at higher-level application setup, role-based configuration, and idempotent service management. On legal-tech portals I maintain, shell scripts install PHP-FPM and configure UFW rules first, then Ansible applies Laravel-specific directory structures and permissions. Avoid mixing too many provisioner types; stick to two complementary tools maximum to keep debugging tractable when builds fail mid-provisioning.

SSH timeouts usually stem from incorrect communicator settings, firewall rules blocking port 22, or slow cloud-init processes delaying SSH availability. Increase ssh_timeout to 10m initially, verify security groups allow inbound SSH from Packer’s IP, and add pause_before_connecting if cloud-init takes time. On Ubuntu 24.04 builds I have seen systemd-networkd delays cause intermittent failures; adding a wait_for_cloud_init provisioner step resolved this. Also confirm username matches the base image default (ubuntu for AWS, vagrant for VirtualBox) and that key-based auth is properly configured.

Aim for under 5GB for web application images. Larger images increase storage costs, slow instance launches, and expand attack surface. Remove package caches, logs, temp files, and unnecessary documentation in final cleanup provisioners. On eCommerce projects serving WooCommerce or Laravel apps, my optimized AMIs typically range 3-4GB including PHP 8.4, Nginx, Redis, and application dependencies. Monitor actual usage post-deployment; if disk utilization stays below 40%, consider further trimming. Bloated images often indicate leftover build artifacts or unremoved development packages that should never reach production.

No. Packer is open-source under BSL 1.1 and free for commercial use. Costs arise only from underlying cloud resources consumed during builds (compute, storage, data transfer). A typical AWS AMI build costs Rs 50-200 (~USD 0.40-1.50) depending on instance type and duration. Budget for EBS snapshot storage if retaining multiple image versions. There are no licensing fees regardless of team size or revenue. Enterprise features like HCP Packer registry require paid HashiCorp Cloud Platform subscriptions, but core image building remains completely free for all production workloads.

Embed semantic versioning and git commit hashes in image names and tags automatically using template variables. Include build timestamp, branch name, and pipeline ID for traceability. On shared Deployer 7 pipelines serving sites like notarykathmandu.com and translationnepal.com, every AMI gets tagged with app_version, git_sha, and built_by fields. This enables instant rollback identification and audit compliance. Never rely solely on auto-generated IDs; human-readable naming prevents confusion when managing dozens of images across staging and production accounts. Clean up obsolete images regularly to avoid storage bloat and accidental deployment of outdated builds.

Yes. Run packer init and packer build commands in GitLab CI jobs triggered on tag pushes or manual approval. Store state and artifacts in CI variables or S3-compatible backends. I configure pipelines so merging to main triggers a validation-only build, while tagged releases produce production AMIs with full testing. Use Docker executors with Packer installed or dedicated runner images preloaded with required plugins. Protect credential variables as masked CI/CD settings. Add retry logic for transient cloud API failures and set appropriate job timeouts since image builds often exceed default 1-hour limits.

Apply CIS Benchmark controls during provisioning: disable root login, enforce key-only SSH, configure fail2ban, enable automatic security updates, remove unused services, and set restrictive file permissions. Install and configure UFW with explicit allow rules. On legal-tech platforms handling sensitive client documents, I also bake in auditd logging, AppArmor profiles, and verified package signatures. Scan final images with tools like Lynis or OpenSCAP before marking them production-ready. Security must be immutable at the image layer; runtime patching alone leaves windows of vulnerability between deploys.

Launch test instances from the new image in an isolated VPC or subnet and run integration tests against expected services, ports, and application health endpoints. Verify monitoring agents connect, backups execute, and authentication flows work. On Laravel applications I validate queue workers start, scheduled tasks register, and database migrations complete successfully. Automate smoke tests using tools like Testinfra or Serverspec within the same CI pipeline that built the image. Only promote images passing all checks to production catalogs. Manual verification catches edge cases automation misses, especially around third-party integrations like payment gateways or SMS providers.

Skipping cleanup steps leaves temporary files and cached packages that bloat images and leak build-time secrets. Not testing provisioner idempotency causes failures on rebuilds. Hardcoding region-specific values breaks multi-region deployments. Ignoring cloud-init race conditions leads to intermittent SSH failures. On one migration project, missing locale generation caused PHP string function errors only visible post-deploy. Always validate images in an environment matching production topology before release. Document known quirks per base OS version; Ubuntu 24.04 behaves differently than 22.04 regarding systemd services and network configuration defaults.

Rebuild monthly or when base OS receives major security patches, framework versions change (e.g., PHP 8.3 to 8.4), or accumulated runtime patches create significant drift. Minor dependency updates can wait for scheduled rebuild cycles unless addressing critical CVEs. On long-running legal portals, quarterly rebuilds balance freshness with stability. Track patch accumulation metrics; if runtime Ansible runs exceed 15 minutes applying updates, the base image is stale. Never modify running production instances directly except for emergency hotfixes; always flow changes through Packer to preserve immutability guarantees and ensure disaster recovery produces identical replacements.

Share this article

Quick Contact Options
Choose how you want to connect me: