Control Plane Configuration Reference
What a self-hosted control plane (the Deno control plane, run by the turbopanel-instance unit from /opt/turbopanel/bin/turbopanel) reads from its environment at boot, in one place. On a managed install the daemon's instance-launch role writes these into the unit and /etc/turbopanel/instance/runtime.env; you only set them by hand on a bespoke host. TurboPanel High Availability (Cloudflare Workers) reads the subset marked Workers too from Wrangler vars and secrets.
Three behaviours matter more than any single value:
- Two variables throw at boot when missing:
TURBOPANEL_DATABASE_URLandTURBOPANEL_SECRET/TURBOPANEL_SECRETS. Everything else has a default. - Redis is required but not checked up front: the daemon cell, the query cache and the auth rate limiters open
TURBOPANEL_REDIS_SOCKETlazily; a missing socket surfaces as failures on the first request that needs it, not as a refusal to start. - The email queue degrades silently: with
TURBOPANEL_AMQP_URLunset the control plane probes the dev broker, and with no broker reachable it logs one line and uses a no-op queue — sign-ups, invitations and verifications then produce no email at all.
Required
| Variable | Read by | Behaviour |
|---|---|---|
TURBOPANEL_DATABASE_URL | boot, migrate verb | Postgres URL. Socket form on managed installs: postgresql://<user>:<pass>@/<db>?host=/var/run/turbopanel/postgres. Throws TURBOPANEL_DATABASE_URL is required when unset. Managed installs reach Postgres over that Unix socket path; TCP Postgres is a source-mode development option. |
TURBOPANEL_SECRET or TURBOPANEL_SECRETS | boot | The root secret keyring (session signing, OTP and TOTP verifiers, data-encryption key derivation). TURBOPANEL_SECRETS is the versioned form 2:<new secret>,1:<old secret> written by instance-launch to /etc/turbopanel/instance/.instance_secrets. The first entry is the current key: it signs and encrypts, and the later entries only verify and decrypt. List the highest version first. An out-of-order list is used as written, with a warning in the log, so putting the old key first keeps signing with it. TURBOPANEL_SECRET is a single value. Throws when neither is set. Losing this keyring loses every encrypted row — see Security. |
Runtime services
| Variable | Default | Behaviour |
|---|---|---|
TURBOPANEL_REDIS_SOCKET | /run/turbopanel/redis.sock | Redis over a Unix socket: daemon cell (presence, outbox, request correlation), query cache, auth and daemon rate limiters. Opened lazily; not validated at boot. |
TURBOPANEL_AMQP_URL | probe amqp://guest:guest@localhost:19828, else no-op | RabbitMQ for the email queue (the control plane's in-process email consumer drains it). Empty string means "no queue" explicitly; unset probes the dev broker URL and falls back to a no-op queue with one log line. Managed installs set it from the rabbitmq role. |
TURBOPANEL_UPDATE_CHANNEL | release | The channel this control plane resolves daemon updates on (trunk, canary, rc, release; edge reserved). Every queued update carries it. An unknown value is a boot error, like the daemon's. Managed installs write the channel they installed from. |
TURBOPANEL_AUTO_FAILOVER | on | Whether this control plane may fence and promote a managed database's failover replica on its own when a primary is reported dead. on or off (also true/false, 1/0), read when each report arrives, so no restart is needed after a change. Any other value counts as off. Unset is on, except when TURBOPANEL_ENVIRONMENT is staging or live. Off records a blocked recovery with auto_failover_disabled and queues nothing; manual switchover and disaster recovery are unaffected. See Automatic failover. Workers too. |
TURBOPANEL_FIREWALL_APPLY_SERVERS | empty | Up to three server UUIDs, separated by commas, that may be sent firewall rules to load (not just a preview) when an owner or manager also sets their firewall mode to Managed. Empty, or any *, all or malformed entry, means no server. Ignored when TURBOPANEL_ENVIRONMENT is live. See Turn enforcement on for one server. |
TURBOPANEL_INSTANCE_SERVICE | turbopanel-instance | The systemd unit the developer surface restarts after Upgrade System. Set it only when the unit has a non-standard name. |
Paths (FHS layout)
| Variable | Default | Purpose |
|---|---|---|
TURBOPANEL_STATE_DIR | /var/lib/turbopanel | Durable state: the platform CA (tls/), metrics, execution logs, and in compiled mode the server leaf certificate (tls/certs/). |
TURBOPANEL_CONFIG_DIR | /etc/turbopanel | Protected config (instance/runtime.env, the secret keyring, public URLs). |
TURBOPANEL_RUN_DIR | /run/turbopanel | Runtime sockets. TURBOPANEL_SOCKET_DIR is the older name for the same value; TURBOPANEL_SOCKET overrides the control plane listen socket path itself (<run dir>/instance.sock). |
TURBOPANEL_LOG_DIR | /var/log/turbopanel | File logs. |
TURBOPANEL_METRICS_DIR | <state>/metrics | The embedded DuckDB metrics store. |
TURBOPANEL_EXECUTION_LOG_DIR | <state>/execution-logs | Command and deploy logs when TURBOPANEL_EXECUTION_LOG_DRIVER is file (the default). |
LD_LIBRARY_PATH (unit) | /opt/turbopanel/lib | Where the compiled instance finds libduckdb.so. The turbopanel-instance unit sets it; there is no TURBOPANEL_* variable for this path. |
TURBOPANEL_UI_ROOT | /opt/turbopanel/share/ui | The static web export Caddy serves (TURBOPANEL_UI_MODE=static); co-located development points it at a checkout. |
Public identity and TLS
| Variable | Default | Behaviour |
|---|---|---|
TURBOPANEL_PUBLIC_URLS | derived | Comma-separated projection of the Platform CA hostnames. Admin → Access → Hostnames is the editor; this setting stays in sync so certificate SANs, webhook reachability, and the install command keep reading it. Unset, the control plane derives an origin from the host's interfaces. Every stored name is served at https://<host>:8443. |
TURBOPANEL_BASE_URL | derived from the request | The origin used in outbound links (verification and invitation emails, offline-sweep notices). Unset, it is resolved per request; behind a Unix socket the control plane guards against the literal null origin. |
TURBOPANEL_TLS_PUBLIC | unset | 1 / true when any hostname is Let's Encrypt, or when the operator marks TLS public. GET /api/daemon/v1/instance/ca then 404s for a name that presents a Let's Encrypt or uploaded leaf. An unlisted name on :8443 still receives the Platform CA bundle. Install commands omit --insecure-tls for a publicly trusted leaf. |
TURBOPANEL_INSTANCE_ACME__CONTACT_EMAIL | unset | Contact email for this control plane's Let's Encrypt account. Env wins over Admin → Access → Certificates. Independent of every organization's Let's Encrypt opt-in. |
TURBOPANEL_INSTANCE_ACME__TOS_ACCEPTED | false | Terms acceptance for that account. Same env-wins rule. |
TURBOPANEL_INSTANCE_ACME__DIRECTORY_URL | Let's Encrypt production directory | ACME directory URL. Same env-wins rule. |
TURBOPANEL_INSTANCE_ACME__USE_STAGING | false | true selects the Let's Encrypt staging directory. Same env-wins rule. |
TURBOPANEL_TLS_CA, TURBOPANEL_TLS_CA_KEY, TURBOPANEL_TLS_CA_BUNDLE | <state>/tls/ca.crt, ca.key, ca-bundle.pem | The Platform CA the generate-self-signed-cert verb maintains and daemons trust. |
TURBOPANEL_TLS_CERTS_DIR | <state>/tls/certs (compiled) / <checkout>/certs (source) | Where the Platform CA server leaf is written. Let's Encrypt and uploaded leaves for individual hostnames are files beside it. |
CADDY_PORT | 8443 | Development only. The port the development Caddy unit (and the docs site's dev unit) listens on and links to. The control plane does not read it: install origins always use :8443, and a managed Caddyfile binds :8443 whatever this is set to. |
CADDY_TLS_CERT, CADDY_TLS_KEY | <certs dir>/self-signed.crt, .key | Leaf paths the development Caddyfile reads. A managed Caddyfile bakes platform-ca.* plus per-hostname files at render time. The Caddy unit sets these from TURBOPANEL_TLS_CERTS_DIR. |
TURBOPANEL_REVISION | stamped at build | The exact source commit /api/health reports for AGPL Corresponding Source. A release build carries its commit already; source-mode installs set it from git rev-parse HEAD. |
The four TURBOPANEL_INSTANCE_ACME__* keys are this control plane's settings. Environment wins over Admin → Access → Certificates, the same way other Admin settings work, and an env-sourced key is read-only there. They have no effect on an organization's Allow Let's Encrypt certificates, and that organization toggle has no effect on them. Hostname rows (source, uploaded certificate, last issuance error) live in the database, edited from Admin → Access → Hostnames, not in these variables.
Sign-up and providers
| Variable | Default | Behaviour |
|---|---|---|
TURBOPANEL_IS_SIGNUP_ENABLED | unset | Forces sign-up open (1 / true) or closed (0 / false), overriding the admin setting. Rarely set on a self-hosted host; the install wizard creates the first account either way. Workers too. |
TURBOPANEL_AUTH_PROVIDERS__GITHUB_CLIENT_ID, …__GITHUB_CLIENT_SECRET, …__GOOGLE_CLIENT_ID, …__GOOGLE_CLIENT_SECRET | unset | Sign in with GitHub / Google. Env wins over the SYSTEM_AUTH_PROVIDERS setting row. Workers too (as secrets). |
Every SYSTEM_EMAIL setting has an environment form TURBOPANEL_SYSTEM_EMAIL__<KEY> that wins over the value stored in the admin settings: PROVIDER (smtp default, mailgun, mailpit-api on Workers only, mailpit-smtp on Deno only), FROM (noreply@turbopanel.local), SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, MAILPIT_API_URL (Workers mailpit-api — full HTTP base URL), MAILPIT_SMTP_PORT (Deno mailpit-smtp — co-located Mailpit SMTP, default 1025), MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_REGION, RATE_LIMIT_PER_MINUTE, RATE_LIMIT_BURST, QUEUE_PREFETCH. Secrets stored in the database are tpsecret envelopes sealed with the root keyring. On self-hosted Deno the email consumer runs inside the control plane process and reads the same variables; on Workers the mailpit-api and mailgun providers send directly from the request path.
Metrics and logs
| Variable | Default | Behaviour |
|---|---|---|
TURBOPANEL_SERVER_METRICS_RETENTION_DAYS | 90 | How long the DuckDB metrics store keeps samples. Workers too (Analytics Engine). |
TURBOPANEL_SERVER_METRICS_DUCKDB_THREADS, TURBOPANEL_SERVER_METRICS_DUCKDB_MEMORY_LIMIT | 2, 128 (MiB) | Resource caps for the embedded store. |
TURBOPANEL_EXECUTION_LOG_DRIVER | file | file (the default) or s3; any other value falls back to files. With s3: TURBOPANEL_EXECUTION_LOG_S3_BUCKET, _ENDPOINT, _REGION, _ACCESS_KEY_ID, _SECRET_ACCESS_KEY, _FORCE_PATH_STYLE. |
TURBOPANEL_EXECUTION_LOG_RETENTION_DAYS | 90 | Retention for command and deploy logs. Workers too. |
TURBOPANEL_DAEMON_WS_INBOUND_LIMIT, TURBOPANEL_DAEMON_WS_INBOUND_WINDOW_MS | built-in | Per-daemon inbound WebSocket rate limit. Workers too. |
TURBOPANEL_ARGON2ID_MEMORY_KIB, TURBOPANEL_ARGON2ID_TIME_COST | built-in | Password-hash work factor; the hasher is verified available at boot. |
TURBOPANEL_UI_CORS_ORIGINS | unset | Extra browser origins (a docs site, a Metro dev server) allowed read-only cross-origin API access. Workers too. |
TURBOPANEL_DAEMON_DEBUG | unset | 1 / true turns on debug logging. |
TURBOPANEL_UPGRADE_STEP_RETENTION_DAYS | 14 | How long a finished successful upgrade step (done or skipped) is kept. Blank, non-integer, or out of range (not 1–3650) falls back to 14. Failed steps (failed, rolled_back, needs_attention) stay 90 days. Terminal upgrade runs stay 365 days, and the newest 50 are kept past that. High Availability too. |
TURBOPANEL_UPDATE_HEALTH_TIMEOUT_SECONDS | 600 | How long this host's daemon waits for the new control plane to pass GET /api/health after an update restarts it, before it restores the previous build. Whole seconds, 30 to 3600; anything else keeps 600. Set it in /etc/turbopanel/daemon.env and restart turbopaneld. |
TURBOPANEL_UPGRADE_VERIFY_TIMEOUT_MINUTES | 20 | How long a control-plane update step may stay Restarting or Verifying with no word from the daemon before it becomes needs_attention with verify_timeout. Whole minutes, 5 to 180; anything else keeps 20. Keep it longer than TURBOPANEL_UPDATE_HEALTH_TIMEOUT_SECONDS on the host. |
Development only
Read only when the developer surface is on (TURBOPANEL_DEV_SURFACE=1, written solely by the managed turbopanel-instance unit for co-located Deno source-mode development; TURBOPANEL_UI_MODE only selects Expo vs. static serving), never on a managed install: TURBOPANEL_DEV_USER, TURBOPANEL_DEV_HOST_AUTH, TURBOPANEL_TRUNK_BRANCH, TURBOPANEL_DAEMON_REPO / TURBOPANEL_UI_REPO (optional checkout overrides; defaults derive from TURBOPANEL_DEV_ROOT), TURBOPANEL_UI_MODE=dev, TURBOPANEL_DRIZZLE_STUDIO_HOST / _PORT, TURBOPANEL_NODE, TURBOPANEL_DENO, TURBOPANEL_RUNTIMES_DIR, TURBOPANEL_USER, CADDY_PORT. See Local development.
Workers only
HYPERDRIVE / HYPERDRIVE_CACHED (Hyperdrive bindings), TURBOPANEL_COMMAND_QUEUE (Cloudflare Queue binding), TURBOPANEL_TLS_CA_PEM_B64 (the platform CA served to daemons), TURBOPANEL_ANALYTICS_ENGINE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, TURBOPANEL_PROJECT_ID, and the Stripe billing secrets. These are configured in wrangler.jsonc and the Cloudflare dashboard; the self-hosted binary never reads them.
Stripe webhook signing secret (High Availability). A Stripe Dashboard webhook endpoint has its own whsec_… signing secret, distinct from the one the Stripe CLI prints. Set it with wrangler secret put TURBOPANEL_STRIPE_WEBHOOK_SIGNING_SECRET (a secret, never a vars entry); until it is set, /webhook/stripe answers 503 stripe_webhook_not_configured. A self-hosted control plane has no billing surface and no Stripe webhook, so there is nothing to set there.
The install-time verbs
The compiled instance binary carries three verbs the installer runs before the unit exists, each reading the variables above: turbopanel migrate (TURBOPANEL_DATABASE_URL), generate-secret (none), and generate-self-signed-cert (TURBOPANEL_STATE_DIR, TURBOPANEL_TLS_*, TURBOPANEL_PUBLIC_URLS, TURBOPANEL_TLS_EXTRA_SANS). None of them loads the metrics store, so they run without libduckdb.so on the library path.
Last updated on