Portal Relay Deployment Guide
Portal production deployment uses one image and one public HTTPS origin. The Go binary owns TLS, SNI routing, relay APIs, tunnel ingress, and the embedded React SPA. Operators can replace only the SPA files without adding a reverse proxy or changing ownership of the Portal API paths.
Production Topology
Browser or tunnel client
-> https://portal.example.com or https://*.portal.example.com
-> Portal :443 SNI router
-> root host: internal API listener
-> /api/*, /sdk/*, /discovery*, /v1/sign
-> embedded React SPA for /, /admin, and client routes
-> registered wildcard host: tunnel ingress The internal API listener defaults to 4017/tcp and is not published by the
bundled Compose deployment. Root-host traffic reaches it through Portal’s own
SNI router.
Prerequisites
- A public Linux server with Docker and Docker Compose.
- A public hostname such as
portal.example.com. - DNS
Arecords forportal.example.comand*.portal.example.com. - Inbound
443/tcpand51820/udpwhen the overlay is enabled. - A certificate covering the root and wildcard names, or a configured Portal DNS provider that can issue it.
Configuration
Create .env next to docker-compose.yml:
PORTAL_URL=https://portal.example.com
ADMIN_TOKEN=replace-with-a-long-random-value
LANDING_PAGE_ENABLED=false
DISCOVERY=false
BOOTSTRAPS=
ACME_DNS_PROVIDER=cloudflare
CLOUDFLARE_TOKEN=replace-with-an-api-token LANDING_PAGE_ENABLED supplies the initial value. Changes made from the admin
dashboard are stored in IDENTITY_PATH/policy.json and survive restarts.
Custom Community Frontend
Portal serves the official embedded SPA when PORTAL_FRONTEND_DIR is empty.
To use a React, Vue, Svelte, Astro, or other static SPA, build it with client
routes falling back to index.html, then mount its output directory read-only:
services:
portal:
environment:
PORTAL_FRONTEND_DIR: /srv/portal/frontend
volumes:
- ./community-frontend/dist:/srv/portal/frontend:ro The mounted directory must contain a regular index.html; Portal fails at
startup with a configuration error when it does not. Custom assets own / and
client-side routes, while /api, /sdk, /discovery, and /v1 remain reserved
Portal paths. Community frontends can therefore use the stable JSON APIs
without owning TLS routing or running nginx.
Frameworks that require a live SSR server cannot be mounted as static files. Run those applications separately and call the Portal API over HTTPS; the API allows cross-origin requests. Static-export modes can use the mount directly.
When ACME_DNS_PROVIDER is empty, place these files in ./.portal-certs:
fullchain.pem
privatekey.pem The certificate must cover the Portal root hostname. A wildcard certificate is also required when wildcard tunnel names terminate TLS at Portal.
Deploy
mkdir -p ./.portal-certs
docker compose pull portal
docker compose up -d --force-recreate --remove-orphans portal For a local source build:
docker compose up -d --build --force-recreate --remove-orphans portal The Compose stack publishes:
| Port | Purpose |
|---|---|
443/tcp | Portal HTTPS, SPA, APIs, and SNI tunnel ingress |
51820/udp | Relay discovery overlay |
| configured lease range | Optional UDP and raw TCP leases |
Port 80/tcp is not required. Operators who need HTTP-to-HTTPS redirects may
add a small external redirect service, but it must not terminate wildcard
tunnel TLS.
Verify
curl -fsS https://portal.example.com/api/healthz
curl -fsS https://portal.example.com/sdk/domain
curl -I https://portal.example.com/
curl -I https://portal.example.com/admin
docker compose ps Expected runtime services:
portal The /admin request must return the SPA entry rather than 404. Registered
subdomains must continue to reach their tunnel targets through the same public
443 listener.
Automated Updates
Production deployments should follow the v2 release track:
ghcr.io/gosuda/portal:2 The bundled watcher tracks that image and recreates only the Portal service:
cp <repo>/docs/static/examples/auto-update/watch_and_deploy.sh ./watch_and_deploy.sh
chmod +x watch_and_deploy.sh
./watch_and_deploy.sh Migration From the Split Stack
- Confirm
./.portal-certscontains the Portal certificate and state. - Pull or build the new single Portal image.
- Stop the old stack so it releases public port 443.
- Start
portalwith--remove-orphansto remove the old edge and frontend containers. - Verify the SPA, relay APIs, and at least one wildcard tunnel.
The old edge configuration and its separate browser certificate are no longer used. Portal owns the public certificate and performs only one TLS handshake for root-host requests.
Troubleshooting
Port 443 Is Already Allocated
Stop the previous edge container or host service before starting Portal:
docker compose down --remove-orphans
docker compose up -d portal SPA Routes Return 404
When PORTAL_FRONTEND_DIR is empty, use an image built after the embedded
frontend migration. When it is set, confirm the mounted directory contains index.html and all asset paths expected by that file. /admin and other
non-reserved client routes fall back to the selected SPA’s index.html.
Certificate Errors
Confirm PORTAL_URL matches the certificate root hostname and inspect the
certificate files under IDENTITY_PATH. With managed ACME, verify the DNS API
token has permission to update the selected zone.
API Port 4017
Do not publish or browse directly to 4017 in the bundled deployment. It is the internal TLS API listener used by Portal’s root-host SNI route.