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 WebSockethello
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
Deployment models
| Feature | TurboPanel High Availability | Self-hosted instance |
|---|---|---|
| Instance URL | Hosted Workers URL (manifest default) | https://<host>:8443 by default (lets_encrypt uses :443) |
| Co-located daemon | Optional (contributor dev / socket mode) | Yes on control plane |
| Remote daemon | Required per extra server | Required per extra server |
| Communication | WSS through instance URL | WSS through Caddy or direct socket |
| Daemon coordination | WSS (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:
| Mode | Listen | GET /api/daemon/v1/instance/ca | --insecure-tls |
|---|---|---|---|
self_signed (default) | :8443 | 200 | yes |
upload | :8443 (or caddy_port) | 200 unless turbopanel_tls_public | follows turbopanel_tls_public |
lets_encrypt | :443 (+ :80 challenge) | 404 | no |
lets_encrypt needs :80/:443 free on the control-plane host. Full table: Control plane.
Installation
Supported platforms
| OS | Architecture | Notes |
|---|---|---|
| Debian 12+ (Bookworm/Trixie) | x86_64 (amd64) | Recommended for managed servers |
| Debian 12+ (Bookworm/Trixie) | aarch64 (arm64) | Supported |
| Raspberry Pi OS 64-bit | aarch64 (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.
curl -fsSL turbopanel.sh | TURBOPANEL_LICENSE=<base64url-encoded-license> shcurl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE=<base64url-encoded-license> \
TURBOPANEL_HOST=https://<instance-host>:8443 \
shThe 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:
curl -fsSL http://<dev-host>:8880/run.sh | \
TURBOPANEL_LICENSE=<base64url-encoded-license> \
TURBOPANEL_HOST=http://<dev-host>:8880 \
shRefresh 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):
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 \
shSee 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 / flag | Description |
|---|---|
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-tls | Use 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-start | Provision 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:
| Purpose | Path |
|---|---|
| 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 viaflock). The unit runs the native binary whenturbopaneld --versionsucceeds. On hosts where that probe fails — notably some Raspberry Pi arm64 kernels with a 16 KiB page size — install downloadsturbopaneld.js, installs vendored Deno, and usesdeno run …/bin/turbopaneld.jsas 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).
| Variable | Purpose |
|---|---|
TURBOPANEL_INSTANCE_URL | HTTPS base URL of instance (remote nodes) |
TURBOPANEL_INSTANCE_CA | Platform CA PEM path for TLS (default /etc/turbopanel/instance-ca.pem) |
TURBOPANEL_UPDATE_CHANNEL | Release channel for manual updates and UI updates (trunk default) |
TURBOPANEL_DEV_INSTANCE | 1 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, optionalserverId,machineId; instance returns canonicalserverId - Commands: instance routes over
daemon-hub; dev-sync and tunnel-token messages for operator pushes
Common issues
| Issue | Solution |
|---|---|
| Connection failures | Verify --host / TURBOPANEL_INSTANCE_URL, firewall, TLS CA trust |
| TLS errors | Re-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 architecture | TurboPanel requires 64-bit aarch64 or x86_64; 32-bit Raspberry Pi OS is not supported |
| Docker permission errors | Ensure tp user is in docker group (installer adds this when Docker is installed) |
See Deployment troubleshooting for more.
Related documentation
- Deployment hub — Operator overview
- Control plane — Instance and Caddy layout
- Security — TLS and socket hardening
- Daemon README — Maintainer reference
Last updated on