TurboPanel Docs
Development

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.

ChannelWhat it isWhere it resolvesMoves when
canaryThe newest green build of trunkreleases/download/canary/manifest.json — one rolling pre-release tagged canaryEvery merge to trunk that passes CI
rcThe current release candidatereleases/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
releaseThe promoted releasereleases/latest/download/manifest.json — GitHub's own latest pointerA 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:

Terminal
curl -fsSL turbopanel.sh | TURBOPANEL_UPDATE_CHANNEL=canary sh

The 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:

Text
  ╭──────────────────────────────────────────────────────────────╮
  │  ⚡ 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 complete

Open 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:

JSON
{ "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:

Terminal
curl -fsSL turbopanel.sh | \
  TURBOPANEL_LICENSE=<license from the wizard or the console> \
  TURBOPANEL_UPDATE_CHANNEL=canary \
  sh

How a canary moves

A build soaks in order. The hosted environments and the package channels are the same ladder:

EnvironmentChannelWhat lands
testing (testing.turbopanel.dev)canaryThe newest green build of trunk, on every merge that passes CI
staging (staging.turbopanel.dev)rcA 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
livereleaseThe 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:

Terminal
curl -fsSL turbopanel.sh | TURBOPANEL_UPDATE_CHANNEL=canary sh

It 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:

Terminal
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 \
  sh

The 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.

RepositoryTriggerBuildPublishes
turbopaneldEvery push to trunk (publish-daemon-trunk.yml, after verify)Native linux-amd64 and linux-arm64 daemon, JS fallback, orchestration treeThe rolling canary release and the trunk CDN drop the hosted platform follows
turbopanelA green Build on trunk (canary.yml → release.yml with channel=canary)Native linux-amd64 and linux-arm64 instance packagesThe rolling canary release
uiA green Verify on trunk (canary.yml → release.yml with channel=canary)The web exportThe rolling canary release

The publish step itself is one reusable workflow in TurboPanel/dev, gh-canary.yml, which:

  1. uploads the build's assets to the canary pre-release — every asset name carries the build's version, so a new build never overwrites an older build's file — then uploads manifest-<version>.json and finally the rolling manifest.json, last, so nothing a consumer reads is missing;
  2. re-downloads every asset from its live URL and checks its sha256 and size against the manifest — a publish that fails here leaves the previous manifest in place;
  3. prunes the builds older than the newest 20;
  4. moves the canary tag 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:

Text
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 release

They 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.

  • 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
Edit on GitHub

Last updated on

On this page