Networking
Temps runs a built-in Pingora reverse proxy that handles TLS termination, routing, and load balancing for your deployments. This page covers container ports, internal networking, host firewalls, and the routing surfaces available for managed and unmanaged services. For custom domains and certificates, see Domains and SSL.
Ports
Each deployed container has one primary port, which is where the proxy sends traffic. Temps picks it in this order:
- The environment's port override.
- The project's default port.
- The first port your image declares with
EXPOSE. 3000.
Set the port under Project → Settings → Build & deploy → Deployment: Default Port applies to every environment, and Environment Port Overrides sets a different port for one environment.
When you create a project from a Git repository, Temps pre-fills the port from the detected framework — for example 3000 for Next.js, 5173 for Vite, 8000 for FastAPI or Django, 5000 for Flask, and 8080 for Go. That value is saved as the project's default port.
Temps also sets the PORT environment variable in every container to the configured port, or 3000 if none is configured. It overrides any PORT you set yourself, so changing PORT in your environment variables does not change the port.
Your app must bind to 0.0.0.0, not 127.0.0.1, or the proxy cannot reach it. Temps sets HOST=0.0.0.0 by default for frameworks that read it.
Internal networking
On a Temps server, your apps and managed services (Postgres, Redis, and so on) are attached to the same private Docker network, temps-app-network, so they can talk to each other without exposing any ports to the internet.
Connecting to managed services
Don't hand-write hostnames for managed services. When you link a service to a project, Temps injects its connection details into the project's environment variables — for example POSTGRES_URL, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER and POSTGRES_PASSWORD for PostgreSQL, and REDIS_URL for Redis. The hostname in those variables is the service's container name on the private network (for example postgres-<service-name>), and the database is scoped to the project and environment.
const pool = new Pool({ connectionString: process.env.POSTGRES_URL });
See Managed PostgreSQL and Managed Redis for the full list of variables.
Connecting to another app
App container names include the deployment number, so they change on every deploy — don't hardcode them. To call another app you deploy on Temps, use its domain.
On multi-node clusters with Cluster DNS enabled (see below), apps also get an internal name, <environment>.<project>.temps.local, served over plain HTTP on port 80 by an internal proxy on each worker node:
http://production.api.temps.local
These records are only published for worker nodes, so this name does not exist on a single-server install.
Nodes in the same Temps cluster share a private network (a WireGuard mesh with an overlay network on top), so cross-node traffic doesn't need the public internet. See Multi-node. Separate Temps installs are not connected to each other; use public domains or your own VPN between them.
Internal DNS resolver
Cluster DNS is experimental and off by default. Turn it on from the Worker Nodes page, in the Cluster DNS card (Enable cluster DNS). It is a cluster-wide setting: incorrect DNS configuration can break service discovery, so check node and application health before and after changing it.
When Cluster DNS is on, Temps runs a Hickory-based DNS resolver and injects it as the first nameserver into every deployed container. That makes *.temps.local names — such as the ones that back managed services and HA database clusters — resolve from inside your containers. When it is off, containers use their normal DNS and *.temps.local names don't resolve.
The resolver decides how to answer each query by where the name falls:
| Query | How it is answered |
|---|---|
In the temps.local zone (temps.local or any *.temps.local, matched case-insensitively, trailing dot optional) | Answered authoritatively from the node's synced zone snapshot. |
| In-zone name that does not exist | NXDOMAIN. |
| In-zone name that exists but not for the requested record type | NODATA (NoError with an empty answer) — so IPv4-only names don't break AAAA lookups in glibc/busybox. |
Outside temps.local | Recursively forwarded to the upstream public resolvers (see below). |
Recursive forwarding for public domains
Because the internal resolver is the first nameserver your containers see, queries for public domains (package mirrors, third-party APIs, and so on) are recursively forwarded to a pool of upstream public resolvers. Without this, every outbound lookup like apt-get, wget, or an external API call would get NXDOMAIN from it. The host's own DNS servers are added after it as fallbacks (up to three nameservers in total), so outbound lookups keep working if the internal resolver stops responding.
The default upstream pool is hardcoded:
| Upstream | Address | Port |
|---|---|---|
| Cloudflare | 1.1.1.1 | 53 |
| Cloudflare | 1.0.0.1 | 53 |
8.8.8.8 | 53 |
Forwarded answers come back as non-authoritative with recursion-available set. An upstream NXDOMAIN/NODATA is passed through as a negative answer; a transport failure (timeout, unreachable upstream) is returned as SERVFAIL so the client can retry.
If the upstream list is configured empty, the forwarder is disabled — the resolver logs DNS recursive forwarder disabled (empty upstream list) instead of DNS recursive forwarder enabled, and out-of-zone queries fall through to NXDOMAIN (strict authoritative-only behavior).
The upstream resolvers are currently hardcoded in the resolver's defaults. There is no env var, CLI flag, or database setting to substitute a private or corporate (split-horizon) upstream resolver yet.
Firewall rules
Temps does not configure a host firewall. Manage one yourself (for example with ufw or your cloud provider's firewall) and only open what you need:
- The proxy's HTTP and HTTPS ports — usually 80 and 443, set with
temps serve --addressand--tls-address. - SSH (22), for server access.
By default, HTTP requests are redirected to HTTPS when the domain has a certificate, or when the environment forces HTTPS.
On the Temps server, the ports of app containers and managed services are published on 127.0.0.1 only (worker nodes use their private address instead), so they aren't reachable from the internet even without a firewall. To connect to a managed database from your laptop, use an SSH tunnel to the service's host port — 5432 for the first PostgreSQL service, or the next free port if that one was taken (docker ps on the server shows it):
ssh -L 5432:127.0.0.1:5432 user@your-server-ip
# Then connect to localhost:5432 locally
Reverse proxy configuration
The built-in Pingora proxy loads routes for Temps-managed projects automatically. You do not maintain a Caddyfile or Nginx configuration for deployed applications.
To publish a service that Temps does not deploy, create a first-class reverse proxy route. Routes support HTTP TLS termination and TLS SNI passthrough, validate upstream destinations, and protect managed hostnames from accidental overrides. See Reverse Proxy Routes for the command reference and safety rules.
WebSockets
WebSocket connections are proxied automatically. Ensure your application handles the Upgrade and Connection headers.
gRPC
gRPC is not supported through the proxy today. Clients can use HTTP/2 to reach the proxy over HTTPS, but the proxy talks to your app over HTTP/1.1, and gRPC needs HTTP/2 end to end.
Request timeouts
By default, the proxy applies no timeout to your app's traffic at all. Regular HTTP requests, Server-Sent Events streams, and WebSocket connections stay open indefinitely (bounded only by TCP/OS-level limits) unless you or your operator explicitly configure one. This is opt-in by design — upgrading Temps, or deploying an app with a slow endpoint or a long-lived connection, never breaks something that worked before.
Global defaults
An operator can set platform-wide defaults under Settings → Request Timeouts:
| Setting | Default | Applies to |
|---|---|---|
| Hard ceiling | 600s (10m) | Caps any resolved timeout once one is configured — never creates a timeout on its own |
| Regular HTTP | 0 (no timeout) | Non-streaming requests |
| SSE idle | 0 (no timeout) | Server-Sent Events streams (idle time only) |
| WebSocket idle | 0 (no timeout) | WebSocket connections (idle time only) |
Per-project and per-environment overrides
Under Project → Environments → Environment settings → Request Timeouts, you can override any of the three values for a specific environment:
- Leave a field blank to inherit the project default, then the operator's global default.
- Enter
0to explicitly force "no timeout" for that environment, even if an operator has configured a nonzero global default. - Enter any other value to set an explicit timeout — always clamped to the operator's hard ceiling.
Or from the CLI:
bunx @temps-sdk/cli env timeouts <environment> --request 30 --sse-idle 900
bunx @temps-sdk/cli env timeouts <environment> --request 0 # force no timeout
bunx @temps-sdk/cli env timeouts <environment> --inherit # clear all overrides
A timeout you set only ever tightens behavior for your app — it never gets a timeout imposed on it that you (or your operator) didn't ask for.
Load balancing and health checks
When you scale a project to multiple replicas, the proxy spreads requests across the running containers with round-robin. If it can't connect to a container, it retries the request once.
Deploy-time health check
Before a new deployment receives traffic, Temps checks that it responds over HTTP. By default it requests /; to use another path, set it in .temps.yaml:
health:
path: /health
For projects deployed from a service template, set Health-check path under Project → Settings → Build & deploy → Build instead.
- Any
2xxor3xxresponse counts as healthy, and so do404and405(the app is up, it just doesn't serve that path). Other4xxand all5xxresponses count as errors. - The deployment is ready after two successful checks in a row.
- It fails if the app keeps returning error responses for 60 seconds, or isn't ready within 5 minutes. Connection errors while the app is still starting don't count toward the 60 seconds.
Only health.path is used. The other health fields in .temps.yaml (interval, timeout, retries and status) are accepted but currently have no effect.
The health check runs at deploy time only. Temps doesn't probe replicas over HTTP after that, so a replica that keeps running but stops responding stays in rotation.
Private networking (Tailscale)
To reach your Temps server from your laptop without opening more ports to the internet, you can put it on a private network such as Tailscale:
# On the Temps server
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
You can then SSH to the server over its Tailscale IP (100.x.x.x) and close SSH to the public internet. Managed databases still listen on 127.0.0.1 only, so reach them with the SSH tunnel above, run over Tailscale.
To connect several servers into one Temps cluster, you don't need Tailscale: see Multi-node.