TurboPanel Docs
Deployment

Control Plane Deployment

The TurboPanel control plane is the 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 per surface: /api/client/v1/openapi.json + /api/client/v1/reference (client API), /api/admin/v1/openapi.json + /api/admin/v1/reference (superadmin)

Relationship to daemon

On the control plane host, a co-located daemon installs and updates the control plane, 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 control plane (socket or Caddy). Remote servers use the production installer at turbopanel.sh — not a separate control-plane container image.

One server is enough

The server that runs the control plane (this server in the app) is an ordinary deploy target. A single-server install runs the control plane and your projects on the same box; what you give up is multi-node placement — a second database node, spreading projects across hosts — not the ability to deploy. Enrol more servers whenever you want that.

Architecture (self-hosted)

Self-hosted control plane traffic flow — diagram will load when scrolled into view

Deployment models

FeatureSelf-hosted (Deno)TurboPanel High Availability
RuntimeDeno on Unix socketCloudflare Workers
TLSCaddy. :8443 is always the Platform CA recovery address. Each published hostname picks platform-ca, an uploaded pair, or Let's Encrypt.Cloudflare-managed TLS
DatabaseLocal Postgres (socket)Postgres via Hyperdrive
UICaddy → Expo dev or static export (/opt/turbopanel/share/ui; dev override ../ui/dist)Workers assets / separate hosting
Daemon on CP hostCo-located (socket mode)Connects to Workers URL
AvailabilityPrivate alpha — previewPrivate alpha — waitlist
Pricing (planned)Free, unlimited serversSee pricing when available

Control-plane listeners

Self-hosted Caddy listens on :8443. Every hostname is served there, and the certificate is chosen by the name. The Platform CA is still minted, and it stays the catch-all leaf.

The Caddyfile is rendered by the daemon's instance-launch role into /etc/turbopanel/caddy/Caddyfile on every converge. Change names in Admin → Access → Hostnames, then apply. The rendered file is not hand-edited.

SourceLeaf on :8443GET /api/daemon/v1/instance/ca--insecure-tls
Platform CA (default)Platform CA leaf, also the catch-all200 for that name, and for an unlisted name on this listeneryes, until the Platform CA is trusted
Uploadedthe attached pair404 for that nameomitted when the certificate chains to a public root
Let's Encryptleaf copied after HTTP-01404 for that nameno
PortWhen
8443Always. Control plane HTTPS.
80Only during Let's Encrypt issuance or renewal.
443Hosting only.

Port 80 is opened on hosting Caddy for that window, then closed. When another process holds it, the apply fails with port 80 is held by <process>. The flow is in Hostnames and TLS.

TURBOPANEL_TLS_PUBLIC=1 is set when any hostname is Let's Encrypt, or when the operator marks TLS public. A name that presents a Let's Encrypt or uploaded leaf gets a 404 from GET /api/daemon/v1/instance/ca. The daemon then uses the system trust store, or the uploaded issuer for a private pair, and the install command omits --insecure-tls for a publicly trusted leaf. An unlisted name on :8443 still receives the Platform CA bundle.

turbopanel_tls_mode is a label derived from the hostname list (self_signed, upload, or lets_encrypt). It does not choose a different 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:

Terminal
vagrant up
vagrant ssh
dev/console

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

UnitUserRole
turbopaneld.servicecurrent dev userAnsible orchestration (daemon from ~/turbopaneld in dev)
turbopanel-instance.servicecurrent dev userDeno API on Unix socket
turbopanel-caddy.servicecurrent dev userTLS + reverse proxy on :8443
turbopanel-ui.servicecurrent dev userExpo 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), one compiled instance binary, /opt/turbopanel/bin/turbopanel, beside the daemon (its DuckDB library in /opt/turbopanel/lib; the email consumer runs inside it, there is no separate mailer process), 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. When the chosen channel has no release yet, the command stops with "does TurboPanel/turbopanel have a <channel> 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:

Terminal
curl -fsSL turbopanel.sh | sh

Run with nothing else, the installer sets up a control plane on this host. It prints a short welcome (and, on canary or rc, a non-stable-channel warning) then proceeds; on a terminal, Enter continues and q quits. TURBOPANEL_INSTANCE=1 says the same thing explicitly for scripts. Add 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 — to connect this host to an existing control plane, pass TURBOPANEL_LICENSE from that control plane's server install command.

Complete the install wizard at https://<host>:8443 (host PAM + superadmin). A Platform CA name is signed by the certificate at /var/lib/turbopanel/tls/ca.crt. A Let's Encrypt or uploaded name presents that name's own leaf on the same port.

This host's own daemon enrols itself once the wizard has issued the first license; do not run the server installer with a license on it. Enroll every other server with turbopanel.sh and TURBOPANEL_LICENSE — Daemon setup.

The first admin (hosted)

A self-hosted control plane creates its first superadmin in the install wizard, from host PAM. The TurboPanel High Availability (Workers) control plane has no wizard: signup is off unless TURBOPANEL_IS_SIGNUP_ENABLED says otherwise, and no route ever grants a role. So a freshly migrated TurboPanel High Availability database has no way in — and the tier catalogue needs a superadmin to enter it. Once, from the turbopanel checkout, over the same database URL pnpm migrate uses:

Terminal
CLOUDFLARE_ENV=live TURBOPANEL_DATABASE_URL=… pnpm bootstrap:superadmin -- --email you@example.com

It promotes an existing account and nothing else: it refuses when the database already has a superadmin, never creates a user or sets a password, and — with CLOUDFLARE_ENV set — refuses a database that is not that environment's Hyperdrive origin. Add --dry-run to see what it would do. If the account does not exist yet it says so and stops: enable signup on that environment, sign up as that address, run it again, then turn signup back off. Every later admin is promoted from the console.

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 control plane on Cloudflare Workers. Remote daemons connect over HTTPS/WSS to the control plane's URL. It is not yet publicly available — join the waitlist for access updates.

When access is available, operators create an organization in the app, configure Workers bindings and secrets, and enroll servers with turbopanel.sh (manifest default host — no TURBOPANEL_HOST required).

Maintainers deploy the Workers bundle from the control plane repo:

Terminal
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 deploy

pnpm 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 control plane reads at boot — required, defaulted, and silently degrading — is Control plane configuration. The most common ones:

Key environment variables (control plane)

VariablePurpose
TURBOPANEL_SECRETSSession signing (required in production)
TURBOPANEL_DATABASE_URLFull Postgres connection URL for self-hosted Deno boot (instance-launch) and tooling
DATABASE_URLTooling-only fallback for pnpm migrate / drizzle-kit when TURBOPANEL_DATABASE_URL is unset (common in CI and dashboard deploy)
CLOUDFLARE_ENVWrangler env name for Workers deploy (e.g. live, testing) — required by pnpm deploy
TURBOPANEL_SOCKET / TURBOPANEL_SOCKET_DIRUnix socket path overrides
TURBOPANEL_UI_MODEdev (Expo proxy) or static (exported UI)
TURBOPANEL_UI_ROOTStatic 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_PORTContributor dev overlay only (default 8443). The managed Caddyfile binds a literal :8443 and does not read it
TURBOPANEL_TLS_PUBLICWhen 1 / true, Deno GET /api/daemon/v1/instance/ca 404s for a hostname that presents a Let's Encrypt or uploaded leaf
TURBOPANEL_IS_SIGNUP_ENABLEDForces sign-up open (1) or closed (0), overriding the admin setting — both runtimes
TURBOPANEL_REDIS_SOCKET / TURBOPANEL_AMQP_URLRedis socket (required, opened lazily) and the RabbitMQ URL for the email queue (unset → probe, then a silent no-op queue)
TURBOPANEL_UPDATE_CHANNELThe channel this control plane resolves daemon, control-plane, and UI updates on (release default; trunk, edge, canary, rc)
TURBOPANEL_UPGRADE_STEP_RETENTION_DAYSHow long a successful upgrade step is kept (default 14). Failed steps stay 90 days; runs stay 365 days, newest 50 kept. See Control plane configuration
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 control plane checkout. Provider setup (Admin → Sign-in providers, env-wins, and linking rules): Accounts and access.

Admin → Access

/admin opens Access. /admin/networking redirects there. The area is how people and machines reach this control plane. Organization TLS stays on the organization.

PageWhat it sets
HostnamesEvery address this control plane answers on, each with a source: Platform CA, an uploaded certificate, or Let's Encrypt. Save & Apply (self-hosted) regenerates the Platform CA leaf and reloads Caddy. The body of the apply is empty — the hostname save already persisted the rows.
CertificatesUploaded pairs (the private key is sent once and never shown again) and this control plane's Let's Encrypt settings: contact email, terms, directory URL, staging. A wildcard is an uploaded pair. These settings are unrelated to any organization's Let's Encrypt opt-in.
Trusted proxiesThe effective TURBOPANEL_TRUSTED_PROXY_CIDRS list, read-only. A custom list replaces the loopback default.
TunnelWrite-only token for the co-located tunnel. An empty token tears the tunnel down. The API never returns the stored value.
Platform CAFingerprint, subject, validity, PEM download, and a trust reconcile that asks connected daemons to pick up the current bundle. Self-hosted only. This is the Platform CA.

Per-hostname sources need a co-located daemon that can render them (instance-cert-sources-per-hostname, daemon 0.1.1 or newer). Older daemons keep the rows visible and leave the sources disabled until that daemon is updated.

Admin → Updates is the managed platform upgrade. On self-hosted it runs this host's daemon, then the control plane, then the other servers, and the other servers wait until the first two are on target. On TurboPanel High Availability the control plane line is Managed by TurboPanel and only daemons roll out. Detail: Upgrade and rollback.

API entrypoints

PathDescription
/api/healthUnversioned identity payload — licence, version, commit. It touches nothing, so it is not an outage signal; see What to point a monitor at
/api/daemon/v1/readinessThe readiness probe. Reads the database; this is the one to monitor
/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/v1Daemon WebSocket
/api/client/v1/openapi.json · /api/admin/v1/openapi.jsonOpenAPI 3.1 specs (Scalar at …/reference)
/Web UI (via Caddy)

HTTPS entrypoint (self-hosted): https://<host>:8443. Every hostname is served there. The certificate is chosen by the name. Trust the Platform CA for a name that presents that leaf.

Communication patterns

  • Browser → Caddy → control plane: REST and cookies on /api/client/v1/*
  • Daemon → control plane: 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 control plane; 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 app, 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.

Edit on GitHub

Last updated on

On this page