Updating code
See also: Updating ontology · Updating data · How a code update works
Dédalo is an active, rapidly developing software project. It is therefore important to keep it updated and in good condition for stability and security reasons.
There are two ways to update the server's code: the maintenance-panel self-update (below), driven by a configured code server, and a manual git pull (see Updating manually). Both operate on this install's own tree — there is no separate install they delegate to.
The update process is based on the Dédalo cadence numbering. Versions are installed step by step — the updater refuses to skip any version, patch releases included: the only target it accepts is the very next rung — and a code version sometimes depends on the ontology version. Update the ontology before updating the Dédalo code, following this guide.
Updating the Dédalo code should be supervised by the IT team. Some changes — such as new configuration settings — must be applied manually in ../private/.env, because neither update path touches your configuration.
Update pre-production system and test before update new versions into production system
Is highly recommended to test your new Dédalo installation before deploying the changes into the production environment. This will help ensure that the update will not have a negative impact on your catalogue.
Panel self-update
The "Update code" maintenance panel (update_code widget,
src/core/area_maintenance/widgets/update_code.ts) downloads a release
archive from a configured code server, verifies its sha256 checksum,
pre-validates every archive entry, extracts it into a quarantine directory,
installs its dependencies and boot-tests it there, and only then swaps it
onto the live tree — never over the live tree directly. The swap is always a
clean, rename-based replacement with the previous tree kept as a backup; see
How a code update works for the phases, the
safety gates and the rollback contract.
A release is installed only against a declared checksum
Every release the code server advertises carries the sha256 of its archive, and the update refuses outright if that checksum is missing, malformed, or does not match the bytes received. An archive that cannot be verified is never extracted.
It requires a process supervisor (systemd, Docker, pm2, …) so the
server can restart itself onto the new code: set DEDALO_SUPERVISED=true, or
run under a supervisor that sets INVOCATION_ID/JOURNAL_STREAM
(systemd does this for you). Without a detected supervisor the update
refuses rather than risk a self-exit with nothing to restart it.
The update refuses to start — it does not merely warn — unless the operator
is the superuser (root), the server is in maintenance mode, and a
recent database backup exists. The backup requirement is the one an
operator can waive: when the backup is stale or missing, the version modal
offers an Update without a recent database backup checkbox above the Update
button, and every waiver is logged loudly in the server log with the requesting
user. It waives the database backup only — the code backup is the swap itself
and is never optional. It also
refuses, without changing anything, on a number of unsafe situations — runtime
data inside the code tree, an unaccounted file at the tree root, a
containerised (image) deployment, a Bun version mismatch, a failed dependency
install or pre-flight boot of the new tree; see
How a code update works.
What the panel tells you before you start
Opening the panel shows the installation's own status, so the answer to "can this install take an update?" comes before the button, not from a failed run:
- This installation — the running version, whether it is a release build or a development checkout, the release's build date and commit, the code tree and the backup root.
- Update readiness — one line per condition the update actually refuses on, each marked ok, warning or blocked, with the reason: supervisor detected, deployment channel, maintenance mode, superuser identity, recent database backup, backup root outside the code tree, runtime data outside the code tree, the archive tools, the Bun version pin, a leftover staging directory, and the free disk space where the update stages. The panel is ready only when nothing is blocked. A stale or missing database backup is a warning, not a block, because the update can be run with a waiver — so the headline reads Ready to update, but only with a waiver rather than a plain Ready to update, and the waiver checkbox waits in the version modal. Other warnings (a Bun pin drift, a leftover staging directory) do not change the headline: nothing has to be waived for them.
- Last code update — which version replaced which, when, and whether the new tree confirmed itself at boot. A status still reading pending confirmation means the update did not complete its own health check.
- Restore points — the code copies on disk, each with the Dédalo version it holds and a Restore button that puts it back on the tree (see Restoring a previous code version). Each row is marked bootable or incomplete (a copy without its dependencies cannot be started again). The Restore button is disabled, with the reason written under the row, when the copy is incomplete, when it declares no Dédalo version (its provenance cannot be read), or when it pins a different Bun than the one this server runs — install that Bun first, as for an update. In that last case the row also names both versions ("This copy pins Bun 1.3.9; this server runs Bun 1.4.0. Install Bun 1.3.9 to restore it."), so you can read which runtime to install off the row itself, and tell apart several restore points cut on either side of a Bun change. A development checkout gets no Restore button at all, for the same reason it gets no Update button — nor a Delete button, for the same reason again. Every other row carries one (see Reclaiming disk).
Some readiness lines cannot be decided in advance and say so rather than guessing: the release's own root file list and its Bun pin are only known once the archive has been downloaded, and how much disk an update needs is measured when it starts (walking the whole tree is too slow for a panel). For those the panel reports the inputs — the count of root entries in the live tree, the running Bun and this tree's pin, and the bytes free where the update stages.
When a release changes the Bun version
The update refuses — without changing anything — if the release pins a different Bun than the one this server is running. Install the pinned Bun on the machine first, restart Dédalo onto it, and only then apply the update:
# the version the release pins is shown in the panel's readiness list
curl -fsSL https://bun.sh/install | BUN_INSTALL=$HOME/.bun bash -s bun-v1.4.0
Then point the service at that binary (ExecStart, see the production
guide), restart, and confirm the boot line reads
starting on Bun 1.4.0 (pinned: 1.4.0) with no mismatch warning.
-
Close access to the work system.
Change the Dédalo status to maintenance — the update refuses to run otherwise. Follow this guide to change the Dédalo status and disable Dédalo access.
-
Enter the maintenance panel.
Log in as root user and go to the Maintenance panel, located in:
System administration -> Maintenance
Make a backup of the database first — the update refuses without a recent one. Follow this guide.
-
Locate the "Update code" control panel.
Choose the server to obtain the code. By default, the panel shows the official Dédalo server, but you can configure other mirrors or providers via
CODE_SERVERS, set in../private/.env(see the Configuration Administrator Guide).Press "Check available updates", choose the version you want, and press
Update. The panel then shows the pipeline's phase track (download → verify → extract → deps → preflight → swap → restart → health) while the update runs; the server restarts itself during therestartphase and the panel polls its health endpoint until the new version answers.The panel scrolls that track into view when the run starts and keeps it readable without any scrolling on your part: the progress bar stays pinned while the run lasts, the phase rows scroll inside their own box rather than filling the window, and the live process output — and whatever sentence ends the run, whether it succeeded, refused or lost the connection — stays visible underneath. If a run appears to stop with nothing to show, the panel is not hiding the reason further down; look instead at the server log.
Confirm success by both the engine version and the build stamp changing in the panel's readout — the build stamp is the release's commit date, so an unchanged stamp means old code is still running.
Re-login after update
After the update the panel prompts for a reload: the browser still holds the previous client code, and only logging in again loads the new one. If you dismiss the prompt, a persistent "Reload required" note with its own button remains.
-
Check for new settings.
Some code updates add or change configuration settings; the "Check config" control panel flags these. Settings live in
../private/.env— compare it against../private/sample.envand add any new key(s). See the Configuration Administrator Guide. -
Follow the update instructions and update data.
Locate the update code panel and check if it indicates further instructions. Some code updates require a data process; follow update data.
-
Open access to the work system.
Revert the maintenance status to
false. -
Log out and re-login with a normal user.
Restoring a previous code version
Every update renames the outgoing tree aside as a restore point, kept with its dependencies so it can be started again with no network. The panel's Restore button puts one of those copies back on the live tree, running the update's road backwards: same preconditions, same phase track, same rollback sentinel, same restart.
Restoring code does NOT revert database migrations
A restore moves code only. Migrations already applied by the newer version stay applied, and older code running against a newer database can fail. When the outcome that is actually wanted is the previous state, the database backup has to be restored separately — see Backup. The panel refuses a restore across versions unless that hazard is explicitly acknowledged (below), and logs the acknowledgement.
Restoring is a move, not a copy: the restore point is consumed, and the tree that was running is renamed aside as a new restore point. Nothing is deleted, disk usage does not double, and the move can be undone the same way.
-
Close access to the work system — maintenance mode, superuser, as for an update (maintenance status). A restore refuses on the same preconditions, except the recent-backup one: it has its own confirmation instead.
-
In the "Update code" panel, find the copy under Restore points and press
Restore. -
Read the confirmation modal. It names the version running now, the version the copy holds, and the copy's name.
When the two versions differ, the modal adds a checkbox that must be ticked before the Restore button unlocks: it acknowledges that the installation will run the older code while the database stays as it is. At an equal version — a developer build restored over itself — no checkbox is shown, because there is nothing to acknowledge.
-
The phase track runs as for an update, with
download,verify,extractanddepsreported skipped (a restore fetches and installs nothing — the copy carries its own dependencies). The server restarts itself and the panel polls its health endpoint. -
Confirm the result in the panel's readout. Across versions the version changes; for a developer build restored over the same version the value to watch is Installed archive, the digest of the tree now running.
The copy is boot-tested first — when it can be
Before the live tree is moved, the restore point is started once in
isolation and must answer a health check, exactly like a new release in
quarantine. A restore point cut before 2026-08-23 does not honour the
smoke-boot flag: booting it would run its full boot — migrations,
schedulers, diffusion runners, watchers — against the live database while
this server is still serving. Such a copy is therefore restored without
that check, the preflight phase is reported skipped rather than passed,
and the server log says which copy it was and why.
The refusals a restore can produce are listed in How a code update works.
Running a code server
A Dédalo install can publish releases for other installs — the official server,
or an institution's own mirror. Set in ../private/.env (see the
Configuration Administrator Guide):
| Setting | Purpose |
|---|---|
IS_A_CODE_SERVER |
Answer release manifests at all. Without it every request is refused. |
DEDALO_CODE_FILES_DIR |
Where release archives are stored, <major>/<major.minor>/<version>.zip. |
DEDALO_CODE_SERVER_GIT_DIR |
A git checkout of the engine, only needed to BUILD releases. A pure mirror does not need it. |
CODE_SERVERS |
Must include this server's own entry: the code in it is the shared secret a caller has to present. |
DEDALO_CORS_ALLOWED_ORIGINS |
The origins allowed to read the manifest. Each client fetches it from the browser, so without this the update panel of every remote install fails with a network error. Use * for a public master. |
What the panel tells a code server
On a code server the panel adds a second status block, answering whether this instance can publish at all:
- Code server — the role flag, the two directories, and whether the build
itself would be accepted, checked through the same planner the Build buttons
use. Also whether a
masterref exists (only amasterbuild claims the published release name), whether the worktree is clean, and whether an archive of it carries symbolic links. - Build source — the commit, its date and the branch currently checked out, plus the checkout's Bun pin.
- Release ref — the ref a published release is actually built from
(
master), its own commit and date, and how many commits the checked-out branch has that it does not. Every check in the first block reads this ref, not the checked-out branch, and each says so ("checked against master"). - Published releases — every archive already on disk with its size and
date, marked published or developer, and flagged when its
.sha256sidecar is missing (without it a remote install has no digest to verify). - Offered to an installation at this version — the release list a remote install would actually receive. This is not the same as the archives on disk: a version is offered only when it is the next step of the upgrade path, so a perfectly good archive can be present and still not be offered. The panel shows both so the difference is visible rather than inferred.
A dirty worktree does not stop a build
A release archives the committed HEAD of the configured checkout.
Uncommitted changes are simply absent from the archive, which is why the
panel marks a dirty worktree as a warning before you press Build.
The checks read the release ref, not your branch
Work committed on a working branch is not in a release until it is merged into the release ref. Until then the publish checks keep reporting the old state — correctly, because that is what a release built now would contain. The panel names the ref on every such line, and counts the commits the release ref is missing, so a check that looks like a false alarm can be told apart from a real one.
An archive with symbolic links cannot be installed
The installer refuses an entire archive that contains a symbolic-link
entry. If the panel marks this blocked, releases built from this checkout
will be refused by every install that downloads them — exclude those paths
from the archive (export-ignore in .gitattributes) before publishing.
Building a release
On a code server, the "Update code" panel shows two extra buttons, "Build
master release" and "Build developer release". Each archives a branch of the
configured git checkout at the engine's current version. A build of the
master branch writes the published release name; a build of any other branch
gets a -dev suffix, so it can never overwrite the published master release
of the same version:
<DEDALO_CODE_FILES_DIR>/<major>/<major.minor>/<version>.zip (master)
<DEDALO_CODE_FILES_DIR>/<major>/<major.minor>/<version>-dev.zip (any other branch)
Each archive is written together with its .sha256 sidecar — that sidecar is
what remote installs verify against; keep the two files together, because an
archive without its sidecar cannot be installed.
Beside each button the panel shows the archive that button writes, and it reports the write rather than leaving you to infer it. While the build runs the row is marked building… and its facts are dimmed, so it is clear that the name, size and date beside the button are about to stop being true. When it finishes the row is re-read from disk and marked updated just now, with the value it replaced spelled out underneath. That before-value is the point: a build rewrites the archive in place and the new size is usually identical to the old one, so the timestamp is the only thing that moves — and one timestamp on its own tells you nothing.
It shows only what moved, so the line stays short and the difference is the
part you read: the size appears only when it changed, and the date only when
the build crossed midnight. A rebuild minutes after the last one therefore
reads was 11:41:43 beside a row dated 173 MB · 28/08/2026, 11:45:09.
A build that changes nothing says so
If the archive's timestamp does not move, the row reads unchanged: the build wrote no new file instead of claiming an update. Read the build's own response underneath the panel for the reason.
Developer builds are only offered when both sides ask for them
A -dev archive is never offered to an install that did not ask for one.
Two switches must be on: this code server must set
DEDALO_CODE_SERVER_DEV_CHANNEL=true, and the receiving install must tick
Developer builds in its own "Update code" panel. A code server that
leaves the setting unset answers a developer-channel request exactly as it
answers a normal one. The archive is served at its own URL either way
(/dedalo/install/code/<version>/<version>-dev.zip).
Reclaiming disk
Every update keeps the outgoing tree with its node_modules — that is what
makes a rollback a bare mv that boots with no network, and it means each
restore point is a full copy of the code tree on the same disk the update
measures before it starts. They used to accumulate for ever.
Retention runs by itself. DEDALO_CODE_RESTORE_POINTS_KEEP (default 3)
prunes the oldest points down to that many. It runs after an update the
booted tree has confirmed, never at the moment of the swap: until that flip
the new tree is unproven and the points behind it are the way back. Pruning is
best-effort and reported in the server log — a disk that will not give a
directory back never fails a boot that is otherwise healthy.
Deleting one by hand is the escape hatch, for what retention does not
reach: a corrupt copy, a leftover, or an installation that needs the space now.
Each row carries a Delete button beside Restore; it asks for confirmation
naming the point, and removes that copy from disk. It is irreversible.
The newest bootable copy cannot be deleted
That copy is the rollback for the code running right now. Its Delete button is disabled with the reason under the row — not a confirmation you can click through, because a dialog is not a sufficient guard for removing disaster recovery. Delete an older copy, or update first so that a newer rollback exists.
Deleting needs the superuser, exactly as restoring does, and every deletion is recorded in the server log with the user who asked for it. It does not require maintenance mode: removing a backup directory never touches the live tree, and an installation that has run out of disk must not have to close itself to the public before it may reclaim it.
If a deletion does not complete
The engine re-checks the directory after removing it and reports a failure naming what survived, rather than claiming a removal it cannot see. On a bind-mounted tree (a container whose code directory comes from the host) this is usually a mount the host still holds on a directory inside the copy: free it and retry.
Testing branch work on a real installation
A developer build carries no version bump — it is the same version as the release it was branched from. That is deliberate: a version number that moves without a release stops naming a release. So a developer build is installed over the same version, and it can be installed again as often as the branch moves.
The receiving install ticks Developer builds, checks for updates, and picks
the build (marked as a developer build, and listed first). Everything else is
the ordinary update: superuser, maintenance mode, a recent backup, the same
.sha256 verification, the same backup, smoke boot, swap and rollback.
Because the version cannot move, the panel does not use it to tell you the
update landed. Each installed tree records the archive it came from, and the
panel shows it as Installed archive — that value changing is the proof, and
it is also what the rollback machinery compares. An install running a developer
build says so everywhere: its version reads <version>.dev and its build
posture is Developer build (unreleased branch code).
Not for production installations
A developer build is unreleased branch code. Use it to test development work on an installation you can afford to break — never on a production one.
Serving a release
The server publishes each archive at
/dedalo/install/code/<version>/<version>.zip (and its digest at the same URL
plus .sha256), mapping the request back to DEDALO_CODE_FILES_DIR. Only that
release's own files are reachable, and only while IS_A_CODE_SERVER is set —
so the storage directory does not have to sit in the web root, and should not.
The reverse proxy has to route it, and the panel now checks
/dedalo/install/code/ has no counterpart in the client tree, so a vhost
that does not forward it to the engine does not fall back — the static
alias answers 404 and the request never reaches the server. Everything
else looks healthy: the archive is on disk, the version is right, the
manifest is correct, and every museum fails at download time with
bad server response code: 404.
The code-server panel probes its own advertised URL and reports which layer answered: the reverse proxy is not routing the path, the engine answered (so routing is fine and the fault is this server's own configuration), or the origin did not answer at all. The rule to add is in Reverse proxy and TLS.
The probe fetches the master's own public origin from inside its own network, so it proves this vhost routes the path — not that the wider internet can reach you. Split-horizon DNS or a firewall can still make the two differ.
A built release is not automatically offered
A release is advertised only when the engine's update catalogue knows the target version, and only to callers for which it is the next step on the upgrade path. Building an archive publishes the file; it does not make every install eligible for it.
Updating manually
For most installs, the simplest and most predictable update is manual:
git pull
bun install --frozen-lockfile
# restart the server (however your process supervisor does it), e.g.:
systemctl restart dedalo
Boot migrations (install/db/migrations/) run automatically at startup — there
is nothing extra to run for schema changes to dedalo_ts_* tables. Your
per-install configuration and secrets live in ../private/, a sibling of the
install tree, so a git pull never touches it.
The Bun runtime itself is pinned per install (.bun-version); a code update
does not upgrade it. Upgrading Bun is a deliberate, separately-tested change.
- Close access to the work system (maintenance mode, as above).
git pull(or check out the release tag you want) andbun install --frozen-lockfile.- Restart the server process.
- Check for new settings: compare
../private/.envagainst the shipped../private/sample.envand add any new key(s). See the Configuration Administrator Guide. - Follow any update instructions and update data if the release requires it.
- Open access to the work system again.
- Log out and re-login with a normal user.
Issues
Sometimes the update process asks you to log out and log in to set the correct files and refresh the browser cache. In some cases the update is left unfinished and some config variables are not assigned; the login may then show an alert as an error.

Don't panic and press continue button. Navigate to the area maintenance and check the messages in the Update data or Check config panels about the errors.