Canary environment
A canary is a self-hosted TurboPanel that follows trunk — the integration branch every
repository merges into — instead of a release. It runs the newest green build of the control
plane, the web app and the daemon, and moves forward on every merge. It is how the team, and
anyone who wants to see a change before it ships in a release, runs trunk on a real host.
This page is for contributors. Running TurboPanel for real is covered under Self-hosted overview and Installation.
Channels and environments
Every package a self-hosted install needs — the daemon (turbopaneld), the compiled control
plane (turbopanel) and the web export (ui) — publishes to that repository's
GitHub Releases on three channels. The installer and the
daemon follow a channel by name and resolve its manifest.json straight from GitHub; the
manifest names each asset with its sha256 and size, and every download is verified against it.
| Channel | What it is | Where it resolves | Moves when |
|---|---|---|---|
canary | The newest green build of trunk | releases/download/canary/manifest.json — one rolling pre-release tagged canary | Every merge to trunk that passes CI |
rc | The current release candidate | releases/download/rc/manifest.json — the pointer at the newest versioned candidate (0.1.3-rc.1, 0.1.3-rc.2, …) | A new rc.N is published |
release | The promoted release | releases/latest/download/manifest.json — GitHub's own latest pointer | A release candidate is promoted |
TurboPanel High Availability (the hosted, Cloudflare Workers build of the same code) has the
same three stages under different names — testing.turbopanel.dev tracks trunk,
staging.turbopanel.dev the release candidate, and the live platform the release — but those
deploys are operated by TurboPanel and are not something you stand up yourself.
Set up a canary host
The canary is an ordinary self-hosted install on the canary channel. Nothing else differs:
the same installer, the same wizard, the same daemon enrolment.
Prerequisites
-
A Debian 12 or 13 host (the bootstrap refuses other distributions), root access, and a public or LAN address you can reach on port
8443. Four vCPUs and 6 GB of RAM are enough for a single-host canary that deploys a few small projects. -
curl— a minimal Debian image does not ship it:Terminal apt-get update && apt-get install -y curl
Install
Run the public installer with the channel set. Piped from curl with no license, it installs a
control plane on this host. The installer names the canary build and prints a pre-release-channel
warning before it continues:
curl -fsSL turbopanel.sh | TURBOPANEL_UPDATE_CHANNEL=canary shThe installer resolves the canary manifest of all three repositories, downloads each package,
verifies it, converges the host (Postgres, Redis, RabbitMQ, Docker, certificates, systemd units,
Caddy) and prints the wizard address:
╭──────────────────────────────────────────────────────────────╮
│ ⚡ TurboPanel · Self-Hosted Instance Installer / Updater │
╰──────────────────────────────────────────────────────────────╯
v0.1.1-canary.20260919-143000-a1b2c3d
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
WARNING: PRE-RELEASE UPDATE CHANNEL (CANARY)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Canary follows every green trunk merge. It is not a supported
release: builds can break, change behaviour or need a fresh install
at any time. Do not run it in production.
Supported release: curl -fsSL turbopanel.sh | sh
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This installs the full TurboPanel control plane on this host.
…
✓ Release manifest resolved (channel canary, arch linux-amd64)
…
✓ Packages unpacked (instance v0.1.1-canary.20260919-143000-a1b2c3d, UI v0.1.1-canary.20260919-142100-e4f5a6b)
✓ TurboPanel instance provisioning completeOpen https://<host>:8443/install, complete the wizard, and GET /api/health reports the
canary's version and the exact trunk commit it was built from:
{ "ok": true, "version": "0.1.1", "revision": { "commit": "a1b2c3d…" } }Enrol servers on the same channel
Servers you add to a canary control plane follow canary too: the install command the console
issues for a new server carries the instance's own channel, so a daemon enrolled from it updates
from the same rail. Enrolling the control-plane host itself as a server is supported — a
single-host canary is a complete TurboPanel.
To enrol a host by hand on the canary channel:
curl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE=<license from the wizard or the console> \
TURBOPANEL_UPDATE_CHANNEL=canary \
shHow a canary moves
A build soaks in order. The hosted environments and the package channels are the same ladder:
| Environment | Channel | What lands |
|---|---|---|
testing (testing.turbopanel.dev) | canary | The newest green build of trunk, on every merge that passes CI |
staging (staging.turbopanel.dev) | rc | A versioned release candidate: rc.1, then rc.2, and so on (0.1.3-rc.1, 0.1.3-rc.2, …). The rc manifest pointer moves to the newest of those |
| live | release | The candidate that was promoted. The bytes that soaked on staging are the bytes live serves |
A self-hosted canary follows the first row. Admin → Updates installs that channel in the managed order — this host's daemon, then the control plane, then other servers — and pins the manifest it resolved when the run started. The installer still moves this host in one shot:
curl -fsSL turbopanel.sh | TURBOPANEL_UPDATE_CHANNEL=canary shIt fetches the current canary packages, re-converges the host, applies any new
migrations with the instance binary's own migrate verb, and restarts the
units. The same command with rc or release follows the later rows. Detail:
Upgrade and rollback.
Migrations only go forward
A trunk build may ship a database migration. Once a canary's database has been migrated by a newer build, an older build refuses to serve it. Treat a canary's data as disposable, or take a Postgres backup before each re-run if you care about rolling back.
Pin a canary to one build
The rolling release keeps the newest 20 builds. Each keeps its own manifest as
manifest-<version>.json, so a host can be pinned to one exact build instead of whatever the
channel serves now — for a daemon, with the pin the installer already understands:
curl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE=<license> \
TURBOPANEL_MANIFEST_URL=https://github.com/TurboPanel/turbopaneld/releases/download/canary/manifest-0.1.3-canary.417.json \
shThe pin is written to daemon.env as the daemon pin (TURBOPANEL_MANIFEST_URL), so a later console-driven daemon update honours it. The control plane has its own pin (TURBOPANEL_INSTANCE_MANIFEST_URL). Setting one leaves the other following the channel. A plain channel install clears the daemon pin. Builds older than the newest 20 are pruned, and a pinned host whose build was pruned falls back to the channel on its next install.
How the canary builds are produced
Each repository publishes its own canary from the same workflow that builds its releases, so a canary package is the same shape as a release package — only its version and its home differ.
| Repository | Trigger | Build | Publishes |
|---|---|---|---|
turbopaneld | Every push to trunk (publish-daemon-trunk.yml, after verify) | Native linux-amd64 and linux-arm64 daemon, JS fallback, orchestration tree | The rolling canary release and the trunk CDN drop the hosted platform follows |
turbopanel | A green Build on trunk (canary.yml → release.yml with channel=canary) | Native linux-amd64 and linux-arm64 instance packages | The rolling canary release |
ui | A green Verify on trunk (canary.yml → release.yml with channel=canary) | The web export | The rolling canary release |
The publish step itself is one reusable workflow in
TurboPanel/dev,
gh-canary.yml, which:
- uploads the build's assets to the
canarypre-release — every asset name carries the build's version, so a new build never overwrites an older build's file — then uploadsmanifest-<version>.jsonand finally the rollingmanifest.json, last, so nothing a consumer reads is missing; - re-downloads every asset from its live URL and checks its
sha256and size against the manifest — a publish that fails here leaves the previous manifest in place; - prunes the builds older than the newest 20;
- moves the
canarytag to the built commit and refreshes the release notes.
Versions on the canary channel
A build's version is the repository's declared number plus a build counter, so the name says exactly which build it is:
0.1.3-canary.417 canary build 417 of the 0.1.3 line (the number is that repository's canary run counter)
0.1.3-rc.2 release candidate 2 of 0.1.3
0.1.3 the releaseThey sort in that order, so a canary always sorts below the release candidate it leads to,
and a release candidate below the release (0.1.3-canary.417 < 0.1.3-rc.1 < 0.1.3). A release
candidate is the canary's exact bytes with a new name, version and signature; the number after
rc. stays the same until the version ships, and a bad candidate is replaced by the next one
(rc.2), never by burning the version. How a candidate and a release are published is on
How to ship. The number in package.json / deno.json is
bumped to the next patch right after each release, by a "Start" pull request.
The version a canary reports on the wire — /api/health version, the daemon hello — is the bare
number (0.1.3); the counter lives in the manifest, the asset names and the release notes, and
the control plane's Updates screen shows it (Canary #417, RC 2). Daemons decide whether to
update by the manifest's commit, not its version string. The console reload check compares
revision.commit (the x-turbopanel-revision header) the same way, so two canary builds that
both advertise 0.1.3 still ask for a reload. Semantic version stays the compatibility floor.
Related
- How to ship — the two merges that publish a release candidate and a release
- Daemon update — channels and refreshes from the daemon's side
- Upgrade and rollback — the release-channel procedure this page mirrors
- Compatibility — which control plane and daemon builds work together
Last updated on