Repository navigation
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).
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.
| 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.
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.
nself update images writes a project digest file (.nself-image-digests.json); nself build does not read it today (debt D-0673). A lock reference already carries its digest.
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.
Upstream renamed nhost/hasura-auth to nhost/auth; the old name stops at 0.42.x. nhost/auth:0.36.0 has the same index digest as nhost/hasura-auth:0.36.0 (sha256:2e0713ef…), so every mode now names nhost/auth and nothing that runs changes. The image string does change, so the first nself start after the upgrade pulls nhost/auth:0.36.0 and recreates the auth container once. The lock carries two auth entries: auth-v14 (docker.io/nhost/auth:0.36.0) and auth (docker.io/nhost/auth:0.52.0, the current release).
| Project | Auth image |
|---|---|
| v1.4 |
auth-v14: docker.io/nhost/auth:0.36.0@sha256:... in lock mode, nhost/auth:0.36.0 in legacy mode |
v1.5, no AUTH_ENCRYPTION_KEY
|
auth-v14; nself build prints one notice |
v1.5, AUTH_ENCRYPTION_KEY set (64 hex characters) before the first build, so no auth pin yet |
auth (0.52.0) |
| the lock pins 0.43.0 or later (0.52.0) but the key is missing or malformed |
nself build stops with an error naming AUTH_ENCRYPTION_KEY
|
AUTH_VERSION=0.52.0, no lock pin |
auth with the key; without it auth-v14 and the notice |
| the project lock pins auth 0.36.0 (either repository name) | the pin, in both modes, until nself update moves it |
AUTH_VERSION set to a release from 0.43.0 (0.45.0) |
nhost/auth:<tag>, unpinned, in any mode; without the key nself build prints a notice |
AUTH_VERSION=latest, a release before 0.43.0 (0.42.0) or another tag |
nhost/hasura-auth:<tag>, the exact string earlier releases rendered (the frozen line) |
AUTH_VERSION=0.36.0 (what nself init writes into .env) and AUTH_VERSION=0.52.0 are defaults, not overrides: neither drops a lock pin.
nhost/auth 0.43.0 and later refuse to start without AUTH_ENCRYPTION_KEY (32 bytes, hex) and encrypt every TOTP secret in auth.users with it on first start. The CLI does not generate the key yet, so v1.5 moves to the current release only once you set it. Setting it moves a project that has no auth lock pin (no nself.lock.yaml yet, or no auth line in it) on its next build. Keep the key with your backups: after the first start, losing or changing it locks out every user who signs in with TOTP.
- Take a verified backup:
nself backup create, thennself backup verify <id> --restore-test. Copynself.lock.yamlaside too: it pins auth 0.36.0. - Add the key to
.env.secrets:AUTH_ENCRYPTION_KEY=<output of openssl rand -hex 32>. - Move to the new release. A v1.4 project without
nself.lock.yamlsetsAUTH_VERSION=0.52.0. Every other project has an auth pin: a v1.5 build pins the image the project already runs (its first lock takes it from the existingdocker-compose.yml), and the pin wins overAUTH_VERSION=0.52.0. A pinned project moves withnself update: in v1.5 with the key,nself update --planshows the auth move with a one-way note andnself update --yesapplies it. Without the key (or in v1.4) update keeps 0.36.0, and it never moves a pin that already migrated the database back. -
nself build, thennself start. Auth migrates its schema and encrypts the TOTP secrets on that first start.
Signed-in users stay signed in: a refresh token issued by 0.36.0 is accepted once by 0.52.0, which returns a new refresh token and rejects the old one from then on (refresh-token rotation, introduced upstream in 0.38.0). Clients must store the new token; the Nhost SDKs do.
Auth migrates its schema one way, so rolling back means restoring the backup from step 1:
-
nself stop auth hasura, so neither reconnects during the restore. -
nself backup restore <id> --only pg --yes. The restore loads the backup into a scratch database and swaps it in only afterpg_restoresucceeds, so a migrated database needs nonself db reset; a failed restore leaves the current database untouched. - Put the saved
nself.lock.yamlback. A project without a lock removesAUTH_VERSION=0.52.0andAUTH_ENCRYPTION_KEYinstead (keep a copy of the key): while the key is set, v1.5 renders 0.52.0 again and re-migrates the restored database. -
nself build, thennself start.
Pinning back without the restore is unsupported: 0.36.0 starts on the migrated schema, but every TOTP sign-in fails because the secrets are now encrypted. scripts/ci/auth-smoke.sh proves the upgrade and both rollback paths on Linux with users, sessions, a refresh token and a TOTP user (NSELF_V15=1 bash scripts/ci/auth-smoke.sh; it needs Docker, Go, curl, python3 and free ports 80, 443, 4000, 5432 and 8080).
The lock carries two Hasura entries on docker.io/hasura/graphql-engine: hasura-v14 (v2.44.0) and hasura (v2.51.0, the current v2 release).
| Project | Hasura image |
|---|---|
| v1.4 |
hasura-v14: docker.io/hasura/graphql-engine:v2.44.0@sha256:... in lock mode, hasura/graphql-engine:v2.44.0 in legacy mode |
| v1.5, no lock pin yet (a new project) |
hasura (v2.51.0) |
| the project lock pins v2.44.0 | the pin, in both modes, until nself update moves it |
HASURA_VERSION=v2.44.0 (what nself init writes into .env) or blank |
the mode default, or the pin if there is one: a default, not an override |
HASURA_VERSION=v2.51.0 |
hasura in any mode; it is an override, so it drops a lock pin of v2.44.0 with one notice |
any other HASURA_VERSION (v2.45.0) |
hasura/graphql-engine:<tag>, unpinned, in any mode |
Hasura v2.51.0 does not change the metadata format or the migration contract. scripts/ci/hasura-upgrade-smoke.sh starts a project on v2.44.0 with a migrated fixture (tables and rows) and metadata (tracked tables, a relationship, a select permission, an event trigger to a webhook, a remote schema), takes a verified backup, switches the pin to v2.51.0 and checks that the rows are readable, that the metadata export is identical before and after (the nself db hasura metadata export tree and the normalised API JSON), that apply then export round-trips, and that a mutation, a subscription event, a remote schema call and an event trigger delivery work. The metadata catalog version is 48 on both releases.
- Take a verified backup:
nself backup create, thennself backup verify <id> --restore-test. Copynself.lock.yamlaside too: it pins v2.44.0. - Move the pin with
nself update(nself update --planshows the move with a note,--yesapplies it), or setHASURA_VERSION=v2.51.0for a project without a lock. In v1.4nself updatekeeps v2.44.0 and never moves a newer pin back. -
nself build, thennself start. Hasura applies its catalog migrations on that first start.
Treat the catalog migrations as one way: a Hasura release that raises the catalog version may leave an older engine unable to use the database (Hasura ships its own downgrade command for that). Rolling back means restoring the backup from step 1:
-
nself stop hasura. -
nself backup restore <id> --only pg --yes. The restore loads the backup into a scratch database and swaps it in only afterpg_restoresucceeds, so nonself db resetis needed over the newer catalog. - Put the saved
nself.lock.yamlback (the v2.44.0 pin) together with the database backup. On a project with a lock this is the route: the build that appliedHASURA_VERSION=v2.51.0rewrote the lock pin, so removing the variable from the env alone leaves the lock and the compose file on v2.51.0. Only a project withoutnself.lock.yamlcan go back by removingHASURA_VERSION=v2.51.0from the env. -
nself build, thennself start.
Pinning back without the restore is unsupported: the CLI does not check the catalog version, so a future pair of releases may refuse to start. For this pair the smoke observed that v2.44.0 starts on a v2.51.0 database (catalog version 48 on both) and passes every functional check, but rows written on v2.51.0 stay in the database and nothing restores the pre-switch state. The smoke proves both paths on Linux (NSELF_V15=1 bash scripts/ci/hasura-upgrade-smoke.sh; without NSELF_V15 it runs the same functional checks on v2.44.0 as the baseline; it needs Docker, curl, python3, openssl and free ports 80, 443, 5432 and 8080).
Caution: with no nself.lock.yaml and no docker-compose.yml (for example after nself clean), a v1.5 build has no pin to keep and starts Hasura on v2.51.0 with no notice. Take a verified backup before building an existing project with a v1.5 binary (tracked as debt).
- Edit the entry's
versionininternal/compose/images.yaml(a version bump is its own reviewed change). Leavelegacy_refalone. The weekly bump (tools/imagebump,.github/workflows/image-pin-bump.yml) does the same. -
go run ./tools/imagelock -resolvereads each index through the docker CLI (network), proves each digest addresses the index it came from, and rewrites the lock. -
go run ./tools/imagelock -checkverifies the lock offline: header, sorted, every entry has an index digest andlinux/amd64, and the lock coversimages.yaml. Adding an entry without resolving fails it. -
go run ./tools/imagelock -resolve -plugins images.jsonalso merges the plugins repository'simages.jsonv1:kind: pluginentries becomeplugin/<slug>,kind: upstreamentries keep their name,kind: cientries 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.
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.
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.
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.
Getting Started
Commands
- Commands, Overview
- Lifecycle: cmd-init · cmd-build · cmd-start · cmd-stop · cmd-restart · cmd-dev
- Monitoring: cmd-status · cmd-logs · cmd-health · cmd-urls · cmd-doctor · cmd-monitor · cmd-alerts · cmd-sentry · cmd-watchdog
- Data: cmd-db · cmd-backup · cmd-dr · cmd-queue · cmd-webhooks
- Config: cmd-config · cmd-service · cmd-env · cmd-promote
- Networking: cmd-ssl · cmd-trust · cmd-dns-setup
- Security: cmd-access · cmd-security · cmd-secrets
- Tenancy: cmd-tenant · cmd-billing
- Plugins: cmd-plugin · cmd-license · cmd-dogfood (extracted, CLI-R11) · cmd-k8s (extracted, CLI-R11) · cmd-encryption (extracted, CLI-R11) · cmd-waf (extracted, CLI-R11) · cmd-federation (extracted, CLI-R11) · cmd-mail (extracted, CLI-R11) · cmd-dlq (extracted, CLI-R11)
- AI: cmd-ai · cmd-claw · cmd-model
- Templates: cmd-template
- Utilities: cmd-exec · cmd-clean · cmd-reset · cmd-update · cmd-upgrade · cmd-version · cmd-admin · cmd-migrate · cmd-migrate-firebase · cmd-migrate-supabase · cmd-completion
Features
- Features, Overview
- Feature-Auth
- Feature-Storage
- Feature-Search
- Feature-Functions
- Feature-Email
- Feature-Monitoring
- Feature-Plugins
- Feature-nClaw, AI Assistant
- Feature-nChat, Messaging
- Feature-nFamily, Family Social
- Feature-nCloud, Managed Hosting
- Feature-Memory-Rooms, Knowledge Organization
- Feature-Agent-Dashboard, Agent Metrics
- Feature-Image-Generation, AI Image Generation
Configuration
- Configuration, Overview
- Config-Env-Vars
- Config-Postgres
- Config-Hasura
- Config-Auth
- Config-Nginx
- Shared-Proxy
- Config-Optional-Services
- Config-Custom-Services
- Config-System
Plugins (87 + 10 monitoring)
- Plugin-Overview
- Plugin-Catalog
- Plugin-Install
- Plugin-Licensing
- Plugin-Architecture
- Plugin-Dev-Guide
- Plugin-Migrations-Contract
Free (25)
- plugin-backup
- plugin-content-acquisition
- plugin-content-progress
- plugin-cron
- plugin-donorbox
- plugin-feature-flags
- plugin-github
- plugin-github-runner
- plugin-invitations
- plugin-jobs
- plugin-link-preview
- plugin-mdns
- plugin-mlflow
- plugin-monitoring
- plugin-notifications
- plugin-notify
- plugin-paypal
- plugin-search
- plugin-shopify
- plugin-stripe
- plugin-subtitle-manager
- plugin-tokens
- plugin-torrent-manager
- plugin-vpn
- plugin-webhooks
Pro (62)
- plugin-access-controls
- plugin-activity-feed
- plugin-admin-api
- plugin-analytics
- plugin-auth
- plugin-bots
- plugin-browser
- plugin-calendar
- plugin-cdn
- plugin-chat
- plugin-claw
- plugin-claw-budget
- plugin-claw-news
- plugin-claw-web
- plugin-cloudflare
- plugin-cms
- plugin-compliance
- plugin-ddns
- plugin-devices
- plugin-documents
- plugin-entitlements
- plugin-file-processing
- plugin-game-metadata
- plugin-geocoding
- plugin-geolocation
- plugin-google
- plugin-home
- plugin-idme
- plugin-knowledge-base
- plugin-linkedin
- plugin-livekit
- plugin-media-processing
- plugin-meetings
- plugin-moderation
- plugin-mux
- plugin-object-storage
- plugin-observability
- plugin-photos
- plugin-podcast
- plugin-post
- plugin-realtime
- plugin-retro-gaming
- plugin-rom-discovery
- plugin-social
- plugin-sports
- plugin-support
- plugin-tmdb
- plugin-voice
- plugin-web3
- plugin-workflows
Planned (26)
plugin-auditplugin-blogplugin-checkoutplugin-commerceplugin-drmplugin-exportplugin-flowplugin-importplugin-ldapplugin-mailgunplugin-mediaplugin-oauth-providersplugin-pagesplugin-postmarkplugin-rate-limitplugin-reportsplugin-samlplugin-schedulerplugin-sendgridplugin-ssoplugin-subscriptionplugin-thumbplugin-transcoderplugin-twilioplugin-wafplugin-watermark
Guides
- Guide-Production-Deployment
- Guide-SSL-Setup
- Guide-Multi-Tenancy
- Guide-Security-Hardening
- Guide-Monitoring-Setup
- Guide-Backup-Restore
- Guide-Custom-Services
- Guide-Migration-from-v1
Architecture
- Architecture
- Architecture-Guardrails
- Service-Graph
- Compose-Generation
- Nginx-Generation
- Plugin-Architecture
Reference
Machine interface
-
JSON-Output,
--jsonenvelope -
Command-Registry,
nself help --json - Exit-Codes
- error-codes, Error Codes
-
Compat-V15,
NSELF_V15gate
Licensing
Security
Brand
Operations
- operations/release-cascade, Release Cascade
- operations/self-healing, Self-Healing Schema
- operations/redis-tuning, Redis Pool Tuning
- operations/meilisearch-warmup, MeiliSearch Warm-Up
- operations/jwt-rotation, JWT Key Rotation
- operations/windows-wsl2-setup, Windows / WSL2 Setup
- operations/gemini-oauth-reauth, Gemini OAuth Reauth
Contributing
Admin
- USER-ACTION-QUEUE, Pending Admin Actions
All commands (21)
- A: cmd-add · cmd-admin
- B: cmd-backup · cmd-build
- C: cmd-clean · cmd-config
- D: cmd-db · cmd-deploy · cmd-doctor
- E: cmd-exec
- I: cmd-init
- L: cmd-license · cmd-logs
- M: cmd-mcp
- R: cmd-remove · cmd-reset · cmd-restart
- S: cmd-start · cmd-status · cmd-stop
- U: cmd-update