For the complete documentation index, see llms.txt.

Guardener Container Image Migration

Configure Guardener to open a pull request that replaces the container images a repository already uses with Chainguard container images.
  9 min read

Guardener scans the default branch of a repository for container image references and opens a pull request that replaces them with Chainguard container images your organization already has. As your repository and your organization’s images change, Guardener keeps that pull request up to date.

Note: Guardener is in beta. Available to organizations that have installed and linked the Chainguard App.

Container Image Migration and Container Image Suggestions work together. Image Suggestions reviews the images that each new pull request adds, and migration replaces the images the repository already uses. Each has its own setting in .chainguard/images.yaml, so you can use either one or both.

A migration pull request changes image references only. Guardener doesn’t build your images, run your tests, or convert your Dockerfiles, so review and test the pull request as you would any other change.

Prerequisites

Before you enable Container Image Migration, make sure that:

  • The Chainguard App is installed with access to the repository, and your GitHub organization is linked to your Chainguard organization, as described in Getting started. You don’t need to change the repository visibility scope: unlike Image Suggestions, migration runs on private repositories even with the default PUBLIC scope.
  • Your Chainguard organization has the replacement images. Guardener doesn’t add images for you. Adding an image isn’t always self-service: how you do it depends on your subscription, and the steps are in The container isn’t in your organization’s catalog.

Enable Container Image Migration

Add a migrate block to the repository’s .chainguard/images.yaml file, and set enabled inside it to true:

enabled: true
migrate:
  enabled: true

The top-level enabled key controls Image Suggestions, and the migrate.enabled key controls migration. Image Suggestions is on whenever the file exists. To get only migration pull requests, set the top-level enabled to false:

enabled: false
migrate:
  enabled: true

Commit the file to the repository’s default branch. Guardener reads .chainguard/images.yaml only from the default branch, and the push that adds or changes the file starts the first migration.

To set migration once for every repository in your organization, commit the file to your organization’s .github repository instead, as described in Configuration. A repository’s own .chainguard/images.yaml replaces the organization file entirely, so a repository file without a migrate block turns migration off for that repository.

When migration runs

Guardener runs a migration for a repository at the following times:

  • After a push to the default branch that adds, changes, or removes .chainguard/images.yaml in that repository.
  • On a schedule, about once every migrate.period. The default and minimum period is 24 hours.
  • On demand, when you run chainctl guardener github migrate create, as described in Run an on-demand migration.

Each run scans the whole default branch again. A scheduled run therefore picks up images that your organization added since the last run, as well as new image references in the repository. Changes to the organization-level file in your .github repository take effect at each repository’s next scheduled run.

Review the migration pull request

Guardener keeps one image migration pull request per repository, separate from the Hardened Actions migration pull request. The pull request is titled Migrate container images to Chainguard equivalents, and its description lists the following:

  • Changed files: every file that the pull request edits.
  • Not migrated: each reference that Guardener found and left unchanged, with the reason.

Each run plans its changes against the latest commit on the default branch. If the migration pull request is open, Guardener updates it only when the planned changes differ from the ones it already has. After you merge or close the pull request, the next run opens a new one if there’s anything left to change. Guardener never merges the pull request itself.

Closing the pull request doesn’t turn migration off. To stop migration for a repository, set migrate.enabled: false.

To make your own changes to the pull request, add the skip:guardener/images label or assign the pull request to someone first. Guardener then stops updating it. Don’t push to the pull request’s branch, under guardener/images/, without first adding the label or an assignee, because Guardener force-pushes the branch on each update.

When nothing is left to change, Guardener doesn’t open a pull request, and it doesn’t close one that’s already open. If you make the same changes yourself, close the open migration pull request.

What migration changes

Migration reads Dockerfiles, YAML files, and JSON files across the whole repository. It skips files under node_modules, vendor, .terraform, and testdata directories.

FilesWhat Guardener changes
Dockerfiles: Dockerfile, Dockerfile.*, *.dockerfile, and ContainerfileThe image in each FROM instruction, in every build stage.
YAML and JSON, such as Kubernetes manifests, Compose files, and Helm valuesEach literal image value. Guardener doesn’t read Helm chart templates.
Terraform (*.tf, *.tfvars), shell scripts (*.sh), and MakefilesNothing. Guardener doesn’t read these files. To find the images they use, run chainctl images discover.

Guardener chooses each replacement the same way as Image Suggestions. When Chainguard publishes a Federal Information Processing Standards (FIPS) variant of an image, Guardener prefers it unless you set prefer_fips: false. Guardener keeps the reference’s tag when Chainguard maintains it. Otherwise, it uses the newest maintained version on the same minor version line or, failing that, in the same major version. If the version changes, check its release notes before you merge. When a reference has both a tag and a digest, the replacement is also pinned by digest.

Guardener leaves a reference unchanged and lists it under Not migrated in the following cases:

  • Your Chainguard organization doesn’t have the replacement image, or doesn’t have a matching tag. After you add the image, the next run migrates the reference.
  • Chainguard doesn’t publish an equivalent image.
  • The reference isn’t one literal value on its own line. For example, it’s assembled from a Dockerfile ARG, it’s split across Helm registry, repository, and tag keys, or another image shares its line.
  • The Dockerfile build stage, or a stage built on it, runs commands with RUN, a shell-form CMD or ENTRYPOINT, or an ONBUILD instruction. Chainguard container images without a -dev suffix usually have no shell or package manager, so swapping only the image could break the build. To convert the whole Dockerfile, use Dockerfile migration.
  • The Dockerfile build stage sets CMD without ENTRYPOINT. Many Chainguard container images set ENTRYPOINT to their runtime, so the command could change.
  • The reference has a tag that Chainguard doesn’t maintain, and no maintained version is close enough to replace it, so the only replacement would be the latest tag.
  • The reference is pinned by digest without a tag, so there’s no version to carry over.

Exclude files and images

To keep Guardener from changing specific files or images, add a migrate.ignore block:

migrate:
  enabled: true
  ignore:
    files:
      - "Dockerfile.dev"
      - "examples/*"
    images:
      - "busybox:*"

Both lists take glob patterns. A * matches any characters except /, and ** doesn’t match across directories.

  • ignore.files matches a file’s path from the repository root, or its file name. In this example, examples/* matches examples/Dockerfile but not examples/app/Dockerfile.
  • ignore.images matches the image reference as written in the file, with Dockerfile ARG defaults filled in. In this example, busybox:* matches busybox:1.36, but it doesn’t match docker.io/library/busybox:1.36 or busybox with no tag.

Ignored files and images don’t appear in the migration pull request.

Run an on-demand migration

Rather than waiting for the next scheduled run, you can start a migration right away with chainctl. For example, you might do this after you add an image to your organization. An on-demand run performs the same migration as a scheduled run and uses the same configuration. It doesn’t change the schedule.

Before you start, make sure that:

  • You have chainctl 0.2.375 or later. Earlier versions migrate only GitHub Actions. To update, run chainctl update.
  • The repository has migrate.enabled: true in its own .chainguard/images.yaml file or in the organization-level file.
  • You hold the guardener.images.migrate capability on the Chainguard organization that’s linked to the repository’s GitHub organization. The built-in viewer, editor, and owner roles include it, as do guardener.user and guardener.admin. For more information, refer to the Built-in roles and capabilities reference.

Run chainctl guardener github migrate create with the repository:

chainctl guardener github migrate create <owner>/<repo>

The command runs every migration that the repository opts in to: Container Image Migration through .chainguard/images.yaml and Hardened Actions migration through .chainguard/actions.yaml. Each opens or updates its own pull request.

By default, the command waits up to 10 minutes for the migrations to finish and then prints one result for each:

Repository: https://github.com/<owner>/<repo>
Triggered by: you@example.com (user)
Actions: migration is not enabled here or in repository configuration
Images: https://github.com/<owner>/<repo>/pull/43
Status: completed

In this example, the repository opts in to Container Image Migration only. The Images: line reports one of the following results:

ResultMeaning
A pull request URLGuardener opened or updated the migration pull request.
no changes neededGuardener found nothing it could change. This includes references whose replacement your organization doesn’t have yet. To see the replacement for every reference, run chainctl images discover.
migration is not enabled here or in repository configurationThe repository doesn’t have migrate.enabled: true.
failedGuardener couldn’t finish the migration. The message in parentheses says why, such as a missing capability or a scan limit.

When any migration fails, the last line reads Status: completed with failures, and the command exits with an error. To return without waiting, or to check a migration later, refer to Check a migration operation.

Limits

Because migration reads the whole repository, it has the following limits. When a repository exceeds one, Guardener doesn’t open or update the migration pull request, and an on-demand run reports Images: failed with the limit it reached. To bring the repository under the limit, exclude directories or files with migrate.ignore.files.

LimitValue
Image references, counted once per file, line, and image100
Image reference occurrences1,000
Files in Helm chart templates directories, which Guardener checks so it can skip them300
Total size of the files read128 MiB

Guardener also skips any file larger than 1 MiB and lists it under Not migrated.

Configuration reference

The following keys in .chainguard/images.yaml control migration:

KeyTypeDefaultDescription
migrate.enabledbooleanfalseTurns migration pull requests on for the repository.
migrate.periodduration24hHow often Guardener runs a scheduled migration, written as a Go duration such as 168h for one week. A period shorter than 24 hours, or a value that isn’t a duration, uses 24 hours.
migrate.ignore.fileslist of glob patternsNoneFiles that migration doesn’t read.
migrate.ignore.imageslist of glob patternsNoneImage references that migration leaves unchanged.
prefer_fipsbooleantrueRanks FIPS variants before standard variants, for both migration and Image Suggestions.

The top-level enabled key controls Image Suggestions only. For more information, refer to the Image Suggestions configuration reference.

Full example

The following .chainguard/images.yaml file turns on Image Suggestions and weekly migration pull requests. It prefers standard images to FIPS variants, and it leaves a development Dockerfile and every busybox:<tag> reference unchanged:

enabled: true
prefer_fips: false
migrate:
  enabled: true
  period: "168h"
  ignore:
    files:
      - "Dockerfile.dev"
    images:
      - "busybox:*"

Commit the file to the repository’s default branch. The push that adds or changes it starts a migration.

Next steps

Last updated: 2026-10-08 02:59