TurboPanel Docs
Deployment

Daemon Setup

The TurboPanel daemon (turbopaneld) runs on managed servers. It connects to the instance over HTTPS/WSS, runs Ansible to install runtimes and services, and manages Docker, tunnels, and orchestration locally.

Overview

Purpose

  • Connect to the instance control plane as a remote daemon
  • Run Ansible playbooks (installs, upgrades, Postgres, Caddy, instance, UI)
  • Apply dev-sync pushes and Cloudflare tunnel tokens from the instance
  • Report server identity (serverId, hostname, machineId) on WebSocket hello

Relationship to control plane

The daemon is the constant on every TurboPanel-managed host and the only party that runs Ansible. On the control plane host it runs co-located (Unix socket or Caddy URL). On additional servers it dials the instance URL you provide at install time.

Architecture

Diagram will load when scrolled into view

Deployment models

FeatureTurboPanel High AvailabilitySelf-hosted instance
Instance URLHosted Workers URL (manifest default)https://<host>:8443 by default (lets_encrypt uses :443)
Co-located daemonOptional (contributor dev / socket mode)Yes on control plane
Remote daemonRequired per extra serverRequired per extra server
CommunicationWSS through instance URLWSS through Caddy or direct socket
Daemon coordinationWSS (Durable Objects)WSS (Redis cell)

Both control-plane models are in private alpha and not yet publicly available. The installer and daemon behavior below apply when you have access.

Self-hosted TLS modes (turbopanel_tls_mode) and the ports they bind:

ModeListenGET /api/daemon/v1/instance/ca--insecure-tls
self_signed (default):8443200yes
upload:8443 (or caddy_port)200 unless turbopanel_tls_publicfollows turbopanel_tls_public
lets_encrypt:443 (+ :80 challenge)404no

lets_encrypt needs :80/:443 free on the control-plane host. Full table: Control plane.

Installation

Supported platforms

OSArchitectureNotes
Debian 12+ (Bookworm/Trixie)x86_64 (amd64)Recommended for managed servers
Debian 12+ (Bookworm/Trixie)aarch64 (arm64)Supported
Raspberry Pi OS 64-bitaarch64 (arm64)Supported on 64-bit images only

Not supported: 32-bit ARM (armv7l, armhf), 32-bit Raspberry Pi OS, or any CPU architecture other than aarch64 and x86_64. Unsupported hosts fail during Ansible provisioning with an explicit architecture error.

Remote managed server (production installer)

Obtain a license from your TurboPanel organization. Set TURBOPANEL_LICENSE to the base64url-encoded licenseId:licenseToken (the UI copy-paste command includes this).

Run the channel installer from turbopanel.sh — production and self-hosted instances use this host, not the contributor Vagrant workflow.

For self-hosted HTTPS, the installer fetches the platform CA from GET /api/daemon/v1/instance/ca by default and configures TURBOPANEL_INSTANCE_CA. A 404 (Let's Encrypt / TURBOPANEL_TLS_PUBLIC) uses the system trust store instead. Re-run the same command any time to upgrade or reconcile a node.

Self-hosted examples below use :8443 by default. Omit the port (or use :443) when turbopanel_tls_mode=lets_encrypt. Public TLS omits --insecure-tls.

Terminal
curl -fsSL turbopanel.sh | TURBOPANEL_LICENSE=<base64url-encoded-license> sh
Terminal
curl -fsSL turbopanel.sh | \
  TURBOPANEL_LICENSE=<base64url-encoded-license> \
  TURBOPANEL_HOST=https://<instance-host>:8443 \
  sh

The installer self-escalates with sudo and prompts for your password when needed — do not prefix the pipeline with sudo.

While provisioning runs, the terminal shows a rolling status view (spinner on TTY hosts) with neutral labels — orchestration instead of Ansible tool names, cache instead of Redis, queue instead of RabbitMQ. Vendor paths, role names, and env vars on disk are unchanged; full detail remains in /var/log/turbopanel/daemon.log after install. See the daemon repo AGENTS.md — Installer presentation layer.

Contributor dev overlay only

The instance host /run.sh path is development-only — served by the dev overlay Caddyfile (plaintext :8880), not the production instance Caddyfile:

Terminal
curl -fsSL http://<dev-host>:8880/run.sh | \
  TURBOPANEL_LICENSE=<base64url-encoded-license> \
  TURBOPANEL_HOST=http://<dev-host>:8880 \
  sh

Refresh an existing node

If the server is already installed, re-run the same turbopanel.sh installer. Build the license argument from state on disk (or copy a fresh install command from the UI):

Terminal
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('='))
")

curl -fsSL turbopanel.sh | \
  TURBOPANEL_LICENSE="$LICENSE_B64" \
  TURBOPANEL_UPDATE_CHANNEL=trunk \
  sh

See Refresh a stuck daemon for offline UI mismatches, missing license files, and self-hosted hosts.

Control plane host

On the machine that runs the instance, use contributor dev (Start dev stack) for local development — or the managed production converge for self-hosted when available. Do not use the remote node installer on the co-located dev control plane.

Install options

Variable / flagDescription
TURBOPANEL_LICENSE / --license <b64>Required. Base64url-encoded licenseId:licenseToken.
TURBOPANEL_UPDATE_CHANNEL / --channel <name>Release channel. Default: trunk. Channel names (trunk, canary, rc, release, …) are technical identifiers — not product branding. release and rc resolve from the daemon's GitHub Releases (latest non-pre-release, and the rolling rc pre-release); trunk from the per-merge build drop. Written to .env as TURBOPANEL_UPDATE_CHANNEL.
TURBOPANEL_HOST / --host <URL>Optional for TurboPanel High Availability (defaults to manifest defaultControlPlaneUrl). Required for self-hosted instances (https://…).
TURBOPANEL_INSECURE_TLS=1 / --insecure-tlsUse curl -k for bootstrap downloads only (platform CA / self-signed). Omitted when TURBOPANEL_TLS_PUBLIC is set (Let's Encrypt, or an uploaded publicly trusted cert).
--tunnel-token <TOKEN>Cloudflare tunnel token for this node
--instance-ca <PATH>PEM platform CA (skips automatic CA fetch)
--no-startProvision without starting turbopaneld.service

What gets installed (FHS layout)

The installer bootstraps uv/Python/Ansible and runs daemon-install.yml, which lays out a clean Filesystem Hierarchy Standard tree — there is no source checkout on a managed server:

PurposePath
Native daemon binary/opt/turbopanel/bin/turbopaneld
Deno JS runtime (hosts where the native binary cannot load)/opt/turbopanel/bin/turbopaneld.js
Orchestration assets (Ansible)/opt/turbopanel/share/orchestration
Static UI export (control plane host)/opt/turbopanel/share/ui
Vendored runtimes (deno/uv/python/ansible/cloudflared)/opt/turbopanel/vendor
Config (daemon.env, instance-ca.pem)/etc/turbopanel
Persistent identity (license, server.id, keys, tunnels)/var/lib/turbopanel
Logs/var/log/turbopanel
Runtime (sockets, daemon.lock)/run/turbopanel
  • User tp:tp (UID/GID 9999) with passwordless sudo
  • turbopaneld.service (systemd, single process via flock). The unit runs the native binary when turbopaneld --version succeeds. On hosts where that probe fails — notably some Raspberry Pi arm64 kernels with a 16 KiB page size — install downloads turbopaneld.js, installs vendored Deno, and uses deno run …/bin/turbopaneld.js as the supported ExecStart for that hardware.

Contributor dev is different

A control-plane host provisioned via the dev console runs the daemon from a source checkout under $HOME (e.g. ~/turbopaneld, via deno run main.ts). Config lives in /etc/turbopanel/daemon.env; mutable state, logs, and sockets under dev-user-owned FHS paths; vendored runtimes under /opt/turbopanel/vendor. No dedicated tp / tpctrl / tpcache service accounts are created. The FHS tree above (including tp:tp) applies to managed/production installs only.

Configuration

Runtime config: /etc/turbopanel/daemon.env (EnvironmentFile= on turbopaneld.service).

VariablePurpose
TURBOPANEL_INSTANCE_URLHTTPS base URL of instance (remote nodes)
TURBOPANEL_INSTANCE_CAPlatform CA PEM path for TLS (default /etc/turbopanel/instance-ca.pem)
TURBOPANEL_UPDATE_CHANNELRelease channel for manual updates and UI updates (trunk default)
TURBOPANEL_DEV_INSTANCE1 for co-located dev (Ansible installs instance/UI)

Co-located socket mode omits TURBOPANEL_INSTANCE_URL and dials unix:///run/turbopanel/instance.sock.

Communication

  • WebSocket: wss://<instance>/ws/daemon/v1
  • Hello: daemon sends hostname, optional serverId, machineId; instance returns canonical serverId
  • Commands: instance routes over daemon-hub; dev-sync and tunnel-token messages for operator pushes

Common issues

IssueSolution
Connection failuresVerify --host / TURBOPANEL_INSTANCE_URL, firewall, TLS CA trust
TLS errorsRe-fetch CA from /api/daemon/v1/instance/ca or pass --instance-ca. A 404 means public TLS — use the system trust store (no instance-ca.pem, no --insecure-tls).
Unsupported architectureTurboPanel requires 64-bit aarch64 or x86_64; 32-bit Raspberry Pi OS is not supported
Docker permission errorsEnsure tp user is in docker group (installer adds this when Docker is installed)

See Deployment troubleshooting for more.

Edit on GitHub

Last updated on

On this page