WarmblyDocs

Updates

How a self-hosted instance learns about a new Warmbly version, what the indicator in the admin panel means, and how Update and restart pulls, rebuilds and restarts the stack for you.

A self-hosted Warmbly checks for a newer version on its own and shows it in the admin panel. Applying it is one button when the updater runs next to the stack: the checkout is pulled, every image is rebuilt, the services that changed are recreated, and the panel reconnects once the backend answers again. Nothing in flight is lost: sending and syncing pause for the restart and resume from the database on the other side.

The indicator

The top bar of the admin panel (:5174) carries a version pill next to the environment label. It reads the running backend's build identity, so it shows exactly what is deployed, not what the checkout says.

PillMeans
v1.4.0 (grey)Up to date. Click it for the details and a manual check
Update to v1.5.0 (amber)A newer release exists on the configured channel
Update available (amber)The checkout is behind its branch (an install that tracks main)
Updating (blue, spinning)An update job is running; the pill follows it from any tab
Restarting (blue, spinning)The backend is coming back after an update

The same facts sit at the top of Instance > Setup and health, and an available update is also a finding there (update_available, info severity), so a page that only lists findings still tells you.

In the dashboard

Every member of a self-hosted workspace sees the same version pill in the dashboard header, next to the plan badge: the running version in grey, or "Update to vX.Y.Z" in amber with a pulsing dot once a newer release exists. It is there so nobody has to ask which version the server runs, and so the people who cannot update still know one is waiting.

Who can act on it follows platform admin access, not workspace roles:

  • Members see a badge. Its tooltip names the version and says to ask a platform admin.
  • Platform admins click it and get the update dialog: the running and available versions with the release notes, the checkout and updater state, a "Check now" button, and Update and restart. That button leads to a confirmation pane that spells out what the update does (pull, rebuild and restart, sending pauses and resumes, migrations apply, the tab reconnects) before anything runs.

While the update runs the dialog shows a progress bar, the step list with the live step highlighted, and the log behind a toggle. When the backend goes away for the restart the dialog says it is reconnecting and keeps polling; the pill in the header turns into a spinner so the job stays visible with the dialog closed, and a reload picks it back up. When the new backend answers, the dialog shows the result and reloads the dashboard after a short countdown, or, if the dialog was closed, a toast reports the new version and every list refreshes.

The dashboard reads GET /auth/instance for the version (any member) and the admin endpoints below for the rest (platform admins only, the same permission gates as the admin panel).

What counts as newer

Two signals, and either one lights the pill:

  • A release. The newest release of RELEASES_GITHUB_REPO on UPDATE_CHANNEL (stable, or dev to include prereleases) is compared with the running build's version. A build stamped v1.4.0-3-gabc1234 (three commits past the v1.4.0 tag) is treated as newer than v1.4.0 and older than v1.4.1, so an instance built from main is not nagged about the release it already contains.
  • Commits. When the updater runs, it fetches the remote on UPDATER_FETCH_INTERVAL and reports how far the checkout is behind its branch. Any distance counts as an update on an install that tracks a branch.

A build that carries no version (an image built without the build arguments, reported as dev) cannot be compared with a release, and only the commit distance applies.

The version comes from the binary itself: the Dockerfiles and make up stamp the tag, commit and build time in (internal/version), CI does the same for published images, and warmblyctl status prints it as the first line.

Update and restart

The button appears when the updater is reachable and something newer exists. It needs the manage_settings admin permission, and every press is an audit row (upgrade on instance).

What happens, in the order the dialog shows it:

  1. Fetch. git fetch --tags --prune on the checkout.
  2. Pull. On a branch, a fast-forward merge of the remote branch. On a checkout pinned to a tag, a checkout of the newest release. A checkout with local modifications is refused rather than merged over; commit or stash them, or set UPDATER_ALLOW_DIRTY=true.
  3. Build. docker compose build for every service, with the version stamped in.
  4. Restart. docker compose up -d --no-build for every service the checkout defines plus everything running, minus the updater itself. Compose recreates only the containers whose image or configuration changed, so Postgres, Redis and NATS stay up, and the backend applies migrations as it boots.
  5. Clean up. docker image prune -f, unless UPDATER_PRUNE=false.
  6. Wait for backend. The updater polls /health until it answers, for up to six minutes, and marks the job as succeeded or failed.

If the updater's own image changed, it recreates itself last, after the outcome is already on disk, so the new updater reports the finished job.

The dialog streams the log. Close it and the pill keeps following the job; reload the page and the pill picks the job up again. When the backend answers with a new build, a toast says which version is now running and every list refreshes.

Migrations are forward-only, so an update never needs to be rolled back to be safe with data in place. Read backing up anyway: a backup before an update is the one you are glad to have.

The updater holds the docker socket

Inside the compose stack the updater mounts /var/run/docker.sock, which is root on the host. That is why it lives behind the updater compose profile and answers only to a bearer token the backend holds, on the compose network, with no published port. The backend gates the button on an admin permission and audits it. If any of that is more trust than the host should extend, leave the profile off and update by hand.

Enabling the updater

Docker Compose

make up starts the stack with the updater profile, so a stock install already has the button. For a plain docker compose up, put this in .env:

COMPOSE_PROFILES=updater

The service mounts the checkout at its own host path (WARMBLY_REPO_DIR, $PWD by default) so the compose file's relative paths resolve the same way inside the container, and shares INTERNAL_API_TOKEN with the backend as UPDATER_TOKEN. Nothing else to configure.

Files git writes as root are given back to the checkout's owner after each pull, so your own git pull on the host keeps working.

To update by hand instead: make upgrade (a fast-forward pull, then make up). To leave the button out entirely, start with make up UPDATER=false and set UPDATER_URL=none in .env; the panel then only reports.

Without Docker

On a bare-metal install the updater is a systemd unit that runs scripts/upgrade-bare-metal.sh after pulling. The script builds every artifact this host runs as the checkout's owner, then hands off to one privileged installer, warmbly-install-release, which copies the artifacts into /opt/warmbly, keeps each frontend's config.js, restarts the backend first and the rest once it answers.

cd /opt/warmbly/src
go build -o out/updater ./cmd/updater && sudo install -m 0755 out/updater /opt/warmbly/bin/
sudo install -o root -g root -m 0755 deploy/systemd/warmbly-install-release.sh /usr/local/sbin/warmbly-install-release
sudo install -m 0644 deploy/systemd/warmbly-updater.service /etc/systemd/system/
printf 'UPDATER_TOKEN=%s\n' "$(grep ^INTERNAL_API_TOKEN= /etc/warmbly/warmbly.env | cut -d= -f2-)" | sudo tee /etc/warmbly/updater.env >/dev/null
sudo chmod 0600 /etc/warmbly/updater.env
sudo systemctl daemon-reload && sudo systemctl enable --now warmbly-updater

The unit runs as the user who owns the checkout (deploy as shipped; edit it). The only root step is the installer, so that user needs exactly one sudoers line, and nothing broader:

echo "deploy ALL=(root) NOPASSWD: /usr/local/sbin/warmbly-install-release" | sudo tee /etc/sudoers.d/warmbly-upgrade >/dev/null
sudo chmod 0440 /etc/sudoers.d/warmbly-upgrade

The installer is root-owned and not writable by that user, takes no arguments, reads only from fixed paths under the checkout and refuses symlinks there, so owning the checkout does not become owning the host. The binaries it installs run as the unprivileged warmbly user either way.

Then point the backend at the updater in warmbly.env and restart it:

UPDATER_URL=http://127.0.0.1:8095

The same script works by hand: scripts/upgrade-bare-metal.sh --pull.

Configuration

Backend:

VariableWhat it doesDefault
UPDATE_CHECK_ENABLEDPolls GitHub Releases and shows a newer version in the paneltrue
UPDATE_CHECK_INTERVALHow often the release check runs; minimum 5m30m
UPDATE_CHANNELstable follows releases; dev also offers prereleasesstable
RELEASES_GITHUB_REPOThe owner/repo whose releases are Warmbly versions. Point a fork's instance at the forkwarmbly/warmbly
RELEASES_GITHUB_TOKENOptional; only raises the GitHub API rate limitunset
UPDATER_URLThe updater. Unset, or none, leaves the panel report-onlyhttp://updater:8095 under compose
UPDATER_TOKENBearer token presented to the updaterINTERNAL_API_TOKEN

Updater:

VariableWhat it doesDefault
UPDATER_TOKENThe token it accepts; falls back to INTERNAL_API_TOKEN. Refuses to start without oneunset
UPDATER_MODEcompose rebuilds and recreates the compose project; command runs UPDATER_COMMANDcompose
UPDATER_COMMANDThe build-and-restart command for command modeunset
UPDATER_REPO_DIRThe checkoutworking directory
UPDATER_REMOTEThe git remote to fetch and pull fromorigin
UPDATER_COMPOSE_PROJECTThe -p the stack was started withwarmbly
UPDATER_COMPOSE_PROFILESExtra profiles on every compose call, so its own image is rebuiltupdater
UPDATER_BACKEND_HEALTH_URLPolled after the restart until it answershttp://backend:8080/health
UPDATER_FETCH_INTERVALHow often it fetches for the commits-behind count30m
UPDATER_PRUNEdocker image prune -f after a successful updatetrue
UPDATER_ALLOW_DIRTYUpdate over local modifications in the checkoutfalse
UPDATER_STATE_DIRWhere the last job is kept across restarts/var/lib/warmbly-updater
UPDATER_ADDRListen address:8095

Endpoints

EndpointAuthReturns
GET /admin/instance/updateplatform admin, view_analyticsRunning build, latest release, whether an update is available, updater and job state. ?log=1 includes the job log
POST /admin/instance/update/checkplatform admin, manage_settingsRuns both checks now and returns the state
POST /admin/instance/update/applyplatform admin, manage_settingsStarts an update job ({"target": "latest"} or a release tag) and returns it. The backend restarts as part of it

The updater's own API (GET /status, POST /check, POST /update) is bearer-authenticated and meant for the backend only.

Findings

Two instance health checks belong to this page:

  • update_available (info): a newer version exists, with the version and how to apply it.
  • updater_unreachable (warning): UPDATER_URL is set but nothing answers there, so the button cannot work. Start the updater, fix the address, or remove the variable to update by hand.

Workers on other machines

This page is about the control plane. Remote workers installed from the panel keep their own daily self-update timer and the Pull latest and restart action on the worker's page; see day-2 operations.

On this page