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.

Immutable Infrastructure and Golden Images with Packer

By Kokil Thapa | Last reviewed: August 2026

Server drift kills production reliability. When you patch live servers or install packages manually, every instance slowly diverges until debugging becomes guesswork. Adopting immutable infrastructure and golden images with Packer solves this by baking a verified, pre-configured machine image once and replacing servers entirely during updates rather than modifying them in place. This approach pairs naturally with the zero-downtime deployment workflows I use for CI/CD pipeline setups across client projects.

What is immutable infrastructure and golden images with Packer?

Immutable infrastructure treats servers as disposable artifacts. You never SSH into a production box to install PHP, tweak Nginx configs, or apply security patches. Instead, you define the entire server state in a Packer template, build a machine image (AMI, DigitalOcean snapshot, OpenStack image), verify it automatically, and deploy fresh instances from that image. When an update is needed, you build a new image and replace the old servers.

A "golden image" is simply the validated output of this process. It contains the OS, runtime (PHP 8.4, Node 22 LTS), web server, application dependencies, security hardening, and monitoring agents — all frozen at a specific point in time. On a recent legal-tech portal deployment, switching to this model reduced provisioning failures from weekly occurrences to near-zero because every server started from an identical, tested baseline.

Git RepoPacker HCLProvisionersPacker BuildValidateProvisionGolden ImageVerified ArtifactTagged + StoredDeploy / ReplaceNew InstancesOld TerminatedImmutable Infrastructure PipelineNo SSH • No Live Patching • Full Replacement
End-to-end immutable infrastructure and golden images with Packer workflow: code → build → verified artifact → full server replacement.

This contrasts sharply with mutable infrastructure where Ansible or Chef runs against long-lived servers. Mutable systems work until they don't — a missed package version, a partial config merge, or a manual hotfix creates snowflakes that break unpredictably. For teams managing Laravel applications in production, immutability removes an entire class of deployment bugs.

How do you write a Packer template for a PHP-FPM golden image?

Packer uses HCL2 templates to define sources (cloud providers), builds (provisioning steps), and post-processors (tagging, export). Below is a minimal but production-viable template targeting Ubuntu 24.04 on DigitalOcean, suitable for hosting Laravel 12.x or Symfony 7.x applications. This mirrors configurations I've used for legal-tech portals requiring consistent PHP 8.4 environments.

packer {
  required_plugins {
    digitalocean = {
      version = ">= 1.3.0"
      source  = "github.com/digitalocean/digitalocean"
    }
  }
}

variable "do_token" { type = string sensitive = true }

source "digitalocean" "php_app" {
  api_token     = var.do_token
  image         = "ubuntu-24-04-x64"
  region        = "sgp1"
  size          = "s-2vcpu-4gb"
  ssh_username  = "root"
  snapshot_name = "php-app-golden-{{timestamp}}"
}

build {
  sources = ["source.digitalocean.php_app"]

  provisioner "shell" {
    script = "scripts/install-php.sh"
  }

  provisioner "file" {
    source      = "configs/www.conf"
    destination = "/tmp/www.conf"
  }

  provisioner "shell" {
    inline = [
      "mv /tmp/www.conf /etc/php/8.4/fpm/pool.d/www.conf",
      "systemctl disable php8.4-fpm",
      "apt-get clean",
      "rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*"
    ]
  }
}

Key provisioning principles

  • Disable services at build time. Systemd units like PHP-FPM should be disabled so they start cleanly on first boot via cloud-init or your orchestrator, avoiding stale PID files baked into the image.
  • Clean caches aggressively. APT lists, temp files, and log artifacts add hundreds of MBs to image size. Smaller images deploy faster and reduce storage costs.
  • Pin explicit versions. Use apt-get install php8.4-fpm=8.4.* or Ondřej Surý’s PPA with pinned releases. Never rely on latest tags in production images.
  • Validate inside the build. Add a final provisioner that runs php -v, nginx -t, or application-specific smoke tests. Fail the build if validation fails — never publish unverified artifacts.

How does immutable deployment compare to mutable configuration management?

Choosing between immutable and mutable approaches depends on team size, change frequency, and tolerance for drift. Here's a practical comparison based on real deployments for eCommerce and legal-tech clients:

CriteriaImmutable (Packer)Mutable (Ansible/Chef)
Configuration driftEliminated by designAccumulates over time
Rollback speedInstant (revert image tag)Slow (reverse playbook)
Build time10–30 min per imageN/A (config applied at deploy)
Deployment complexitySimple artifact swapIdempotency edge cases
Debugging productionReproduce locally from same imageGuess current state
Best forStable runtimes, regulated appsRapidly changing dev environments

For business-critical systems like payment-processing eCommerce stores or legal document portals, immutability wins decisively. The upfront cost of building proper Packer templates pays back within weeks through eliminated "works on my machine" incidents and faster incident recovery. Mutable tooling still has value for local development environments or exploratory prototypes where rebuild cycles would slow iteration.

Mutable DriftDay 0: BaseIdenticalDay 30Hotfixes appliedDay 90Configs divergeDay 180Snowflake ❄️Immutable ConsistencyImage v1.0Verified ✓Image v1.1New buildImage v1.2Patch appliedImage v2.0Major upgradeEvery deployment starts from a known-good, tested baseline
Mutable servers accumulate unique configurations over time while immutable infrastructure and golden images with Packer ensure every instance matches the verified artifact exactly.

How do you integrate Packer into a GitLab CI pipeline for automated builds?

Manual Packer builds defeat the purpose. Automate image creation on every merge to your infrastructure repository. This ensures the golden image stays synchronized with application requirements. For teams using DevOps automation practices, this integration is foundational.

  1. Store secrets securely. Use GitLab CI/CD variables (masked, protected) for cloud provider tokens. Never commit credentials to the repository.
  2. Tag images semantically. Include git SHA, timestamp, and version: php-app-golden-v1.2.3-a1b2c3d-20260818. This enables precise rollback and audit trails.
  3. Run validation tests. Use InSpec, Goss, or custom shell scripts inside the Packer build to verify PHP extensions, open ports, file permissions, and service states before marking the image as ready.
  4. Promote artifacts explicitly. Tag verified images as candidate, then promote to production after staging validation. Never auto-deploy untested images.
stages:
  - validate
  - build
  - test

packer-build:
  stage: build
  image: hashicorp/packer:1.11
  script:
    - packer init .
    - packer validate .
    - packer build -var "do_token=$DO_TOKEN" .
  only:
    - main
  tags:
    - docker

image-test:
  stage: test
  script:
    - ./scripts/verify-golden-image.sh $CI_COMMIT_SHA
  needs:
    - packer-build

This pipeline runs on every merge to main. The build job initializes plugins, validates syntax, and executes the full provisioning sequence. The test job spins up a temporary instance from the new image and runs acceptance checks. Only passing images get promoted for production deployment.

What are common mistakes when adopting immutable infrastructure and golden images with Packer?

I've seen teams adopt immutability incorrectly and create worse problems than they solved. Avoid these pitfalls:

  • Baking secrets into images. API keys, database passwords, and TLS private keys must never exist in the golden image. Inject them at boot via environment variables, Vault, or cloud provider secret managers. Images are shared artifacts; treat them as potentially public.
  • Skipping validation. An untested golden image is just a frozen bug. Always include automated checks that verify critical functionality before publishing. On one project, we caught a missing PHP extension during the build phase that would have caused silent queue worker failures in production.
  • Ignoring cleanup. Failing to remove APT caches, log files, SSH host keys, and cloud-init artifacts bloats images and leaks metadata. Run thorough cleanup as the final provisioning step.
  • Treating images as static forever. Golden images expire. Schedule regular rebuilds (weekly or monthly) to incorporate security patches even when application code hasn't changed. Subscribe to CVE feeds for your base OS and runtime.
  • Over-provisioning. Don't bake application code into the golden image unless it changes infrequently. Keep the image focused on platform dependencies; deploy application artifacts separately via your existing CI/CD workflow. This keeps rebuild cycles fast and images reusable across multiple services.
Include in Image?YES ✓NO ✗OS + Security PatchesPHP / Node RuntimeNginx / Apache ConfigMonitoring AgentsAPI Keys / SecretsDatabase PasswordsApplication CodeTLS Private KeysStable Platform DepsInject at Boot / DeployRule: If it changes per-env or is secret, keep it OUT of the image
Decision framework for immutable infrastructure and golden images with Packer: stable platform components belong inside; secrets and application code stay external.

Making the shift to immutable infrastructure and golden images with Packer

Start small. Pick one non-critical service — perhaps a staging environment or internal tool — and build your first Packer template. Validate thoroughly before trusting it for production traffic. Document the build process and share ownership across the team; immutability fails when only one person understands the image pipeline. Budget NPR 50,000–150,000 (~USD 375–1,125) for initial setup if hiring expertise, though the long-term operational savings justify this investment quickly.

The discipline required for immutable infrastructure and golden images with Packer pays compounding returns. Fewer production incidents, faster scaling, predictable rollbacks, and auditable deployments become your baseline rather than aspirational goals. For teams ready to modernize their deployment practices, this is the foundation everything else builds on. Reach out via the contact page if you need hands-on guidance implementing this for your PHP or Laravel stack.

Frequently Asked Questions

Immutable infrastructure treats servers as disposable, never modified after deployment. Instead of patching live systems, you rebuild and replace them from a known-good image. Packer automates creating these "golden images" — pre-configured, versioned machine images containing your OS, dependencies, and application code. This eliminates configuration drift, ensures consistency across environments, and speeds up scaling. In my experience working on production Laravel applications, immutable infrastructure with Packer reduces deployment failures caused by manual server tweaks or inconsistent package versions.

Packer is open-source and free to use. The only costs are your build infrastructure — typically a CI runner or cloud instance (Rs 1,500–3,000/month, ~USD 11–23) for building images. For a small team in Nepal, I recommend using GitLab CI with a shared EC2 t3.small instance (Rs 2,500/month, ~USD 19) or a local Ubuntu 24.04 server if bandwidth is limited. No licensing fees apply.

Packer builds machine images (AMIs, VMDKs, QCOW2) for full virtual machines or cloud instances, while Docker builds container images. Use Packer when you need immutable infrastructure for cloud VMs (AWS EC2, GCP Compute Engine) or bare-metal servers. Use Docker for containerised workloads (Kubernetes, ECS). On a real client project, I used Packer to create hardened Ubuntu 24.04 AMIs for a Laravel application, while Docker handled the app container itself — they serve different layers of the stack.

Packer supports all major cloud providers: AWS (AMI), Google Cloud (Compute Engine images), Azure (Managed Images), DigitalOcean (snapshots), and VMware (VMDK). It also works with on-prem platforms like Proxmox, VirtualBox, and QEMU. For Nepal-based projects, I’ve used Packer with AWS and DigitalOcean most frequently due to their regional data centers and predictable pricing.

Start with a JSON or HCL2 template. Here’s a minimal HCL2 example for an AWS AMI: packer { required_plugins { amazon = { version = ">= 1.2.0" } } } source "amazon-ebs" "ubuntu" { ami_name = "my-ubuntu-2404-{{timestamp}}" instance_type = "t3.micro" region = "ap-south-1" source_ami = "ami-0abcdef1234567890" # Ubuntu 24.04 LTS AMI ID ssh_username = "ubuntu" } build { sources = ["source.amazon-ebs.ubuntu"] provisioner "shell" { inline = ["sudo apt update", "sudo apt install -y nginx php8.3-fpm"] } } Run with `packer build template.pkr.hcl`. This creates a reproducible AMI with Nginx and PHP 8.3 pre-installed.

Packer supports multiple provisioners: shell (inline or script), Ansible, Chef, Puppet, Salt, and file (copy files). For most projects, I use shell for simple tasks and Ansible for complex configuration. Example: provisioner "ansible" { playbook_file = "./playbook.yml" extra_arguments = ["--extra-vars", "php_version=8.3"] } This keeps configuration declarative and version-controlled alongside the Packer template.

Use dynamic variables in your template. Example: ami_name = "laravel-app-{{user `git_commit`}}-{{timestamp}}" This embeds the Git commit hash and build timestamp. For AWS, tag the AMI during creation: tags = { "Name" = "laravel-app" "Environment" = "production" "Commit" = "{{user `git_commit`}}" } In production, I’ve seen teams use semantic versioning (v1.2.3) or date-based tags (2026-05-15) — consistency matters more than the format.

Never hardcode secrets in Packer templates. Use environment variables or HashiCorp Vault. Example with environment variables: provisioner "shell" { environment_vars = ["DB_PASSWORD={{env `DB_PASSWORD`}}"] inline = ["echo $DB_PASSWORD > /tmp/db_password"] } For AWS, use IAM roles with least privilege. On a real deployment, I store secrets in GitLab CI variables and pass them to Packer at build time — never commit them to Git.

Use `packer build -debug template.pkr.hcl`. This pauses after each step, allowing SSH access to the instance. Common issues: - Incorrect AMI/source image ID (verify with `aws ec2 describe-images`) - SSH key permissions (ensure `chmod 600 ~/.ssh/id_rsa`) - Provisioner failures (check logs in `/tmp/packer-provisioner-*`) - IAM role permissions (Packer needs EC2 create/delete permissions) In my experience, 90% of failures are due to incorrect AMI IDs or missing SSH keys.

Yes. Define multiple sources in your template: source "amazon-ebs" "aws" { ... } source "googlecompute" "gcp" { ... } build { sources = ["source.amazon-ebs.aws", "source.googlecompute.gcp"] provisioner "shell" { ... } } This creates identical images for AWS and GCP. For Nepal-based projects, I’ve used this to maintain consistency across local Proxmox and AWS environments.

Create a `.gitlab-ci.yml` job: packer-build: image: hashicorp/packer:latest script: - packer init . - packer validate . - packer build -var "git_commit=$CI_COMMIT_SHORT_SHA" . only: - main Store AWS credentials in GitLab CI variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). On a production pipeline, I add a manual approval step before deploying AMIs to production.

Packer builds machine images; Terraform provisions infrastructure using those images. Packer creates the golden image (e.g., an AMI with PHP 8.3 and Nginx), while Terraform deploys EC2 instances using that AMI. They complement each other: Packer for image creation, Terraform for infrastructure-as-code. On a client project, I used Packer to build a hardened Ubuntu AMI, then Terraform to deploy auto-scaling groups with that AMI.

Follow this workflow: 1. Build a new image with Packer (e.g., `laravel-app-v2.1.0`). 2. Deploy the new image to a staging environment and test. 3. Update your Terraform/CloudFormation template to use the new image. 4. Deploy the updated template to production (rolling update or blue-green). 5. Terminate old instances after verifying the new ones. In production, I’ve seen teams use AWS CodeDeploy or Terraform’s `create_before_destroy` to ensure zero downtime.

Harden your images before deployment: - Remove unnecessary packages (`sudo apt autoremove`). - Disable root SSH login (`PermitRootLogin no` in `/etc/ssh/sshd_config`). - Set up automatic security updates (`unattended-upgrades`). - Use minimal base images (e.g., Ubuntu Minimal or Amazon Linux 2023). - Scan images with Trivy or AWS Inspector before deployment. - Rotate SSH keys and credentials after each build. On a legal-tech portal I built, I used Packer to create images with fail2ban and UFW pre-configured — security as part of the image, not an afterthought.

Maintain a versioned inventory of images. For AWS: 1. List available AMIs: `aws ec2 describe-images --owners self`. 2. Update your Terraform/CloudFormation template to use the previous AMI ID. 3. Deploy the updated template (rolling update). 4. Terminate instances using the old image. In GitLab CI, I store AMI IDs in artifacts and use a manual job to trigger rollbacks — critical for production systems.

Share this article

Quick Contact Options
Choose how you want to connect me: