Skip to content

Create a committed image pin file

This content is for the v0.1 version. Switch to the latest version for up-to-date documentation.

Use a pin file when an image is selected outside the cluster definition, such as by a CI build or a release decision. The file records the requested tag and the digest it resolved to. Your cluster reads that committed value during evaluation.

This guide assumes your project uses kix.lib.mkFlake and the package exposes its image as a kix.options.image option.

Run kix pin set from the project root. A new pin needs a key, repository, and tag. --init allows Kix to create pins.json and add the key:

Run in kix-project/
❱ kix pin set web --repository docker.io/nginxinc/nginx-unprivileged --tag 1.28-alpine --init

Kix asks the registry what the tag resolves to and writes the resulting digest. If your flake lives in another directory, pass that directory with --flake; Kix will create the file beside it. You can also choose an explicit path with --pins-file.

The generated file has this shape:

pins.json
{
"version": 1,
"managedBy": "kix pin",
"images": {
"web": {
"repository": "docker.io/nginxinc/nginx-unprivileged",
"tag": "1.28-alpine",
"digest": "sha256:...",
"promotedBy": "you@workstation",
"resolvedAt": "2026-09-09T10:59:45Z"
}
}
}

Do not fill in the digest by guessing it. Let kix pin set resolve the tag, or pass a digest you obtained through your air-gapped promotion process with --digest.

Name the file once in your mkFlake call:

clusters/flake.nix (L26–L28)
# Declared once. A cluster that takes a `pins` argument gets this file
# already loaded, so no cluster definition names the path.
pins = ./pins.json;

View source on GitHub ↗

Any cluster function that declares a pins argument now receives the loaded, shape-checked pin set.

Pass the complete pin to the package’s image option:

clusters/test-pins.nix (L121–L121)
config.image = pins.get "web";

View source on GitHub ↗

The package should declare the option with the shared image schema:

packages/web/default.nix
options.image = kix.options.image {
repository = "docker.io/nginxinc/nginx-unprivileged";
tag = "1.28-alpine";
};

Render the container image with kix.image.ref and carry through the pull policy:

clusters/test-pins.nix (L91–L92)
image = kix.image.ref config.image;
imagePullPolicy = config.image.pullPolicy;

View source on GitHub ↗

The rendered value is repository:tag@digest. The tag remains readable while the digest fixes the content Kubernetes will pull.

Inspect the file as Kix sees it:

Run in kix-project/
❱ kix pin list --pins-file pins.json
web
  docker.io/nginxinc/nginx-unprivileged:1.28-alpine
  sha256:7377697a821c131a924a7105fafbe7414db4e9fcc77a6f08f776f33f141ec3f8
  promoted by [email protected], 2026-09-09T10:59:45Z

Check the file’s schema without contacting the registry:

Run in kix-project/
❱ kix pin check --pins-file pins.json --offline
pins.json

  ok    web  docker.io/nginxinc/nginx-unprivileged:1.28-alpine (format checked offline; registry not checked)

1 pins checked, 0 failed, 0 warned

Before deploying, run the online check as well. It verifies that every recorded digest still exists in its registry:

Run in kix-project/
❱ kix pin check

Finally, add the pin file and the Nix changes to the same commit:

Run in kix-project/
❱ git add pins.json flake.nix packages/web/default.nix cluster.nix

A clean checkout can now render the same image reference. Future promotions use the existing key and need only the new tag:

Run in kix-project/
❱ kix pin set web --tag 1.29-alpine