Lane routing

Relay stores a route for every deploy lane.

You can run Relay in direct host-port mode or let Relay manage host routing through its built-in Caddy global proxy. The dashboard and CLI still use the preview_url field stored on the deploy record, even when the lane is production, staging, or dev, so it helps to understand how routes, signed access, expiry, and promotion connect.

Three common shapes
Port mode:   http://127.0.0.1:3005
Preview:     https://demo-preview-f4a91c2d.relay.example.com
Dev host:    https://demo-dev-a1b2c3d4.relay.example.com
Stage host:  https://demo-staging.relay.example.com
Port mode

The simplest setup maps a host port directly to the app container.

This is the easiest mode to start with locally. The agent launches the app container with a host port mapping and stores a route like http://127.0.0.1:<port>. Any lane can use port mode when you want direct ownership of the host port.

CLI deploy
relay deploy --host-port 3005 --stream
Managed hosts

If RELAY_BASE_DOMAIN is set, the agent can derive lane hostnames automatically.

In host mode, Relay runs a per-app edge proxy for the app slot and a global Caddy proxy for public domains. staging gets a stable <app>-staging.<base-domain> host, while dev and preview can both generate random aliases that stay attached to that lane until you change the public host explicitly.

Set RELAY_BASE_DOMAIN=relay.example.com on the agent.

Optional but recommended: set RELAY_DASHBOARD_HOST=admin.relay.example.com so the Relay dashboard has its own hostname.

Make sure DNS points both the dashboard host and app hosts at the same server IP.

Relay stores the derived public host on app state and keeps the resulting route on the deploy record.

Derived hosts
RELAY_BASE_DOMAIN=relay.example.com

preview: demo-preview-f4a91c2d.relay.example.com
staging: demo-staging.relay.example.com
dev: demo-dev-a1b2c3d4.relay.example.com
Lane lifecycle

Routing now carries access, expiry, and promotion behavior too.

A lane route is no longer just a hostname. dev and preview can auto-expire after the retention window, signed-link lanes can mint temporary share URLs, and staging can promote the exact staged image into production while keeping rollback attached to the target lane.

dev and preview expiry

These lanes refresh their expiry when you deploy, start, or restart them. Once the retention window passes, Relay stops the lane and records the event in audit.

Signed links

If access_policy is signed-link, the dashboard can mint a temporary share URL that appends relay_exp and relay_sig before the request reaches the app.

Promotion path

Promotion moves the staged image into the target lane rather than rebuilding from scratch, which keeps rollback tied to the exact image that was validated earlier.

Approval gate

When user accounts are enabled, non-owner requests to promote staging into prod pause for owner approval before the production deploy is queued.

Lifecycle examples
preview alias: https://demo-preview-f4a91c2d.relay.example.com
signed link:   https://demo-preview-f4a91c2d.relay.example.com?relay_exp=...&relay_sig=...
expiry worker: lane.expire env=preview branch=main
promotion:     staging/main -> prod/main
Slot routing

Host mode uses blue-green slots behind the edge proxy.

The agent alternates between blue and green containers, points the edge proxy at the newly ready slot, and keeps the previous slot alive for a short drain window. traffic_mode controls how the proxy treats clients during that window, and access_policy controls who is allowed through at all.

active_slot

Stored on app state so the agent knows which slot is currently live.

traffic_mode=edge

All requests move to the new active slot as soon as the proxy is reloaded.

traffic_mode=session

The edge proxy sets a relay_slot cookie and can keep a browser pinned to a specific slot for the duration of the standby window.

access_policy

public, relay-login, signed-link, or ip-allowlist are enforced before the request reaches the current slot.

Signed-link UX

signed-link lanes can mint a temporary share URL from the dashboard instead of asking an operator to handcraft relay_exp and relay_sig values.

Drain behavior

The old slot is removed only after RELAY_ROLLOUT_DRAIN_SECONDS elapses.

App config for slot-aware routing
{
  "app": "demo",
  "env": "dev",
  "branch": "main",
  "mode": "traefik",
  "traffic_mode": "session",
  "public_host": "demo-dev-a1b2c3d4.relay.example.com",
  "access_policy": "relay-login",
  "service_port": 3000
}
Host ownership

Keep the dashboard hostname separate from app hostnames.

Relay itself serves the dashboard root on its own host. If you point an app at the same hostname, requests can resolve to the dashboard instead of the app. Give the dashboard and each app separate hosts so session cookies and route ownership stay legible.

Dashboard host

Use a dedicated admin host such as admin.relay.example.com.

App host

Use a different host or let Relay derive lane hosts such as <app>-preview-<token>.<base-domain>, <app>-staging.<base-domain>, or <app>-dev-<token>.<base-domain>.

Old front proxy

If nginx is still bound to :80 or :443, it will intercept requests before Relay's Caddy proxy can route them. Stop and disable it.

ACME bootstrap

Relay binds :80 on startup and serves ACME HTTP-01 challenge tokens directly. Set RELAY_ACME_EMAIL so Let's Encrypt can issue and renew certs automatically.

Saved guard

The admin rejects saving a public_host that matches the configured dashboard host.

Recommended split
Dashboard:  https://admin.relay.example.com
Preview:    https://site-preview-f4a91c2d.relay.example.com
Staging:    https://site-staging.relay.example.com
Dev:        https://site-dev-a1b2c3d4.relay.example.com
Verification

Verify DNS and host routing with curl before opening a browser.

The quickest way to separate DNS problems from proxy problems is to resolve the host manually and inspect the response headers. Test the dashboard host and the app host independently, then confirm whether the edge policy is returning the expected 401 or 403.

Step 1

Confirm both names resolve to the server IP with nslookup or dig.

Step 2

Use curl --resolve against the dashboard host and the app host before relying on browser cache or DNS propagation.

Step 3

If the response still says Server: nginx, your old front proxy is still in the path.

Step 4

If the app host returns 401 or 403, the edge policy is active. signed-link lanes should return 401 until relay_exp and relay_sig are present. If it redirects to /dashboard/, the app host still collides with the dashboard host or an old proxy rule.

Curl checks
curl -I --resolve admin.relay.example.com:443:185.27.135.235 https://admin.relay.example.com
curl -I --resolve site-staging.relay.example.com:443:185.27.135.235 https://site-staging.relay.example.com
What gets recorded

Lane URLs are part of the deploy history, not a front-end guess.

The dashboard shows a Visit link because the server stores a preview_url on each deploy once rollout completes. Deploy history also carries the routing shape that was live at the time through app state fields like mode, public_host, host_port, active_slot, traffic_mode, and access_policy.

Deploy record

Stores the preview_url for the specific deployment.

App state

Stores host mode, host port, service port, public host, active slot, traffic mode, access policy, and expiry data for the lane.

CLI output

Prints the stored route when the deploy reaches ready state.

Dashboard cards

Render the route as a clickable link when present and expose signed-link generation or promotion controls when the lane policy allows it.

Ready output
lane URL: https://site-staging.relay.example.com
deploy success. image=relay/site-demo:staging-main-...