Skip to content
Pages in Guides

Your own recipes, and signed catalogue packages

Add your own recipes beside the catalogue, grow it with signed packages, and how its versions are kept current.

Guide Updated View as Markdown

Part of Recipes.

Your own recipes

The catalogue reads three places through one schema: the package, a signed catalogue package (below) and your folder, /etc/noust/recipes.

Shell
noust recipe check ./my-recipes             # the schema and the lint, without adding anything
noust recipe add ./my-recipes/myapp.yaml    # keep it; its logo and files are in assets/myapp/ beside it
noust recipe add ./my-recipes --replace     # overwrite one of yours with the same name
noust recipe remove myapp

A private recipe is validated like a shipped one (an unknown key, an image on a floating tag or without a digest, a link or a logo the console's policy would refuse are errors that name the field), and a name the package holds is refused: a private recipe has a name of its own. It is shown as yours, with where it came from, and installed and updated like any other. The folder is read only when root (or the user running Noust) owns it and nobody else can write it, and no link is followed; one bad file is named in noust recipe list and in the API's problems and does not hide the rest. scripts/recipe_lint.py runs the same lint on a folder you are writing, and in CI on the shipped catalogue. The API is POST /api/recipes/private/check, POST /api/recipes/private and DELETE /api/recipes/private/{name} (sudo mode, the server's permission); the Recipes view's Add a recipe checks one you paste and keeps it, and a recipe of yours says so on its card and can be removed from its page.

Signed catalogue packages

The catalogue can grow between releases from a package, never from a poll: nothing contacts a server on its own.

Shell
noust recipe catalog trust acme 5d3c...64 hex digits...   # trust a publisher's Ed25519 key
noust recipe catalog import ./recipes-2026-10-13.tar.gz   # reads a file, needs no network
noust recipe catalog fetch https://example.org/recipes.tar.gz   # the one step that downloads (https only)
noust recipe catalog show                                 # which package, who signed it, whose keys are trusted
noust recipe catalog remove                               # back to the recipes this release ships

A package is a .tar.gz of recipes whose manifest lists every file with its SHA-256 and is signed with Ed25519 by a key you trust (/etc/noust/recipe-catalog.keys, plus the maintainers' keys a release adds). It is data, never code: the archive is read into memory under size limits, a link or a path outside the layout is refused, every recipe passes the schema and the lint, the serial must be higher than the one in place (--force accepts one that is not), and nothing in it runs. A recipe of the package replaces a shipped one only while it is newer; a release that ships a newer one wins again. scripts/recipe_catalog.py keygen|pack|verify is how a publisher makes one; the archive is deterministic. The API is GET /api/recipes/catalog, POST /api/recipes/catalog/fetch and DELETE /api/recipes/catalog (sudo mode); the Recipes view's Catalog package shows the one in place and fetches or removes it.

Updating the catalogue

When upstream releases a version:

Shell
# 1. the new tag's digest, read from the registry
python scripts/recipe_pins.py resolve ghcr.io/dani-garcia/vaultwarden:1.38.0
 
# 2. in the recipe: the image reference, `version`, `updated`, and a `changelog` entry;
#    keep `revision` at 1 for a new version, raise it for a fix to the recipe itself
# 3. check every pin of the shipped recipes against where it is published
python scripts/recipe_pins.py check --evidence /tmp/pins
# 4. start the stack on your Docker and ask the health gate's question
python scripts/recipe_smoke.py vaultwarden --evidence /tmp/smoke

The lookups of writing a recipe are in one tool, scripts/recipe_draft.py, so that none is typed from memory: tags IMAGE lists the release tags of an image (newest first), release OWNER/REPO the latest release of a project, pin FILE --to OUT adds to every image: the digest its registry serves (reusing the digest another recipe already pins for the same tag, so two recipes on postgres:17.11-alpine are one image on a server) and writes the architectures: every image is published for, logo NAME TITLE draws the monogram, and template NAME prints a Coolify template to read next to the project's own documentation.

The smoke test is meant for a machine whose Docker is shared: --project-prefix noust-test-me- (for example) names the Compose project (<prefix><recipe>), everything it creates (containers, volumes, the network) carries the label noust.test=1 so that a run is found and removed by that label and that prefix and never by a prune, the loopback port it asks for must be free, and a run stopped with SIGTERM (timeout sends it) still removes what it made. A container that finished with an error is a problem, not only one that restarts.

scripts/recipe_docs.py writes the table of the catalog from the recipes, and --check fails if it is out of date.

check resolves each image's tag in its registry and compares the digest, reads the architectures it is published for, tests each archive's SHA-256 (or the checksum the project publishes beside it), and looks for each git tag, failing on any difference; --evidence writes what was read, per recipe, as JSON. It makes only anonymous, read-only requests.

Then read the release notes for what the new version asks of an update (a database migration, a new variable, a new port) and put it in the recipe: a new variable is declared, and changelog says what changed. The tests load every recipe, so a typo cannot ship.

Something on this page is wrong or unclear? Report it on GitHub.

Help, contributing and the project’s policies are under Project.

Search the documentation

Type to search every page.

↑ ↓ to move, Enter to open, Esc to close