Skip to content

Reproducibility and upgrades

A workbench should be reproducible, the same yml building the same container every time, and the features inside it should not be weeks out of date. Those two goals pull against each other, so Monoceros splits them along one line: the yml.

Where your yml names a version, you decided, and nothing moves it behind your back. Where it names none, there is nothing to hold on to, so apply keeps that part current for you.

New to Monoceros? Start with Installation.

  • The runtime base image (Debian, Node, system tooling) is pinned in the yml and moves only when you upgrade. That is what makes a workbench reproducible.
  • The features carry no version in your yml, because a pinned cloud client goes wrong on its own as the API moves. They are brought up to date on every apply, before you get a shell.
  • The service images (Postgres, Keycloak, Redis) sit in between. Your yml carries the tag, so you can see it and change it, and upgrade moves it to the one the catalog ships today. That is how a security fix in an upstream image reaches a workbench you built months ago. An apply leaves it alone, because your data lives there.

Agent plugins work like the features: they live outside the image in ~/.claude, and every apply pulls their marketplace again and updates them.

Without a pin, the base image follows a floating tag and is re-derived on every apply. A routine apply - adding a repo, tweaking a port - could then pull a newer image and change the toolchain under a container that already holds real work. That is silent, non-reproducible drift.

So init writes the current runtime version into the yml, version only:

acme.yml
schemaVersion: 1
name: acme
runtimeVersion: 1.8.0

apply builds against exactly that version. The same yml yields the same container on any machine.

A feature installs its tool at build time, and that lands in a cached image layer. The cache key is the install instruction (install the latest), not the version it resolved to, so once the layer is built the version is frozen. Every later apply builds from that layer, and every new workbench with the same features reuses it, even months later. This is not specific to Monoceros or to dev-container features; it is how layer caching works for any build-time install.

The second half of it costs more. An apply recreates the container from the image, so a tool that updated itself while you worked loses that update and drops back to the build-time version. Without a refresh, a re-apply would take away capabilities you had been using the day before.

So each of those features checks itself when the container starts, and installs only when there is something newer. The usual apply costs a version lookup and no download, and apply tells you what it found:

Features refreshed
• claude 2.1.247 (updated from 2.1.240)
• gh 2.60.1 (updated from 2.55.0)

Nothing here can fail your apply. With no network it says so and carries on with the tool from the image.

  • apply keeps your features current and otherwise builds from the cache. It never bumps the runtime and never moves a service image on an existing workbench.
  • monoceros upgrade moves the parts that hold state or sit underneath everything: it bumps the runtime base when a newer one exists, rebuilds the feature layers from scratch, brings the services up to the catalog’s images, and prunes the stale images it replaced.
Terminal window
monoceros upgrade acme

The features keep themselves current, but the base image and the service images wait for you to say so. apply prints a one-line reminder when the last upgrade was a long time ago:

Base image and service images last updated 34 days ago. Run `monoceros upgrade` to move them.

The threshold is 30 days by default; set upgrade.staleDays in monoceros-config.yml to change it.

That reminder is about what is inside a workbench. The monoceros CLI binary is a separate thing, and it keeps itself honest too: commands check (at most once a day, in the background, never blocking) whether a newer version is published, and print a one-line notice when there is one:

⬆ Monoceros 1.31.0 is available (you have 1.30.0).
Update: curl -fsSL https://raw.githubusercontent.com/getmonoceros/workbench/main/installer/install.sh | bash

Updating is the same install script you used the first time - it re-runs the checks (Docker, Node, completion) and pulls the new CLI. To silence the notice, set MONOCEROS_NO_UPDATE_NOTIFIER=1 in your shell. It is also quiet automatically in non-interactive contexts (CI, pipes).

monoceros upgrade with no name refreshes every running workbench. Stopped or never-built ones are skipped, so an upgrade never starts a container you had stopped or builds a config you only init-ed. Name one to refresh it explicitly: monoceros upgrade <name>.