Refresh a stuck daemon
Sometimes a managed server looks offline in the UI even though the daemon is still running, or the Update button fails because the server is on an old build. Re-run the same run.sh installer used for first-time setup — the command below builds the license from state on disk, and the installer reconciles the FHS tree in place.
When to use this
Use a manual run.sh refresh when:
- The server badge shows Offline but the daemon service is running (
systemctl status turbopaneld) - The Update button in the UI errors or does nothing
- You changed update channel in
/etc/turbopanel/daemon.envand want to pull from the new channel immediately - You want to reconcile after a bad upgrade (daemon disabled, wrong version, etc.)
Co-located dev hosts
This page covers managed servers (FHS tree). If your control-plane host
was provisioned with the dev console,
update it from the console (Sync Dev Build / Upgrade System) instead —
that host runs the daemon from a source checkout, not /opt/turbopanel/bin.
Those two actions exist only on that console. A managed self-hosted host
upgrades each unit from Admin → Updates (see Upgrade and
rollback). This page is the path when that button
cannot reach the daemon.
UI vs cell panel
The server list reads a Postgres projection for online/offline. The Cell panel reads live WebSocket state. If those disagree, the daemon is usually fine — run an update to refresh both the installed binary and presence.
One-command update (recommended)
On the managed server, as root:
ENV_FILE=/etc/turbopanel/daemon.env
LICENSE_B64=$(python3 -c "
import base64
id = open('/var/lib/turbopanel/license.id').read().strip()
tok = open('/var/lib/turbopanel/license.token').read().strip()
print(base64.urlsafe_b64encode(f'{id}:{tok}'.encode()).decode().rstrip('='))
")
HOST=$(sed -n 's/^TURBOPANEL_INSTANCE_URL=//p' "$ENV_FILE")
CHANNEL=$(sed -n 's/^TURBOPANEL_UPDATE_CHANNEL=//p' "$ENV_FILE")
curl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE="$LICENSE_B64" \
TURBOPANEL_HOST="$HOST" \
TURBOPANEL_UPDATE_CHANNEL="$CHANNEL" \
shThe command reads the server's current control plane and channel out of daemon.env and passes them back. The installer does not read either from daemon.env on a remote server. Without TURBOPANEL_HOST it enrols against the control plane named in the release manifest, which is TurboPanel High Availability. Without TURBOPANEL_UPDATE_CHANNEL it uses release. A self-hosted server refreshed without them would be moved to the TurboPanel High Availability control plane and the release channel.
The installer:
- Uses your license from
TURBOPANEL_LICENSE(built from/var/lib/turbopanel/license.id+license.tokenabove) - Uses the update channel from
TURBOPANEL_UPDATE_CHANNEL/--channel, elserelease - Uses the control plane URL from
TURBOPANEL_HOST/--host, else the release manifest's TurboPanel High Availability control plane. For TLS it reuses the Platform CA already on disk at/etc/turbopanel/instance-ca.pem - Reinstalls the host-arch native binary + orchestration tree (and
turbopaneld.js+ vendored Deno only when the native binary cannot execute on that host) - Re-runs Ansible and restarts
turbopaneld.service
A manifest pin already in daemon.env (TURBOPANEL_MANIFEST_URL) is cleared by a refresh that does not name one. To keep it, add TURBOPANEL_MANIFEST_URL=<the same URL> to the command.
A panel-driven update does not follow a channel that moves mid-rollout. The control plane sends the manifest URL and targetCommit it resolved when the run started. A host pin already in daemon.env (TURBOPANEL_MANIFEST_URL) still wins over that URL, so a hold you set by hand is not dropped by Update TurboPanel.
Update guard
A daemon self-update arms /run/turbopanel/update-guard.json with the target commit and a deadline, and starts turbopaneld-update-guard.timer (about 10 minutes, one shot per attempt). turbopaneld.service also starts that unit from OnFailure= if the new build crash-loops. The guard runs as root. It restores:
/opt/turbopanel/bin/turbopaneld.prev/opt/turbopanel/bin/turbopaneld.js.prevon hosts that run the JS runtime/opt/turbopanel/share/orchestration.prev
It writes /var/lib/turbopanel/update-rollback.json and restarts the daemon. The restored build reports rolled-back. A new build that attaches successfully disarms the guard. A control-plane update is a different path: it keeps bin/turbopanel.prev, lib/libduckdb.so.prev, and share/ui.prev, and rolls those back when GET /api/health does not match the pinned manifest. See Upgrade and rollback.
Pre-flight errors
The daemon refuses the install before it swaps files. The code is on update-result (and on the upgrade step when the control plane is driving the run):
| Code | Meaning |
|---|---|
preflight_in_progress | An install is already running on this host. The rejected attempt does not replace the one in progress. |
preflight_disk | Free space is below the minimum: 512 MiB on the install root, 128 MiB on the state directory, and 256 MiB on the temp directory (1 GiB on the install root and 1 GiB on the backup directory for a control-plane install). A failed measurement is the same code. |
preflight_manifest | The manifest could not be fetched or verified, the signature or schema failed, or targetCommit does not match the signed commit. |
preflight_trust | Automatic-update TLS is not public trust and not the configured Platform CA. Re-run the installer with --instance-ca to repair trust. The daemon does not disable verification for an automatic update. |
A control-plane install adds preflight_backup when turbopanel-database is not running. After the swap, health_timeout, health_mismatch, and restart_failed mean the previous generation was restored; recovery_required means that restore did not bring the control plane back. The recovery command is on Upgrade and rollback.
Run as root
Run the commands on this page as root (a root shell, or su -): the
license files they read are readable by root only. Do not put sudo in front
of sh in the pipeline, because it would drop the TURBOPANEL_* values. The
installer escalates with sudo by itself when it is not already root.
Self-hosted control plane
Add TURBOPANEL_HOST for a self-hosted control plane. Use https://<instance-host>:8443. A Let's Encrypt hostname, and an uploaded certificate that chains to a public root, omit --insecure-tls / TURBOPANEL_INSECURE_TLS.
curl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE="$LICENSE_B64" \
TURBOPANEL_HOST=https://<instance-host>:8443 \
shChange update channel
Set TURBOPANEL_UPDATE_CHANNEL on the same command. Channel names (trunk,
canary, rc, release, …) are technical identifiers — not product branding.
Each channel has a built-in location: release follows the latest
non-pre-release on the daemon's
GitHub Releases, rc
follows the rolling rc pre-release there, canary follows the rolling
canary pre-release — the newest green build of trunk, replaced on every
merge (see Canary environment) — and trunk is
the per-merge build drop TurboPanel High Availability follows. edge is reserved with
no built-in location. The example below sets trunk explicitly (the default is release):
curl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE="$LICENSE_B64" \
TURBOPANEL_UPDATE_CHANNEL=trunk \
shIf the license file is missing
The refresh command reads:
| File | Purpose |
|---|---|
/var/lib/turbopanel/license.id | Organization license UUID |
/var/lib/turbopanel/license.token | Secret token (shown once at license creation) |
Check on the server:
sudo ls -la /var/lib/turbopanel/If those files are missing, create a new license in the app (Servers → Add server), copy the install command immediately, and run it on the server.
Verify after update
sudo systemctl status turbopaneld
curl -sS https://turbopanel.app/api/health # or https://<host>:8443/api/health (add -k for a Platform CA name)In the UI, refresh the servers page — the server should show Online with a current commit on the update row.
Related
- Daemon setup — First-time install (
turbopanel.sh) - Deployment troubleshooting — TLS, Postgres, connectivity
Last updated on