Control Plane Deployment
The TurboPanel instance is the control plane API. It runs as Deno on self-hosted hosts (Unix socket + Caddy) or on Cloudflare Workers for TurboPanel High Availability. The Expo UI is served by Caddy (static export or dev proxy) or Workers assets.
Private alpha — not yet publicly available
Self-hosted and TurboPanel High Availability control planes are both in private alpha and not yet publicly available. Contributor development (Vagrant + sibling repos) is available for engineers building TurboPanel — not as a production operator install path.
Overview
Purpose
- Centralized API for organizations, servers, auth, and daemon orchestration
- Web UI (Expo web) for operators
- WebSocket hub for remote daemons (
/ws/daemon/v1) - OpenAPI + Scalar at
/api/openapi.jsonand/api/reference
Relationship to daemon
On the control plane host, a co-located daemon installs and updates the instance, Caddy, UI, and Postgres via Ansible. Additional servers run a standalone daemon that connects back over WSS. See Daemon setup.
Co-located daemon
The control plane server includes a co-located daemon that dials the local instance (socket or Caddy).
Remote nodes use the production installer at turbopanel.sh — not a separate control-plane container image.
The control-plane host is not a tenant deploy target
The co-located server (this server in the console) only runs the platform itself. Tenant projects
cannot be pinned to it, and an unpinned deploy never schedules onto it — a single-host install
answers a deploy with "The only connected server is the co-located control-plane host …" until
you enrol a second server.
Architecture (self-hosted)
Deployment models
| Feature | Self-hosted (Deno) | TurboPanel High Availability |
|---|---|---|
| Runtime | Deno on Unix socket | Cloudflare Workers |
| TLS | Caddy (platform CA by default; Let's Encrypt or uploaded cert optional) | Cloudflare-managed TLS |
| Database | Local Postgres (socket) | Postgres via Hyperdrive |
| UI | Caddy → Expo dev or static export (/opt/turbopanel/share/ui; dev override ../ui/dist) | Workers assets / separate hosting |
| Daemon on CP host | Co-located (socket mode) | Connects to Workers URL |
| Availability | Private alpha — preview | Private alpha — waitlist |
| Pricing (planned) | Free, unlimited servers | See pricing when available |
Control-plane TLS modes
Self-hosted Caddy TLS is selected by turbopanel_tls_mode (Ansible extra-var; default self_signed). The Platform CA is still minted in every mode.
| Mode | Caddyfile | Listen | Leaf material | GET /api/daemon/v1/instance/ca | --insecure-tls |
|---|---|---|---|---|---|
self_signed (default) | Caddyfile | :8443 | certs/self-signed.* | 200 | yes |
upload | Caddyfile | :8443 (or caddy_port) | operator pair copied to certs/ | 200 unless turbopanel_tls_public | follows turbopanel_tls_public |
lets_encrypt | Caddyfile.acme | :443 (+ :80 challenge) | Caddy ACME storage | 404 | no |
| Port | When |
|---|---|
| 8443 | Default HTTPS (self_signed / upload) |
| 443 + 80 | lets_encrypt (HTTPS + HTTP-01). Must be free on the control-plane host — it conflicts with a hosting Caddy on the same host. |
| 8880 | Co-located dev plaintext mirror only |
TURBOPANEL_TLS_PUBLIC=1 (set automatically in lets_encrypt, or when turbopanel_tls_public is true) makes the Deno CA route 404 so daemons use the system trust store, and install commands omit --insecure-tls even on a non-443 port.
Installation paths
Pick the path that matches your goal — they do not substitute for one another. See Installation for the full decision guide.
Contributor development (available today)
For engineers working on TurboPanel source — not production self-hosted or TurboPanel High Availability.
Clone (or fork) the six sibling repos, install Vagrant + a provider, then from the dev checkout:
vagrant up
vagrant ssh
dev/consoleSee Local development.
On a fresh guest the console bootstraps the daemon and converges the stack (optional-services picker). Later launches sit idle until Developer → Converge / re-converge.
On the host, open https://localhost:8443 (ports are forwarded). Prefer a LAN hostname for remote test machines.
Services on a contributor dev host:
| Unit | User | Role |
|---|---|---|
turbopaneld.service | current dev user | Ansible orchestration (daemon from ~/turbopaneld in dev) |
turbopanel-instance.service | current dev user | Deno API on Unix socket |
turbopanel-caddy.service | current dev user | TLS + reverse proxy :8443 by default; dev-only plaintext mirror :8880 when enabled |
turbopanel-ui.service | current dev user | Expo web dev (:8081, dev only) |
See Local development. Logs: journalctl -u turbopanel-instance -u turbopanel-caddy -u turbopanel-ui -u turbopaneld -f
Self-hosted control plane (private alpha — preview)
The same turbopanel.sh installer that enrols a daemon installs a control plane, on an explicit opt-in. It provisions a managed FHS layout on Debian — the bootstrap refuses any other distribution up front — with dedicated service users (tp, tpctrl, tpcaddy), the compiled instance and mailer under /opt/turbopanel/lib/instance, the static UI at /opt/turbopanel/share/ui, Postgres / Redis / RabbitMQ / Docker, the platform CA and a self-signed leaf, systemd units and Caddy — from the release packages on GitHub Releases, every asset verified against the release's manifest.json. Packages exist once the first v0.1.0 release is cut; until then the command refuses with "does TurboPanel/turbopanel have a release yet?".
Provision a 64-bit Linux host (Debian 12+ recommended, x86_64 or aarch64), as root or a sudo-capable user.
Install the control plane:
curl -fsSL turbopanel.sh | TURBOPANEL_INSTANCE=1 shAdd TURBOPANEL_UPDATE_CHANNEL=rc to install the current release candidate instead of the latest release. The installer takes no license (the wizard issues the first one) and enrols no daemon.
Complete the install wizard at https://<host>:8443 by default (host PAM + superadmin). The certificate is signed by the platform CA at /var/lib/turbopanel/tls/ca.crt. Use https://<hostname>/ when turbopanel_tls_mode=lets_encrypt.
Enroll managed servers — including this host, once the wizard has issued a license — with turbopanel.sh and TURBOPANEL_LICENSE — Daemon setup.
Overview and operator responsibilities: Self-hosted.
Not the contributor Vagrant workflow
Contributor development uses Vagrant and sibling checkouts — see Local development. It is not the production self-hosted install path.
TurboPanel High Availability (private alpha — waitlist)
TurboPanel High Availability runs the instance on Cloudflare Workers. Remote daemons connect over HTTPS/WSS to a hosted URL. It is not yet publicly available — join the waitlist for access updates.
When access is available, operators create an organization in the hosted panel, configure Workers bindings and secrets, and enroll nodes with turbopanel.sh (manifest default host — no TURBOPANEL_HOST required).
Maintainers deploy the Workers bundle from the instance repo:
export TURBOPANEL_DATABASE_URL="postgresql://user:pass@host:5432/dbname"
# or, for CI / dashboard deploy workflows:
# export DATABASE_URL="postgresql://user:pass@host:5432/dbname"
export CLOUDFLARE_ENV=live # or testing — must match a wrangler.jsonc env name
pnpm install
pnpm deploypnpm deploy runs pending migrations (via TURBOPANEL_DATABASE_URL or DATABASE_URL) then wrangler deploy --env $CLOUDFLARE_ENV --minify. The migration step uses Node only (drizzle-kit migrate + post-migration resource-registry repair) — Deno is not required on the deploy host. Configure Hyperdrive, secrets (TURBOPANEL_SECRETS), and bindings in wrangler.jsonc before deploy.
Daemon Cell on Workers
TurboPanel High Availability coordination uses a hibernation-safe Durable Object per server
(getByName(serverId)) for live WS presence, outbox delivery, and request
correlation — same /api/daemon/v1/* and /ws/daemon/v1 paths as self-hosted.
Self-hosted uses the Redis cell backend instead. UI status reads come from Postgres
only; the cell is not a polling API. See Daemon cell architecture
and instance/AGENTS.md (Daemon Cell) for cost/hibernation rules agents must not regress.
Configuration
The complete list of what a self-hosted instance reads at boot — required, defaulted, and silently degrading — is Instance configuration. The most common ones:
Key environment variables (instance)
| Variable | Purpose |
|---|---|
TURBOPANEL_SECRETS | Session signing (required in production) |
TURBOPANEL_DATABASE_URL | Full Postgres connection URL for self-hosted Deno boot (instance-launch) and tooling |
DATABASE_URL | Tooling-only fallback for pnpm migrate / drizzle-kit when TURBOPANEL_DATABASE_URL is unset (common in CI and dashboard deploy) |
CLOUDFLARE_ENV | Wrangler env name for Workers deploy (e.g. live, testing) — required by pnpm deploy |
TURBOPANEL_SOCKET / TURBOPANEL_SOCKET_DIR | Unix socket path overrides |
TURBOPANEL_UI_MODE | dev (Expo proxy) or static (exported UI) |
TURBOPANEL_UI_ROOT | Static UI export root. Production (FHS) default /opt/turbopanel/share/ui (where the daemon ui-build role publishes the export); co-located dev overrides it to a checkout-relative path such as ../ui/dist |
CADDY_PORT | HTTPS listen port (default 8443; lets_encrypt binds 443) |
TURBOPANEL_TLS_PUBLIC | When 1 / true, Deno GET /api/daemon/v1/instance/ca 404s and install commands omit --insecure-tls |
CADDY_HTTP_PORT | Dev-only plaintext HTTP listen port (default 8880) |
TURBOPANEL_DEV_HTTP_CONTROL_PLANE | Must be "1" to serve the dev plaintext entrypoint on CADDY_HTTP_PORT; injected automatically on co-located dev hosts only |
TURBOPANEL_IS_SIGNUP_ENABLED | Forces sign-up open (1) or closed (0), overriding the admin setting — both runtimes |
TURBOPANEL_REDIS_SOCKET / TURBOPANEL_AMQP_URL | Redis socket (required, opened lazily) and the RabbitMQ URL for the email queue (unset → probe, then a silent no-op queue) |
TURBOPANEL_UPDATE_CHANNEL | The channel this instance resolves daemon updates on (trunk default; rc, release) |
TURBOPANEL_AUTH_PROVIDERS__* | GitHub / Google OAuth client id + secret (GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET). Env wins over the SYSTEM_AUTH_PROVIDERS setting row. On TurboPanel High Availability these are Wrangler secrets. |
Managed hosts inject vars via Ansible instance-launch. Workers dev uses .dev.vars in the instance checkout. Provider setup (Admin → Auth providers, env-wins, and linking rules): Accounts and access.
API entrypoints
| Path | Description |
|---|---|
/api/health | Unversioned health probe |
/api/client/v1/* | End-user REST API + auth |
/api/install/v1/* | Self-hosted install wizard (Deno only) |
/api/developer/v1/* | Developer console (dev tooling) |
/api/daemon/v1/* | Daemon REST (version, instance/ca) |
/ws/daemon/v1 | Daemon WebSocket |
/api/openapi.json | OpenAPI 3.1 spec |
/ | Web UI (via Caddy) |
HTTPS entrypoint (self-hosted): https://<host>:8443 by default. lets_encrypt serves https://<hostname>/ on :443.
Dev-only plaintext HTTP entrypoint
Co-located development also exposes a plaintext mirror at http://<host>:8880
(CADDY_HTTP_PORT, default 8880). It is gated by
TURBOPANEL_DEV_HTTP_CONTROL_PLANE=1 (set automatically by Ansible when
turbopanel_dev_user is configured). Requests are rejected with 403 when
the flag is off. This entrypoint is not enabled on managed or production
installs — it exists to bypass self-signed TLS friction when troubleshooting
daemon WebSocket connections or attaching a daemon without CA setup. Both
TURBOPANEL_INSTANCE_RUNTIME=deno and =workers use the same Caddy
proxy, so :8880 mirrors every route on :8443 for either runtime.
Communication patterns
- Browser → Caddy → instance: REST and cookies on
/api/client/v1/* - Daemon → instance: WSS
/ws/daemon/v1(through Caddy or direct Unix socket when co-located) - Dev sync / tunnel: Operator pushes daemon builds or tunnel tokens via developer API + daemon WS messages
Compose deploys (compiled runtime file)
The control plane stores a project base compose and an optional environment overlay. Merge and platform inject happen on the instance; environment.deploy then publishes a single compiled compose.yaml (role: 'runtime') that the daemon writes under <stateDir>/deployments/<projectId>/<environmentId>/ next to .env (non-secrets) and deployment.json.
Overrides between the authored project and environment layers follow the Compose Spec merge rules: mapping keys merge recursively (a later layer wins per key), most sequences — ports, volumes, env_file, and similar lists — append with attribute-specific key-based de-duplication rather than replacing the earlier layer wholesale, and command, entrypoint, and a service's healthcheck.test always fully replace rather than append. Authors who need to remove a value entirely, or force full replacement instead of the default append/merge behavior for a sequence, can use the reserved !reset (delete the key) and !override (force full replacement) YAML tags on the environment overlay.
In the organization console, Merged compose is a client-side simulation of the fully merged effective document (readability aid). Prepared compose and the live environment.deploy path show that compiled compose.yaml. See also API architecture — compiled compose.
Related documentation
- Installation paths — Decision guide for all install paths
- Accounts and access — Two-factor, passkeys, GitHub / Google sign-in, and invitations
- Self-hosted overview — Operator responsibilities
- Daemon setup — Remote node installer
- Security — TLS, auth, and hardening
- Deployment troubleshooting — Runtime issues
- Instance API architecture — Code layout
Last updated on