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 not a slim Docker-only executor in the new architecture — it 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 | https://your-worker.workers.dev | https://<host>:8443 |
| Co-located daemon | Optional (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) |
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 (official installer)
Obtain a license from your TurboPanel organization, then run the channel
installer. Set TURBOPANEL_LICENSE to the base64url-encoded
licenseId:licenseToken (the UI copy-paste command includes this).
TurboPanel High Availability (Cloudflare Workers — default host from the channel manifest):
curl -fsSL turbopanel.sh | TURBOPANEL_LICENSE=<base64url-encoded-license> shSelf-hosted instance (pass TURBOPANEL_HOST with the full instance URL):
curl -fsSL turbopanel.sh | \
TURBOPANEL_LICENSE=<base64url-encoded-license> \
TURBOPANEL_HOST=https://<instance-host>:8443 \
shProduction and self-hosted instances always fetch the installer from
turbopanel.sh. The instance host /run.sh path is development-only — it
is served by the dev overlay Caddyfile (plaintext :8880), not by the
production instance Caddyfile.
# Dev overlay only (plaintext HTTP control plane)
curl -fsSL http://<dev-host>:8880/run.sh | \
TURBOPANEL_LICENSE=<base64url-encoded-license> \
TURBOPANEL_HOST=http://<dev-host>:8880 \
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.
For self-hosted HTTPS, the installer fetches the platform CA from
GET /api/daemon/v1/instance/ca and configures TURBOPANEL_INSTANCE_CA.
Re-run the same command any time to upgrade or reconcile a node.
Refresh an existing node
If the server is already installed, re-run the same run.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 the dev console (Start dev stack) or Ansible co-located install — not the remote node installer.
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. Written to .env as TURBOPANEL_UPDATE_CHANNEL. |
TURBOPANEL_HOST / --host <URL> | Optional for production (defaults to manifest defaultControlPlaneUrl). Required for self-hosted instances (https://…). |
TURBOPANEL_INSECURE_TLS=1 / --insecure-tls | Use curl -k for bootstrap downloads only (dev/self-signed CDN or instance) |
--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, which cannot load the usual 4 KiB-pagedeno compilebinary — install downloadsturbopaneld.js, installs vendored Deno, and usesdeno run …/bin/turbopaneld.jsas the supported ExecStart for that hardware. Deno is installed only on that path; native hosts skip it.
Co-located dev is different
A control-plane host provisioned via the dev
console runs the daemon from a source checkout under $HOME (e.g. ~/daemon, via
deno run main.ts). Config lives in /etc/turbopanel/daemon.env; mutable state, logs,
and sockets under dev-user-owned FHS paths (/etc/turbopanel, /var/lib/turbopanel,
/var/log/turbopanel, /run/turbopanel); vendored runtimes under
/opt/turbopanel/vendor. No dedicated tp / tpctrl / tpcache
service accounts are created — systemd units and Docker-backed services run as the
current dev user. 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 |
| 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
- Control plane — Instance and Caddy layout
- Security — TLS and socket hardening
- Daemon README — Maintainer reference
Last updated on