Upgrade to 0.4.0
Move an appliance or a Docker Compose installation from 0.3.x to 0.4.0, know what the first boot does to the schema, and understand what a rollback does and does not return.
- Version: 0.4
- Role: admin_user
- Type: task
Every 0.3.x installation moves to 0.4.0 in one step — the release declares no minimum version to upgrade from. What differs is who applies it: an appliance is updated from the dashboard, and a Docker Compose installation is updated from a shell on its own host.
What do I need?
- Licence
- Any
- Role
admin_user
Prerequisites
- You know which of the two installations you have: an appliance carries Settings → System Updates and a privileged updater on its host; a Compose installation is a git checkout the operator maintains.
- A current backup that is not on the appliance's own disk. See Backup and restore.
- A maintenance window. Migrations run before the new containers start, so the still-serving old version runs into their locks and writes can block.
- For an appliance on the online path: it can reach the release channel over HTTPS.
- For an appliance that is air-gapped: the signed .cidupd bundle for 0.4.0, on a machine whose browser can reach the appliance.
- For a Compose installation: shell access with permission to run Docker, and a clean git working tree — update.sh refuses to run on a dirty one.
What the first 0.4 boot does to your database
DB_SYNCHRONIZE now defaults to false, and the container entrypoint applies the TypeORM
migration chain on boot whenever it is not true. On an appliance this is unremarkable — the
release chain has always owned the schema. On a Compose box that never set the variable, it means
the first 0.4 boot stops letting TypeORM reshape the schema on every restart and instead stamps
the baseline and runs the whole chain, for most such boxes the first migration run they have ever
done.
A failure there is fatal on purpose: the container refuses to boot rather than serve against a schema it could not migrate.
Warning
Auto-sync drops every index and trigger the migrations create, because no entity declares them.
Keeping the old behaviour with DB_SYNCHRONIZE=true is possible and is not supported going
forward.
Upgrade
Take a backup that is not on the appliance
Snapshot the virtual machine with it powered off, or take the file-level copy described in Backup and restore.
The updater takes its own database backups immediately before it applies a release — a
compressed logical dump and a physical snapshot of the data directory, both under
/var/lib/cid/backups, with the last 14 dumps kept. Those live on the appliance's own
disk and cover the database alone, so they are an update safety net rather than the backup
this step asks for.
Apply the release
Follow Update the appliance, on the online path or from the signed bundle.
The page streams the updater's progress and shows a duration estimate derived from the size of your database and the number of migrations actually pending.
Let the host repair finish
Stay on Settings → System Updates until the Host repair tile reports its migrations as applied.
0.4.0 introduces host-side repair: a signed release can now replace the updater, install systemd units and correct the deployment mode on a box installed from an older image. A box coming from 0.3.x has repairs to run, and some of them wait for the updater to go idle before they apply.
What a rollback returns, and what it does not
Rollback behaves differently on the two installations, and neither of them undoes a schema migration.
| Appliance | Docker Compose | |
|---|---|---|
| Triggered by | You, from Settings → System Updates, or automatically when the new release fails its health gate | update.sh automatically, when the health wait times out |
| Restores the database | Yes, from the snapshot taken before the release was applied | Yes, from the pg_dump taken at the start of the run |
| Restores the previous images | Yes | No — the new containers are left running so their logs survive |
| Data written since the update | Lost | Lost |
Danger
A rollback restores the database as it was before the update. Sessions, detections, event log entries and configuration changes recorded since are gone. Roll back to recover from a broken update, never to undo a configuration mistake.
On a Compose installation the automatic path is a database rollback only, so a failed update
leaves 0.4.0 images running against a 0.3.x database. Treat exit code 5 from update.sh as an
incident to finish by hand: read the logs it printed, and either fix forward or restore the whole
box from the backup you took in step 1.
Verify
- Settings → System Updates shows 0.4.0 under Installed version, and Last update
reports the run as successful. On a Compose box,
curl localhost:3000/healthanswers200. - No migrations are pending. On a Compose box,
make migration-statuslists none; on an appliance, the Host repair tile lists none as pending or failed. docker compose psshows every container healthy, and ahallucination-guardcontainer is among them.- Settings → License still shows your tier and expiry. An upgrade never changes the licensed package.
- Send one chat request that contains a piece of test PII. It is masked or rejected according to your filters, and the detection appears under All Detections.
If it fails
The gateway will not boot and the log names a migration:
Migrations are pending, and the pages that read the new columns fail
You might see: compiled migration(s) have never been applied to this database · one page returns 500 while the rest of the product works · column does not exist · relation does not exist
Before you start
Checks
- 1
Read the list of compiled migrations that are not recorded in the database
diagnostics · datastores.postgres.migrationsPending
- 2
Verify auto-synchronise is off, since it hides an unapplied chain behind a schema that looks right
diagnostics · posture.dbSynchronize = false
- 3
Verify the gateway is not restarting, which is what a failing boot migration usually produces
diagnostics · containers[nestjs-core].restarts = 3
Causes and fixes
| Reached when | Cause | Explanation, fix and verification |
|---|---|---|
| check 1 fails | boot-migration-failed (critical) | The container entrypoint applies the compiled migration chain on every boot when auto-synchronise is off. A non-empty pending list on a booted appliance therefore means that run failed rather than that it never happened. The product stays up: only the endpoints that select the new columns answer 500, which is why one page breaks and the rest looks healthy. Fix: Read the gateway log from the boot that failed and fix the reason the migration could not apply — a lock held by another connection, an object the migration expected to be absent, or a permission on the database role. Then restart the gateway so the chain runs again. Do not hand-edit the migration history table to mark them applied; the columns will still be missing. Verify: diagnostics · datastores.postgres.migrationsPending If that does not apply: Attach the support bundle and the gateway log covering the failed boot. |
| check 2 fails | synchronize-masking-the-chain (critical) | Auto-synchronise shapes the schema from the entity declarations, so the tables look correct while the migration history stays empty. Every boot then drops whatever the entities do not declare, and the pending list never clears. Fix: Turn auto-synchronise off and let the chain apply. See The database schema is being auto-synchronised. Verify: diagnostics · posture.dbSynchronize = false |
| check 3 fails | migration-crashloop (critical) | A migration that throws during boot takes the gateway with it, so the container restarts, tries the same migration, and fails again. The restart count climbs while nothing else changes. Fix: Stop the container, read the log from one failed start end to end, and resolve the specific error before starting again. Restarting into the same failure produces no new information. Verify: diagnostics · containers[nestjs-core].restarts = 3 If that does not apply: Attach the support bundle and the name of the migration that fails. |
The schema lost its indexes, or the box was running with auto-sync:
The database schema is being auto-synchronised on every boot
You might see: DB_SYNCHRONIZE=true · indexes disappear after a restart · search is slow again after every reboot · the schema changes without a migration
Before you start
Checks
- 1
Read the effective auto-synchronise setting
diagnostics · posture.dbSynchronize = false
- 2
Verify the migration chain is fully applied, since auto-sync hides an unapplied chain
diagnostics · datastores.postgres.migrationsPending
Causes and fixes
| Reached when | Cause | Explanation, fix and verification |
|---|---|---|
| check 1 fails | synchronize-enabled (critical) | Auto-synchronise makes the schema match the entity declarations on every boot, which means it drops every object the entities do not declare — including the indexes the migrations created. The product keeps working and gets slower after each restart, and the loss is silent because nothing errors. Migrations own the schema on every deployment; only the developer overlay opts into synchronise. Fix: Set DB_SYNCHRONIZE to false in the appliance's environment file and restart the gateway container. The entrypoint then applies the compiled migration chain on boot, which restores the objects auto-sync removed. Confirm afterwards that no migrations are pending.Verify: diagnostics · posture.dbSynchronize = false |
| check 2 fails | chain-never-applied (critical) | With auto-synchronise on, the migration chain may never have run, so the recorded migration history does not match the schema in front of it. Turning synchronise off without applying the chain leaves a database that neither mechanism owns. Fix: Apply the pending migrations, then restart. On a database that predates the migration chain, baseline it first so the initial migration is recorded rather than re-run. See Migrations are pending. Verify: diagnostics · datastores.postgres.migrationsPending If that does not apply: Attach the support bundle and the output of the migration status command. |
The appliance update page reports a failure, or a version of 0.0.0:
The System Updates page shows 0.0.0, or the update buttons return 500
You might see: System Updates shows version 0.0.0 · current version 0.0.0 · update check returns 500 · install update → 500
Before you start
Checks
- 1
Read the product version the appliance reports
diagnostics · appliance.version = "0.0.0"
- 2
Verify an update channel is configured
diagnostics · appliance.channel
- 3
Verify the release channel answered on the last attempt
diagnostics · appliance.update.channelReachable = true
- 4
Check whether an update or host-repair intent is stuck waiting for the host updater
diagnostics · appliance.update.pendingIntent
- 5
Read the host-repair status, which is what the page renders alongside the version
GET /admin/system-update/host-repair
Causes and fixes
| Reached when | Cause | Explanation, fix and verification |
|---|---|---|
| check 1 fails | version-file-missing (warning) | The appliance reads its installed version from a file the image build writes. When that file is absent or unreadable the version falls back to 0.0.0, and every comparison against the channel manifest then looks like a downgrade — which is why the page can show 0.0.0 and simultaneously refuse to offer an update. Fix: Run the host repair from Settings → System Updates, which rewrites the appliance version file, then reload the page. If host repair is unavailable, the version can be restored by re-running the installer's finalisation step; see Update the appliance. Verify: diagnostics · appliance.version = "0.0.0" If that does not apply: Attach the support bundle. A 0.0.0 that survives host repair means the image was built without its version stamp. |
| check 2 fails | manifest-url-unset (warning) | No release channel is configured, so the update check has no manifest to fetch. The API rejects the call with a 400 naming the missing manifest URL, which the page surfaces as a failed button rather than as a configuration prompt. Fix: Set the update channel under Settings → System Updates. On an air-gapped appliance, choose the offline channel and upload the update bundle instead of pointing at a URL. Verify: diagnostics · appliance.channel |
| check 3 fails | channel-unreachable (warning) | The channel is configured but the manifest fetch failed — no DNS, no egress, or an outbound proxy that is required and not configured. The fetch failure surfaces as a 500 from the update endpoint. Fix: Fix outbound access to the channel host, or switch the appliance to offline updates. Read the DNS and egress probes in the diagnostics snapshot separately: resolution succeeding does not mean the HTTPS fetch succeeds, and a proxy-only network needs the proxy configured explicitly. Verify: diagnostics · appliance.update.channelReachable = true |
| check 4 fails | updater-daemon-not-running (critical) | An intent is queued but nothing is executing it. The gateway only records the intent; the privileged updater daemon on the host performs the work. When that daemon is not running, intents accumulate and every subsequent action appears to hang or fail. Fix: Start the updater daemon on the appliance host and confirm the queued intent moves out of the queued state. Do not queue further intents until it does; a backlog makes the failure harder to read. Verify: diagnostics · appliance.update.pendingIntent If that does not apply: Attach the support bundle plus the updater daemon's own log from the host. |
| check 5 fails | host-agent-absent (warning) | The host-repair endpoint cannot answer because the host agent has never written its inventory. The page then has no host-side data to render beside the version, and the combination reads as a broken page. Fix: Run the host agent's inventory job on the appliance and reload the page. This is expected on a non-appliance deployment, where there is no host agent at all and the update page is not the supported upgrade path. Verify: GET /admin/system-update/host-repair |
The appliance cannot reach the release channel:
The update channel is not reachable from the appliance
You might see: The update channel host is not reachable from inside the gateway · Check for updates does nothing · update check returns 500 · the appliance never finds a new version
Before you start
Checks
- 1
Verify an update channel is configured at all
diagnostics · appliance.channel
- 2
Read whether the channel answered on the last attempt
diagnostics · appliance.update.channelReachable = true
- 3
Read the name-resolution results for the hosts the appliance must reach
diagnostics · appliance.dns
- 4
Read whether an outbound proxy is configured, on a network that requires one
diagnostics · appliance.proxy.configured = true
Causes and fixes
| Reached when | Cause | Explanation, fix and verification |
|---|---|---|
| check 1 fails | channel-unset (warning) | No release channel is configured, so the update check has no manifest to fetch. The API rejects the call, and the page shows a button that fails rather than a prompt to configure anything. Fix: Set the update channel under Settings → System Updates. On an air-gapped appliance, choose the offline channel and upload a signed bundle instead of pointing at a URL. Verify: diagnostics · appliance.channel |
| check 2 fails | channel-blocked (warning) | The channel is configured and the fetch failed. On an appliance that is deliberately air-gapped this is the expected state and needs no action; on a connected one it means the HTTPS path to the channel host is blocked, intercepted, or requires a proxy the appliance does not have. Fix: Read the egress results in the diagnostics snapshot and fix the test that failed rather than the one you assume failed. See The appliance cannot get out. If the appliance is meant to stay offline, switch it to the offline channel so the failure stops being reported as a fault. Verify: diagnostics · appliance.update.channelReachable = true |
| check 3 fails | dns-cannot-resolve-channel (warning) | The appliance cannot resolve the channel host, so the fetch never reaches the network. DNS succeeding elsewhere proves nothing: the appliance uses this network's resolver, not the browser's. Fix: Set a resolver that answers for the channel host and re-run the check. See Network prerequisites. Verify: diagnostics · appliance.dns |
| check 4 fails | proxy-missing (warning) | This site sends everything through a proxy and the appliance has none configured, so the fetch is attempted directly and times out. A timeout looks like a dead channel rather than a missing setting. Fix: Configure the outbound proxy on the appliance and restart the gateway container so it re-reads the setting. Verify: diagnostics · appliance.proxy.configured = true |
Next steps
- What changed in 0.4.0 — the full list, including the behaviour changes that need no action.
- Backup and restore — the backup this page assumes you took.
- Hardening checklist — re-check the operator settings after a major upgrade.
Last updated on