tnl.dev :: docs

self-hosting tnld.

understand the deployment shapes and responsibilities of a tnl server operator.

A tnl server includes PostgreSQL and one or more tnld processes. Start with the standalone role for a small deployment; use separate control, ingress, and relay services when those roles need independent capacity or failure boundaries.

choose a deployment shape

tnld roles: The responsibilities and durable storage access of each tnld role.
TNLD_MODEresponsibilityPostgreSQL
standalonecontrol, ingress, two logical relay servicesrequired
controlstate, placement, certificates, administrationrequired
ingressvisitor policy, relay selection, forwardingnever
relaypublisher connections and visitor streamsnever

Standalone composes the complete service in one process while preserving two logical relay services. A split deployment runs a control service, an ingress service, and at least two independently addressable relay services. Only control and standalone receive database, storage key, hosted authority, DNS provider, or ACME account credentials.

required configuration

required role settings: The control-owned and process-local settings required by each role.
rolerequired settings
standalonedatabase, domains, ACME, login token, storage key
controlstandalone control settings plus cluster secret
ingresscontrol hostname, cluster secret, ingress ID
relaycontrol hostname, cluster secret, service and relay IDs, addresses

TNLD_CONTROL_HOSTNAME is a canonical hostname without a scheme, path, or port; HTTPS on port 443 is implied. Every ingress and relay creates a process run ID at startup. Configured process identities remain stable across runs.

Listen addresses, limits, timing, metrics, a non-default ACME directory, and static public certificate overrides are advanced settings. There is no private certificate authority or custom client root configuration to distribute.

migrate and protect postgresql

tnld migrate is the only migration path and reads only TNLD_DATABASE_DIRECT_URL:

migrate the control database
TNLD_DATABASE_DIRECT_URL='postgresql://tnl_migrate:...@database.example/tnl?sslmode=require' \
  tnld migrate

Serving control and standalone processes read only the pooled TNLD_DATABASE_URL. They never run migrations and require the exact schema version supported by their binary. Ingress and relay processes must never receive either URL.

Control and standalone also require TNLD_STORAGE_KEY, the canonical unpadded base64url encoding of exactly 32 random bytes. It encrypts recoverable ACME and certificate material in PostgreSQL. Back it up separately in the deployment secret store; losing the current and previous keys makes the encrypted material unrecoverable.

Rotate by deploying the new TNLD_STORAGE_KEY with the old value in TNLD_STORAGE_KEY_PREVIOUS, waiting for control to re-encrypt stored secrets, and then removing the previous value in a second rollout.

plan addresses and dns

The server domain is an infrastructure suffix. The managed deployment domain contains public route namespaces. They are independent:

example domains
TNLD_SERVER_DOMAIN=tnl.example.com
TNLD_MANAGED_DEPLOYMENT_DOMAIN=tunnels.example.com
public DNS: Hostnames derived from the example server domain and the traffic each record receives.
hostnamedestination
control.tnl.example.comcontrol service
ingress.tnl.example.comingress address
relay.tnl.example.comstandalone relay listener
relay-a.tnl.example.comsplit relay service A
relay-b.tnl.example.comsplit relay service B
route hostnames beneath tunnels.example.comingress address

Public route DNS always targets the ingress address. Relay placement never changes route DNS. TNLD_RELAY_SERVICE_ID selects the split service label and stable relay address.

Control obtains exact public certificates through ACME. Relay transport TLS uses an exact-hostname WebPKI certificate managed by control, and publishers verify it with system trust roots. Route TLS is separately publicly trusted and terminates at the publisher.

For automated split relay certificates, control needs the Route 53 zone containing the server domain. Route DNS automation additionally needs the managed-domain zone and at least one stable ingress IPv4 or IPv6 address. DNS credentials belong only on control. Static certificate files and operator-managed route records remain advanced alternatives.

create the bootstrap token

Generate a bootstrap management token and store it as a deployment secret:

generate a management token
tnld login-token

Provide it to control and standalone as TNLD_LOGIN_TOKEN. It authenticates one built-in administrator identity and permanent personal team. Use OIDC for multiple people and retain the token for operator recovery.

configure cluster authentication

A split deployment shares one high-entropy TNLD_CLUSTER_SECRET among control, ingress, and relay processes. It authenticates private process coordination at control.<server-domain>:9443; restrict that listener and internal relay addresses to the deployment network. The cluster secret is not a user credential and does not authorize the public control API.

For rolling rotation, deploy the new value in TNLD_CLUSTER_SECRET and the old value in TNLD_CLUSTER_SECRET_PREVIOUS, update every split process, and then remove the previous value. Standalone does not accept a cluster secret because its components communicate in process.

Each ingress and relay lease is valid only while its exact configured identity, process run ID, and lease revision are current. Restarted or expired processes cannot continue using stale state.

run standalone with compose

The reference Compose file runs migration and then one standalone process. Create deploy/.env, replace every placeholder, and include at least:

standalone environment
TNLD_MODE=standalone
TNLD_DATABASE_URL=postgresql://tnl_runtime:...@pool.example/tnl?sslmode=require
TNLD_DATABASE_DIRECT_URL=postgresql://tnl_migrate:...@database.example/tnl?sslmode=require
TNLD_SERVER_DOMAIN=tnl.example.com
TNLD_MANAGED_DEPLOYMENT_DOMAIN=tunnels.example.com
TNLD_ACME_EMAIL=operator@example.com
TNLD_ACME_ACCEPT_TERMS=true
TNLD_LOGIN_TOKEN=tnl_login_...
TNLD_STORAGE_KEY=...

Pin TNL_IMAGE by digest, then start and probe control:

start the standalone server
docker compose pull
docker compose up -d
curl --fail https://control.tnl.example.com/v1/ready

The image runs as a non-root user with a read-only root filesystem. No daemon state volume or certificate mount is required because PostgreSQL owns durable state.

Authenticate with the bootstrap token and publish:

connect a client
tnl login https://control.tnl.example.com --token
tnl publish 3000

run split services

The reference split Compose file separates control, ingress, and two relay services. Give ingress TNLD_INGRESS_ID. Give every relay process a TNLD_RELAY_SERVICE_ID, unique TNLD_RELAY_ID, stable public TNLD_RELAY_ADDRESS, and reachable TNLD_INTERNAL_RELAY_ADDRESS.

Expose public TCP 443 for control and ingress and public TCP and UDP 443 for every relay service. Restrict private TCP 9443 and internal relay addresses. Ingress and relay containers need no PostgreSQL, storage key, hosted authority, administrator, DNS provider, or ACME account credentials.

Each route session maintains two connection slots assigned to distinct relay services. Initial routability requires the route certificate and both publisher connections. Afterward, one ready publisher connection remains routable while the publisher replenishes toward two.

operate safely

Probe /v1/health for HTTP serving and /v1/ready for control readiness. Scrape TNLD_METRICS_LISTEN only over a private network. Alert on control errors, certificate renewal failures, expired ingress or relay leases, insufficient ready publisher connections, capacity rejection, and file descriptor pressure.

Maintenance controls independently gate route creation, route-session creation, and certificate issuance:

pause new route sessions
tnl admin maintenance list
tnl admin maintenance disable route_session_creation

Drain a concrete relay lease before planned removal. Use the exact process run ID and lease revision reported by tnl admin relays list:

drain a relay process
tnl admin relays drain relay-a-1 \
  --relay-run-id relay_run_0123456789abcdef \
  --relay-lease-revision 42 \
  --deadline 30s

Draining rejects new work and removes the process from selection while admitted visitor streams finish until the deadline. Established streams are never migrated or replayed.

Back up PostgreSQL and TNLD_STORAGE_KEY independently, test restoration, and run migration before rolling control, ingress, and relay services in that order. Production images must remain digest-pinned.

Use the repository's self-hosting runbook, observability guide, and release verification guide for the complete operational contracts.