Packer in GitLab CI lets you build machine images repeatedly, but its real magic is how it enforces infrastructure consistency across your entire development lifecycle.

Imagine you’re building a Docker image for your application. You bake your code, dependencies, and configuration into that image. Packer does the same, but for full virtual machines or containers – think AMIs for AWS, VMDKs for VMware, or Docker images. GitLab CI, a continuous integration platform, orchestrates this process.

Here’s how it looks in a GitLab CI pipeline (.gitlab-ci.yml):

stages:
  - build

build_image:
  stage: build
  image: hashicorp/packer:latest
  script:
    - packer init .
    - packer fmt -recursive
    - packer validate template.pkr.hcl
    - packer build template.pkr.hcl
  only:
    - main

This build_image job uses the official Packer Docker image. It first initializes Packer, formats any configuration files, validates them, and then runs the build. The template.pkr.hcl would contain your Packer configuration, detailing what kind of image to build and how.

Let’s look at a simple Packer HCL template for building a Docker image:

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

source "docker" "app_image" {
  image_build_method = "copy"
  tag_mutable        = false
  image_tag          = "my-app:${git.tag}-${git.commit.short}"
  dockerfile         = "Dockerfile"
}

build {
  sources = ["source.docker.app_image"]

  provisioner "shell" {
    inline = [
      "echo 'Hello from Packer!' > /app/hello.txt",
      "apt-get update && apt-get install -y --no-install-recommends curl",
      "curl -sSL https://example.com/my-app.tar.gz | tar xz -C /app"
    ]
  }
}

This template defines a Docker builder. It specifies that the image will be built using a Dockerfile in the same directory. The tag_mutable = false is crucial: it ensures that each build gets a unique, immutable tag, preventing accidental overwrites and enabling reliable rollbacks. The tag incorporates the Git tag and short commit hash, providing traceability.

The build block defines what happens after the base image is created. Here, a shell provisioner runs commands inside the container. It creates a file, installs curl, and then downloads and extracts your application into /app. This is where you’d install dependencies, configure services, and prepare your application environment.

The power of this comes from treating your infrastructure as code. Every time this pipeline runs, you get a fresh, identical image. If you need to deploy, you simply deploy the image built by Packer, not rely on a manually configured server. This eliminates the "it worked on my machine" problem and ensures that what you test is exactly what you deploy.

Consider the git.tag and git.commit.short variables. These are automatically provided by GitLab CI when you push a Git tag or commit. Packer injects these into the image_tag, making your built images directly traceable to your source code. This is a fundamental aspect of immutable infrastructure: if you can trace an image back to a specific commit, you can also roll back to it if needed.

The most surprising thing is how much complexity Packer abstracts away. You define the desired state of your image – what software should be installed, what files should be present, how services should be configured – and Packer figures out the how. It manages the lifecycle of the temporary build environment (like a Docker container or a temporary VM), runs your provisioning steps, and then captures the final state as a new, immutable image. You don’t manually SSH into a machine, install packages, and hope for the best.

The image_build_method = "copy" in the source block is a subtle but important detail. For Docker, copy means Packer will build the image directly from your Dockerfile. If you were building a VM image, you might use iso to install an OS from an ISO, or vmware-iso for VMware. Packer handles the orchestration for each builder type.

The next logical step is to integrate this immutable image into your deployment process, perhaps using another GitLab CI job that pulls the newly built image and deploys it to your target environment.

Want structured learning?

Take the full Packer course →