Skip to content

Latest commit

 

History

History
106 lines (93 loc) · 5.71 KB

IMAGE_CONTRACT.md

File metadata and controls

106 lines (93 loc) · 5.71 KB

Image identity contract

Slim images for postgres, storage, and edge-runtime must be interchangeable with the live docker.io pin for leftover named volumes and CLI-shaped start. Numbers are never committed here — they are generated from the pin at image-build time.

Pin

Each identity-contract service sets SOURCE_IMAGE_DIGEST and IDENTITY_SOURCE_TAG in recipe.env. UPSTREAM_IMAGE uses ${VERSION:-$SOURCE_REF} so pin selection follows the released tag: release CI always sets VERSION, so UPSTREAM_IMAGE follows it regardless of SOURCE_REF. For GitHub-tag-sourced services (storage, edge-runtime), release CI sets SOURCE_REF to the same tag as VERSION. This is not true for Docker Hub services (postgres): SOURCE_REF there is the distinct git commit pinned by the image's provenance, and release CI does not overwrite it with VERSION — it stays separate even though UPSTREAM_IMAGE pulls by VERSION. The digest belongs to IDENTITY_SOURCE_TAG, not to SOURCE_REF. Introspection, image build, and smokes pull tag@digest. A missing digest or a failed pull is a hard error. When the image tag is not IDENTITY_SOURCE_TAG, that tag's index digest is resolved — the committed digest is not reused across versions. There is no ECR-first fallback.

SKIP_UPSTREAM_IDENTITY=1 is rejected for identity-contract image builds and image smokes. Never invent uid/gid/mode as a substitute for the pin. Storage /mnt is the one exception: invent 0:0:755 only when the path is absent and the pin starts as root. If the path exists but stat failed, fail. A test -e status other than 0 or 1 is a probe failure, not "absent".

Generation

scripts/introspect-upstream-identity.sh (and scripts/identity-lib.sh) pull the digest, then probe the pin:

  1. Config.User (empty means root).
  2. Owner/mode of the CLI mount path on the image filesystem (stat via a bind-mounted busybox so the pin does not need a shell).
  3. The /etc/passwd / /etc/group lines for that owner.

Those values become Docker ARGs. scripts/render-dockerfile.sh stays append-only for runtime.env — it does not rewrite COPY --chown.

Service policy

  • Start user matches the pin. Empty Config.User, 0, and root are the same (euid 0): docker.io leaves USER unset; distroless's root variant bakes USER 0.
  • postgres starts as root, then drops to the probed owner before postgres execs. docker-entrypoint.sh is the image ENTRYPOINT and follows official argv rules: empty / leading - stays the server path (postgres -D /etc/postgresql); any other argv is exec'd as-is so db dump / db pull (bash -c) do not initdb. First boot runs /docker-entrypoint-initdb.d (.sh / .sql) in the temp-server window after bundle initdb. Always run bundle migrate.sh first (roles + extensions); then the CLI --from-backup restore in initdb.d. Hide /etc/postgresql.schema.sql during the bundle pass when initdb.d/migrate.sh is the CLI restore (root copies the contents aside, truncates the inode, and chowns the copy — no mv after drop, and sticky /tmp only lets the owner unlink). Both would run the unconditional CREATE DATABASE _supabase. A real db dump --local has no CREATE ROLE and assumes extensions exists. --help / --version exec postgres directly (no initdb). A non-zero sourced or exec'd *.sh stops the temp server and exits 1 — do not start postgres. The shim uses busybox-su, not extracted upstream gosu. Cluster files stay in PGDATA. /etc/postgresql/postgresql.conf sets data_directory / hba_file / ident_file at PGDATA, includes the bundle recipe (not leftover initdb conf), overrides unix_socket_directories to /run/postgresql,/tmp (native local-dev.conf stays /tmp only), and stays root-writable for CLI >> / > writes. Create /run/postgresql at start (/run is often tmpfs) and chown it to the drop-to uid. The image provides static GNU cp at /usr/local/bin/cp for CLI snapshots of stopped database files. The separate storage helper container invokes other filesystem applets through /usr/bin/busybox.
  • storage and edge-runtime stay root. Seed /mnt and /root from the probe. Ship wget (storage) and sh (edge-runtime) unconditionally because the CLI always uses them. Storage ships dist/scripts/migrate-call.js (third Rolldown input) plus postgres-migrations' 0_create-migrations-table.sql beside that script. Empty ENTRYPOINT and CMD ["/slim-runtime/bin/storage"] so default docker run still serves, while docker run --entrypoint /slim-runtime/bin/prepare IMAGE runs the migration one-shot. Both launchers use the same portable /slim-runtime layout.

Do not extract upstream docker-entrypoint.sh or gosu. Do not root a stateless service that the pin does not start as root.

Smoke

Pairwise image smokes pull the same digest the build used. They fail closed on pull miss. They assert identity (USER + mount owner/mode), leftover volumes in both directions, and a CLI-shaped start for postgres (docker-entrypoint.sh postgres -D /etc/postgresql, plus an initdb.d marker, a --from-backup overwrite of initdb.d/migrate.sh plus /etc/postgresql.schema.sql that must run after bundle migrate without double-creating _supabase, a failed init script that must not start postgres, --version that must not initdb, default-socket psql with no -h, and a foreign-argv id that must not initdb). Storage leftover also checks that the imgproxy pin's Config.User can read objects on /mnt. Edge leftover write/read of /root runs inside the pin (run_in_pin with the named volume mounted). Static busybox is only that container's entrypoint when the pin has no shell — not a separate image.