Skip to content

Lock File

github-actions[bot] edited this page Oct 15, 2026 · 2 revisions

Lock File

← Image-Lock · Compat-V15


nself.lock.yaml pins what a project runs: the CLI major.minor, the images of the services the CLI owns, the installed plugin versions with their checksums, and the installed bundles. Commit it. nself build writes it, nself build honours its image pins, and nself start --frozen refuses to start a stack that no longer matches it.

The file sits at the project root, not under .nself/, because nself init gitignores .nself/ and git cannot re-include a file under an ignored directory.

What is pinned

# GENERATED BY nself build — DO NOT HAND EDIT (change pins with nself update)
schema_version: 1
cli: "1.5"
images:
  hasura: docker.io/hasura/graphql-engine:v2.44.0@sha256:4d34…
  postgres: docker.io/library/postgres:16-alpine@sha256:7218…
unpinned:
  redis: redis:6
plugins:
  ai:
    version: 2.0.0
    sha256: 9f2c…
    tier: licensed
bundles:
  - nclaw
Key Meaning
cli Major.minor of the CLI that wrote the file. A patch upgrade never rewrites it.
images Compose service to image reference with a sha256 digest.
unpinned Service to a tag-only reference whose digest is not known. Listed so the version still cannot move.
plugins Plugin slug to version, sha256 and tier, read from the bundle install-state ledger.
bundles Installed bundles, sorted.

There are no timestamps. Keys are sorted. Two builds with unchanged inputs leave the file byte-identical, and untouched on disk (the mtime does not move). Only services the CLI owns are listed; your own services and their images are not.

When it is written

Mode Behaviour
v1.5 (NSELF_V15=1) Always on. Every build keeps the lock current.
v1.4 (default today) Off. nself build --write-lock creates it; once the file exists every build honours and updates it.

nself build --plan lists nself.lock.yaml as a lock artifact when it would change and writes nothing. The apply writes exactly the planned bytes.

Precedence

When build renders docker-compose.yml, each service image is resolved in this order:

  1. A variable in the env cascade that the generator reads to build that service's image (HASURA_VERSION, POSTGRES_IMAGE, REDIS_VERSION; the table below lists them all).
  2. The pin in nself.lock.yaml.
  3. The CLI image lock (Image-Lock).

So a CLI upgrade never silently moves an existing project's images. To move one, change the env value, or delete its line from the lock and build.

A *_VERSION that is empty, equal to the CLI default (the version nself init writes into .env) or equal to the frozen legacy tag the CLI shipped before the lock means "use the default". It is not an override and never drops a pin, even after a CLI upgrade moves that default. The image lock applies the same test when it renders an image, and it holds for every row in the table below. Only a value that differs from both is an override.

The default test starts with the image-lock entry that produced the rendered image. A few directional compatibility aliases match what the generator does: pgvector accepts the plain-Postgres init version, Valkey accepts the Redis init version, current Hasura accepts the v1.4 init version, and the two auth entries accept both recognized auth defaults. No other entry's default is inherited. For example, pg16 is the pgvector entry's default tag, but POSTGRES_VERSION=pg16 is an explicit override when the project renders plain postgres:pg16; it does not keep an older plain-Postgres pin.

nself init writes *_VERSION defaults into .env. A value that renders to the same image as the pin (HASURA_VERSION=v2.44.0 against a hasura/graphql-engine:v2.44.0@sha256:... pin, with or without the docker.io/ prefix, in legacy or lock pinning mode) keeps the digest. For postgres the comparison uses the image the generator renders (POSTGRES_VERSION=16-alpine matches a pgvector/pgvector:pg16 pin). A different value drops the pin, and build prints one notice naming the variable, once per run. Only a variable the generator really reads for that image counts, whatever its name looks like. One table in the CLI source (internal/compose/image_inputs.go) lists them, and a test fails when it drifts from the generator:

Service Variables that feed its image (first wins)
postgres POSTGRES_IMAGE, then POSTGRES_VERSION (POSTGRES_EXTENSIONS=pgvector picks the pgvector repository)
hasura HASURA_VERSION
auth AUTH_VERSION
redis REDIS_VERSION
minio MINIO_VERSION
mailpit MAILPIT_VERSION
admin (compose service nself-admin, pinned and compared as admin) NSELF_ADMIN_VERSION
functions FUNCTIONS_VERSION for the node runtime; none for deno and python
meilisearch MEILISEARCH_VERSION
typesense TYPESENSE_VERSION
nginx, meilisearch-init none (fixed image)

Any other variable (NGINX_VERSION, ADMIN_VERSION, HASURA_IMAGE, STORAGE_IMAGE, MEILISEARCH_INIT_VERSION) has no effect on the image, does not touch the pin and prints no notice. POSTGRES_IMAGE is the only *_IMAGE the generator reads, and it wins over POSTGRES_VERSION. When it names its own digest (POSTGRES_IMAGE=pgvector/pgvector:pg16@sha256:...) it keeps the pin only when that digest is the pin's; another digest at the same tag wins and drops the pin, even when POSTGRES_VERSION is also set. Tags compare as written (V1 and v1 are different tags).

The first lock

A project that already has a docker-compose.yml gets a first lock that takes its pins from that file, for the services the CLI owns:

  1. The digest of the local image (RepoDigests), when the image is present and has one.
  2. Otherwise the CLI image lock entry whose version matches the reference.
  3. Otherwise a tag-only entry under unpinned.

A service the old compose lacks takes the CLI default. A project with no compose starts from the CLI defaults.

The admin UI's compose service is nself-admin, while its lock key is admin. If another compose file also defines a service named admin, that service cannot replace the admin UI's lock entry: the generated nself-admin service wins the key deterministically.

Review a diff

The lock is written atomically (temp file, then rename) and ends with the line # end of nself.lock.yaml. A file without that line, such as one cut short by a crash or a full disk, is rejected as damaged instead of loading with fewer pins. If you edit the lock by hand, keep the last line. Every top-level key (schema_version, cli, images, unpinned, plugins, bundles) must be present, as an empty {} or [] when there is nothing to record; a missing or null key is a parse error.

The lock is plain YAML, so a pull request shows exactly which image, plugin or bundle moved. nself build and nself.lock.yaml change together, so review both.

nself start --frozen

--frozen compares the stack on disk with the lock and fails with E455 (exit 1) when any of these differ:

  • the image of a CLI-owned service from its pinned (or tag-only) reference, a pinned service missing from the compose, or a CLI-owned service the lock does not list;
  • an image that cannot be read as a plain reference: a CLI-owned service whose image: contains a variable ($), that has extends or build, any compose file with a top-level include, or any compose file with more than one YAML document (a trailing --- that opens an empty second document counts too), which Compose merges but this check cannot read ("image source cannot be verified");
  • an installed plugin version or checksum;
  • the installed bundles;
  • the CLI major.minor.

The files are merged in the order .nself/compose-files.txt lists them, the order start runs; if you edit that manifest, it must still include docker-compose.yml or --frozen fails with E455. The comparison covers every compose file start runs: the generated file, plugin fragments and docker-compose.override.yml. Docker lets the last file win, so an override that sets a CLI-owned service's image: shows up as an image mismatch, and nself build records it in the lock. Under --frozen a cli or plugin checksum that one side recorded and the other left empty also counts as a mismatch. A plain start still skips empty values.

Not covered: your own services (CS_N) and the services that plugin compose fragments add. Their images are not in the lock and --frozen does not compare them; plugins are covered by their version and sha256. A CLI-owned image built from a variable is not pinned: nself build logs a warning that names it.

The file .nself/state/image-overrides.yml, which start writes when a pull falls back to a registry mirror, is read too, but an entry from it counts only when its digest equals the digest of the image it replaces (same bytes, other registry). Any other override in that file is an image mismatch, and nself build never records it in the lock. When such an entry is the difference, the E455 message names the file. nself build drops the entries whose digest no longer equals the image the compose files render (and removes the file when none is left), so a pin that moved does not leave a stale mirror entry behind; you can also delete the file.

A service pinned by tag only (listed under unpinned) cannot detect a moved tag, so --frozen prints one warning that lists those services.

A project with no lock also fails. Use it in CI and on servers, after nself build has run from the committed lock:

nself build
nself start --frozen

Under --frozen, start does not rebuild and does not auto-install the AI plugins, so the stack it checked is the stack it starts. If the compose file is stale, run nself build first. --frozen cannot be combined with --skip-plugins (usage error E401): the lock describes the full stack, and --skip-plugins starts a smaller one the check never compared.

A plain nself start runs the same comparison, prints one warning that lists the differences, and starts anyway. See cmd-start and error-codes.

Contract

Read API for other Go code: lock.Load(projectDir) and (*Lock).PluginVersion(slug). The JSON Schema of the parsed document is schemas/lockfile.v1.schema.json.

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