TurboPanel Docs
Using TurboPanel

Hosting — hostnames, ports and TLS

A service that runs is not yet a service anyone can reach. Hosting is the part of an environment that says how the outside world gets in: which hostnames answer, on which network face of the server, over which certificate, and — for a database or a game server — which TCP or UDP ports are published. This chapter covers the Hosting tab, the organization's TLS library under Servers → TLS, and the Let's Encrypt path that runs through both.

The model

Every server that serves hostnames runs a hosting Caddy on ports 80 and 443, behind a Traefik that also carries raw TCP/UDP. A hosting row is one entry the platform materializes into that Caddy's configuration: a set of hostnames (or a set of ports), a service and target port behind them, a bind scope, and the certificate to serve.

Hosting rows come from two places, and which one owns a row decides where you edit it:

  • The Hosting tab — rows you add by hand for a service. Edit and delete them there.
  • The compose document — rows declared under x-turbopanel.hosting[] on a service. The platform materializes them on every deploy; on the Hosting tab they are shown read-only, captioned as compose-owned, and any change goes in the document. Trying to edit one through the API is refused with hosting_owned_by_compose.

Three rules hold everywhere:

  1. A hostname is unique within the organization. Two hosting rows in one organization can never claim the same hostname, even on different paths; the database enforces it, not just the form.
  2. The certificate is a pin, not a copy. A hosting row points at a certificate in the organization's TLS library (tlsId). No pin means Caddy's own locally-trusted certificate (tls internal) — fine for a private hostname, wrong for a public one.
  3. The Organization CA never issues hosting certificates. It signs the platform's internal traffic (daemon, managed-database ingress). Public hostnames get an uploaded certificate, a self-signed one, or Let's Encrypt.

Bind scopes

ScopeThe hostname answers onUse it for
publicEvery interface — or one specific public IP when a public IP is picked from the organization's registryAnything reachable from the internet. The only scope that can obtain a Let's Encrypt certificate.
datacenterThe server's datacenter address onlyA service other servers in the same datacenter reach, never the internet. Needs the server to have a datacenter address (datacenter_ip_required at deploy otherwise).
local127.0.0.1A service only that host itself talks to.

The bind is frozen into the host's Caddy at deploy time, so changing it — or a member address moving — leaves the environment marked needs redeploy until you deploy again.

Before you begin

  • A deployed (or at least placed) environment — hosting rows attach to a service in one.
  • For a public hostname: DNS for it pointing at the serving server's public address.
  • For Let's Encrypt: an organization owner or manager has turned on Allow Let's Encrypt certificates under Servers → TLS, the hostname's bind is public, and ports 80 and 443 on that server are reachable from the internet.
  • For a published TCP/UDP port: nothing else on that server already publishes the same port.

Add a hostname

Open the environment's Hosting tab. Below the server pin, each service has a hosting section; add a row under the service that should answer.

Enter the Hostnames — one or more, each a full DNS name (no wildcards). Leave Protocol at Http.

Pick the bind: public, datacenter or local. For public, optionally pick a public IP from the organization's registry to bind only that address; the IP must itself be registered with scope public (hosting_bind_scope_mismatch otherwise).

Set the Target port the service listens on inside its container, and an optional Path prefix if only a sub-path of the hostname belongs to this service (with Strip prefix to remove it before forwarding).

Choose the certificate: leave it unset for tls internal, or pin a row from the library. The library only offers rows whose names cover every hostname on the row.

Toggle Force HTTPS, Gzip and Brotli as needed, then save. The hostname is live on the next deploy.

Hosting variables and PHP options

Once a row is saved, it carries its own Variables panel — the hosting scope of the variable cascade, for values that differ per hostname (a SITE_URL, a per-host feature flag). For a site served by a host engine, the row also exposes PHP options: series (8.3, 8.4), memory_limit, max_execution_time, and up to 64 static environment entries (web.env) of at most 4096 characters each.

Publish a TCP or UDP port

Add a hosting row under the service and set Protocol to Tcp or Udp. The hostnames field is replaced by Ports.

Enter up to 10 mappings as published:target (or just published when both sides match). Each published port must be unique on the row; the same port published by another service on the same server is refused at deploy (host_port_conflict).

Pick the bind scope as for a hostname, and save. Traefik carries the port on the next deploy; TLS options do not apply.

The TLS library

Servers → TLS is the organization's certificate library. Every row has a source and a status:

SourceWhat it isWho renews it
uploadA certificate and private key you paste as PEMYou — upload a new row before it expires and re-pin
self_signedA certificate the platform mints for the hostnames you list (RSA-2048, about 90 days)You, by minting again
lets_encryptIntent, not material: the hostnames Caddy should obtain a certificate for. The row never holds a key.Caddy on the serving host, automatically
organization_caThe organization's internal CA. Platform-managed; not pinnable to a hostingThe platform's renewal sweep
StatusMeaning
readyMaterial present and valid; pinnable.
pendingRequested, material not yet present.
managedA Let's Encrypt row Caddy issues and renews on the host — pinnable; the platform holds no material.
expiredPast notAfter. Replace and re-pin.
failedIssuance or import failed; see the row's message.
revokedRetired by you. Pins fall back to tls internal on the next deploy.

Upload a certificate

Servers → TLS → Add certificate, source Upload.
Paste the Certificate PEM (the leaf first, then any intermediates) and the Private key PEM. An optional Display name labels the row.
Save. The platform reads the names off the certificate; a certificate already in the library is refused with tls_fingerprint_conflict.
On the environment's Hosting tab, pin the row on each hosting it should serve, and deploy.

Mint a self-signed certificate

Source Self-signed, list the Hostnames, save. Useful for a datacenter or local hostname, or as a placeholder before a real certificate exists.

Let's Encrypt

Let's Encrypt is off by default for every organization. Some operators do not want the platform contacting a public CA on their servers' behalf at all, so it is an explicit opt-in, and turning it back off is immediate: no new certificate can be requested, and a deploy that still pins a Let's Encrypt (managed) row is refused with acme_requires_org_opt_in until you turn the opt-in back on or pin another certificate. Certificates Caddy already issued are not revoked; they stay on the host.

Servers → TLS, turn on Allow Let's Encrypt certificates. Until this is on, the Let's Encrypt source is disabled in Add certificate and the API refuses with lets_encrypt_not_enabled.

Add certificate, source Let's Encrypt, list the exact Hostnames — no wildcards (wildcard_unsupported), and HTTP-01 is the only challenge (dns_01_unsupported). The row is created with status managed.

Make sure each hostname resolves to the serving server and that ports 80 and 443 on it are open to the internet.

On the Hosting tab, set the hosting's bind to public and pin the row. Deploy. Caddy obtains the certificate on first contact and renews it on its own; the platform never sees the key.

If issuance fails — a hostname that does not resolve, a closed port 80, a Let's Encrypt rate limit — the daemon notices within a minute or two and the row shows Issuance failing with the reason; the deploy itself is not affected, and the hosting keeps serving Caddy's local certificate until issuance succeeds.

Revoke and retire

Revoke on a library row sets it revoked; hostings pinned to it fall back to tls internal on their next deploy. There is no CRL or OCSP: a client that already holds the certificate keeps trusting it until it expires. The organization CA is retired and rotated as a whole, from its own panel — Organization CA.

Reference

Hosting options

FieldValuesNotes
hostnames[]Full DNS namesRequired for http. Unique per organization.
protocolhttp (default) · tcp · udptcp/udp replace hostnames with ports.
ports[]{ published, target }Required for tcp/udp; at most 10; published unique on the row; ports 1–65535.
bindpublic · datacenter · localSee Bind scopes.
ipIduuidA registered public IP to bind; needs bind: public and an IP of scope public.
tlsIduuid or unsetThe library pin; unset = tls internal. Compose spells it tls: { mode, certificateRef }.
targetPort1–65535The container port.
pathPrefix/pathRoute only this prefix.
proxy.forceHttps · proxy.gzip · proxy.brotli · proxy.stripPrefixboolean / stringThe Force HTTPS, Gzip, Brotli, Strip prefix controls.
web.envup to 64 keys ([A-Za-z_]\w*), values ≤ 4096 charsStatic environment for host-engine sites.
web.phpversion, memoryLimit, maxExecutionTimeSite PHP options.

Compose spelling

The same row, declared on a service:

YAML
x-turbopanel:
  hosting:
    - hostname: shop.example.com
      pathPrefix: /api
      targetPort: 3000
      forceHttps: true
      tls: { mode: acme }                  # internal | certificate (+ certificateRef) | acme
      bind: { scope: public, ipRef: edge } # public | datacenter | local

certificateRef names a library certificate by its id or its display name; ipRef names a registered IP by its id or its address. An unknown reference is refused at deploy (hosting_tls_ref_unresolved, hosting_ip_ref_unresolved); the editor's linter flags it earlier when it knows the organization's library.

Certificate lifetimes

CertificateLifetime
Self-signed library certificate~90 days
Let's EncryptSet by Let's Encrypt; renewed by Caddy on the host
Organization CA leaves (internal)90 days, re-issued at a third remaining
Organization CA root3650 days

Errors

CodeStatusMeaningWhat to do
hostname_in_use409Another hosting in the organization already serves this hostname.Find it under that project's Hosting tab; remove it or pick another name.
hosting_owned_by_compose409The row is declared in the compose document.Edit x-turbopanel.hosting[] on the service instead.
hosting_bind_scope_mismatch400A public IP was picked with a non-public bind, or the IP's registered scope is not public.Set bind: public, or pick an IP registered as public.
invalid_hosting_options400A field has the wrong shape (a bad port, a tcp row with no ports, more than 10 ports).See the options table.
lets_encrypt_not_enabled403The organization has not opted in.Servers → TLS → Allow Let's Encrypt certificates.
wildcard_unsupported, dns_01_unsupported400A *. hostname, or a DNS-01 challenge, on a Let's Encrypt row.List each hostname; HTTP-01 only.
tls_fingerprint_conflict409This certificate is already in the library.Pin the existing row.
organization_ca_exists, organization_ca_retire_required409An organization CA already exists, or the current one must be retired before a new one is minted.Use the Organization CA panel's rotate flow.
acme_requires_public_bind400 (deploy)A Let's Encrypt row is pinned on a datacenter or local hosting.Set the bind to public.
acme_requires_org_opt_in400 (deploy)A managed row is pinned but the organization's opt-in is off.Turn the opt-in back on, or pin another certificate.
tls_pin_not_found, tls_pin_mismatch, tls_pin_not_ready400 (deploy)The pinned row is gone, does not cover the hostnames, or is not ready/managed.Re-pin.
hosting_route_conflict, hosting_hostname_conflict409 (deploy)Two rows in the environment claim the same hostname and path, or a compose-declared hostname collides with another hosting in the organization.Change one of them.
host_port_conflict422 (deploy)Two services on one server publish the same port.Change the published port.
datacenter_ip_required422 (deploy)A datacenter bind on a server with no datacenter address.Add the server to a datacenter, or use public/local.
Edit on GitHub

Last updated on

On this page