Skip to content

Image Lock

github-actions[bot] edited this page Oct 12, 2026 · 6 revisions

Image Lock

← Config-Env-Vars · Compat-V15


Every container image the generated compose files and the CLI's own tools (ACME client, admin pull, monitoring fragments) use comes from one file: internal/compose/images.yaml (authored), compiled into internal/compose/images.lock.json (generated, embedded in the binary). A scan test (internal/compose/images_literal_scan_test.go) fails the build when a non-test Go file under internal/ or cmd/ spells an image reference as a single string literal.

What the scan does not see: literals assembled from parts (for example "postgres:" + version, tracked as debt D-0213), Dockerfile templates inside raw strings (scaffold output), tools/, untagged repository strings, and the dead docker-compose.monitoring.yml. Three allowlisted tool images remain (plugin author tool, CI job image, controlplane test harness).

The lock

Each entry records name, role (core, optional, monitoring, tool, plugin), the fully qualified repository, version, the index_digest of the multi-arch index, the per-platform digests (linux/amd64, linux/arm64, linux/arm/v7 when upstream publishes them), legacy_ref, license, source and mirror.

The first lock records what each effective reference resolved to on 2026-10-04. No version moved: legacy_ref is the exact string the CLI emitted before the lock existed. legacy_ref stays frozen: a pin bump moves version (and the digests) only, so legacy mode keeps emitting the pre-lock string. The lock test checks that legacy_ref names the same repository with a tag; it does not require legacy_ref to equal repository:version.

Pinning mode: IMAGE_PINNING

Value Image string Default in
legacy the entry's legacy_ref, byte-identical to earlier releases (redis:7-alpine, nginx:alpine) v1.4.x
lock repository:version@sha256:<index digest> v1.5.0 (NSELF_V15=1)

Set IMAGE_PINNING=lock in .env to opt a project in early. Moving to lock changes every image string, so the next nself start recreates the containers; plan for that on a serving box.

Overrides

A *_VERSION key in the env cascade is an explicit override and is used as given, unpinned, unless it equals the lock version or the legacy_ref tag. The legacy_ref tag is what the config defaults and the .env files written by nself setup carry (HASURA_VERSION=v2.44.0), so it counts as "the default" and stays pinned after a bump. To run the pre-lock version itself, use IMAGE_PINNING=legacy.

Setting IMAGE_PINNING=lock IMAGE_PINNING=legacy
unset, or equal to the lock version or the legacy_ref tag (REDIS_VERSION=7-alpine) docker.io/library/redis:7-alpine@sha256:... redis:7-alpine
any other value (REDIS_VERSION=6.2-alpine) redis:6.2-alpine (no digest) redis:6.2-alpine
POSTGRES_IMAGE / CS_N_IMAGE used as given used as given

An IMAGE_PINNING value other than lock or legacy (case ignored) is an error: nself build and every command that loads the config refuse to run rather than fall back silently.

A project digest file written by nself update images (.nself-image-digests.json) still applies to unpinned references; a lock reference already carries its digest and is left alone.

Valkey, the default cache

Redis 7.4 and later is RSALv2/SSPLv1, so the lock carries a valkey entry (docker.io/valkey/valkey:8.1-alpine, BSD-3-Clause) and the redis compose service runs it by default in v1.5. The service name, REDIS_* variables and REDIS_URL stay. The engine is chosen by REDIS_ENGINE (valkey or redis), then by the engine of the project lock's redis pin, then by the mode default (v1.4 redis, v1.5 valkey):

Project Result
v1.5, new project (no lock, no existing compose) Valkey from the lock entry, volume valkey_data
v1.5, lock pins a redis image stays Redis; nself build --plan shows no cache change
v1.5, no lock, existing docker-compose.yml runs redis stays Redis (the first lock takes the pin from that file)
REDIS_ENGINE=valkey, any mode Valkey; a v1.4 project switches with nself config set REDIS_ENGINE valkey, then nself build --plan, nself build, nself start
REDIS_ENGINE=redis, any mode Redis, whatever the lock pins

Valkey runs valkey-server with the same flags, user, port and network, and its health check is valkey-cli ping (no reliance on redis-* symlinks). A REDIS_VERSION equal to the Redis default (7-alpine) means the Valkey default; any other value is used as the Valkey tag.

Queued jobs are not migrated: Valkey starts on an empty valkey_data volume, and the plan shows a stateful recreate of the redis service. redis_data is kept, so setting REDIS_ENGINE=redis again reuses it.

How pins move

  1. Edit the entry's version in internal/compose/images.yaml (a version bump is its own reviewed change). Leave legacy_ref alone. The weekly bump (tools/imagebump, .github/workflows/image-pin-bump.yml) does the same.
  2. go run ./tools/imagelock -resolve reads each index through the docker CLI (network), proves each digest addresses the index it came from, and rewrites the lock.
  3. go run ./tools/imagelock -check verifies the lock offline: header, sorted, every entry has an index digest and linux/amd64, and the lock covers images.yaml. Adding an entry without resolving fails it.
  4. go run ./tools/imagelock -resolve -plugins images.json also merges the plugins repository's images.json v1: kind: plugin entries become plugin/<slug>, kind: upstream entries keep their name, kind: ci entries are ignored.

An upstream that publishes no digest-addressable index, or misses a required platform, stops the run with the image named; raise it before locking.

Mirror namespace

Each non-plugin entry names a mirror, docker.io/nself/<name>, at the same digest, for pulls when upstream deletes a tag. Plugin entries have no mirror: their repository is already ours.

Probes

tools/imagelist prints service<TAB>repository:version@digest for every entry; .github/workflows/default-images-pullable.yml runs docker manifest inspect on each line and triggers on a change to images.yaml or images.lock.json.

Accessors

compose.LockedRef(name) returns one entry, compose.LockedImages() all of them sorted by name, compose.ImageRef(name, version) the string for the active mode, and compose.PinningMode() the resolved mode.

Home


Getting Started


Commands


Features


Configuration


Plugins (87 + 10 monitoring)

Free (25)
Pro (62)
Planned (26)
  • plugin-audit
  • plugin-blog
  • plugin-checkout
  • plugin-commerce
  • plugin-drm
  • plugin-export
  • plugin-flow
  • plugin-import
  • plugin-ldap
  • plugin-mailgun
  • plugin-media
  • plugin-oauth-providers
  • plugin-pages
  • plugin-postmark
  • plugin-rate-limit
  • plugin-reports
  • plugin-saml
  • plugin-scheduler
  • plugin-sendgrid
  • plugin-sso
  • plugin-subscription
  • plugin-thumb
  • plugin-transcoder
  • plugin-twilio
  • plugin-waf
  • plugin-watermark

Guides


Architecture


Reference


Machine interface


Licensing


Security


Brand


Operations


Contributing


Admin


Changelog


All commands (21)

Clone this wiki locally