Define your services, routing, auth, and environment presets in one file. Check it into git. Run stunl up.
stunl.yaml is a project-level configuration file that declares your tunnel topology — which services to expose, how to route traffic, and who can access them.
Instead of passing flags every time, you define it once and every teammate gets the same setup. Think of it as your compose file for tunnels.
$ stunl -id myapp \
-ports 'web:3000,api:8080,db:5432:tcp' \
-routing-strategy path \
-password secret \
-oauth github \
-oauth-github-org mycompany
$ stunl up
Loading stunl.yaml...
Starting 3 services...
web → localhost:3000 (http)
api → localhost:8080 (http)
db → localhost:5432 (tcp)
stunl auto-detects your running services and generates a config file.
$ stunl init
Detecting running services...
Created stunl.yaml
Detected 3 services:
web → port 3000 (http)
api → port 8080 (http)
postgres → port 5432 (tcp)
Tweak names, add auth, define environments.
$ stunl up
Now everyone on your team runs stunl up and gets the exact same tunnel topology.
Here's a complete stunl.yaml showing all available options.
# Schema version (required, must be "1")
version: "1"
# Tunnel subdomain (optional, auto-generated if omitted)
name: my-project
# Custom domain (optional, requires Pro+ tier)
# domain: mycompany.com
# Routing strategy for multi-service configs
# Options: path, subdomain, header, mixed, round_robin, least_connections
routing: path
# Services to expose (at least one required)
services:
frontend:
port: 3000
# protocol: http (default)
# host: localhost (default)
api:
port: 8080
path: /api # URL path prefix for routing
websocket:
port: 8081
protocol: websocket
path: /ws
postgres:
port: 5432
protocol: tcp
public_port: 15432 # Preferred public port (10001-19999)
# Access control (optional)
auth:
password: "secret123"
# OR use OAuth (mutually exclusive with password):
# oauth:
# provider: github
# allowed_domains: [mycompany.com]
# allowed_emails: [alice@example.com]
# github_orgs: [my-org]
# github_teams: [my-org/engineering]
# Environment presets (optional)
environments:
dev:
description: "Full development stack"
# All services included by default
demo:
description: "Frontend + API for demos"
services: [frontend, api]
name: my-project-demo
auth:
oauth:
provider: github
github_orgs: [my-org]
webhook:
description: "API only for webhook testing"
services: [api]
name: my-project-webhooks
Each service maps a name to a local port. At least one service is required.
| Field | Required | Default | Description |
|---|---|---|---|
port |
Yes | — | Local port number (1–65535) |
protocol |
No | http |
http, tcp, udp, or websocket |
host |
No | localhost |
Local hostname (single-service configs only) |
path |
No | — | URL path prefix for routing (e.g., /api) |
public_port |
No | — | Preferred public port for TCP/UDP (10001–19999) |
Single-service shortcut
When you define only one service, stunl skips multi-port overhead and creates a simple tunnel — exactly like running stunl -port 3000.
services:
web:
port: 3000 # protocol defaults to http
realtime:
port: 8081
protocol: websocket # dedicated WebSocket service
services:
postgres:
port: 5432
protocol: tcp
public_port: 15432 # reserved port for consistent access
redis:
port: 6379
protocol: tcp
# Then connect from anywhere:
# psql -h my-project.stunl.io -p 15432 -U postgres
services:
minecraft:
port: 25565
protocol: udp
public_port: 10042 # friends always connect to this port
Restrict who can access your tunnel. Password auth and OAuth are mutually exclusive.
auth:
password: "my-secret-password"
auth:
oauth:
provider: github # github, google, or microsoft
github_orgs: [my-org] # restrict to org members
github_teams: [my-org/eng] # or specific teams
auth:
oauth:
provider: google
allowed_domains: [mycompany.com]
Define named presets that filter services and override settings. Run different slices of your stack for different purposes.
environments:
dev:
description: "Full stack for development"
# All services included when services list is omitted
demo:
description: "Frontend + API for client demos"
services: [frontend, api] # only these services
name: my-project-demo # different subdomain
auth:
oauth:
provider: github
github_orgs: [my-org]
webhook:
description: "API for webhook testing"
services: [api] # just the API
name: my-project-webhooks
# List available environments
$ stunl up --list-envs
demo Frontend + API for client demos
dev Full stack for development
webhook API for webhook testing
# Start a specific environment
$ stunl up --env demo
Starting 2 services (env: demo)...
frontend → localhost:3000 (http)
api → localhost:8080 (http)
| Override Field | Behavior |
|---|---|
services |
Filter to only these services. Omit to include all. |
name |
Override the tunnel subdomain. |
domain |
Override the custom domain. |
routing |
Override the routing strategy. |
auth |
Override auth. Different presets can have different access controls. |
Controls how traffic is distributed across your HTTP/WebSocket services.
| Strategy | Description |
|---|---|
path |
Route by URL path prefix (default). /api → API service, / → frontend. |
subdomain |
Route by subdomain. api.myapp.stunl.io → API service. |
header |
Route by custom HTTP header value. |
mixed |
Combination of path, subdomain, and header routing. |
round_robin |
Distribute requests evenly across services. |
least_connections |
Route to the service with fewest active connections. |
$ stunl up # auto-discover stunl.yaml, start all services
$ stunl up --env demo # start with environment preset
$ stunl up --file ./other.yaml # use a specific config file
$ stunl up --headless # run without the TUI
$ stunl up --list-envs # list available environments
$ stunl init # detect services, write stunl.yaml
$ stunl init --output .stunl.yaml # custom output path
$ stunl init --force # overwrite existing file
stunl up walks up from the current directory looking for a config file, similar to how git finds .git. The following filenames are recognized:
stunl.yaml
.stunl.yaml (hidden file)
stunl.yml
.stunl.yml
Config hierarchy
stunl.yaml defines tunnel topology (services, routing, auth) and goes into git.
Your personal settings (API key, server address, TLS) live in ~/.stunl/config.yaml and stay local.
When you run stunl up, both are merged — connection settings from your personal config, tunnel topology from the project config.
version: "1"
name: acme-app
routing: path
services:
web:
port: 3000
api:
port: 8080
path: /api
db:
port: 5432
protocol: tcp
auth:
password: "dev-secret"
version: "1"
name: ml-dashboard
services:
dashboard:
port: 8501 # Streamlit default port
api:
port: 8000 # FastAPI default port
path: /api
environments:
share:
description: "Dashboard only for stakeholders"
services: [dashboard]
auth:
oauth:
provider: google
allowed_domains: [mycompany.com]
version: "1"
name: my-api
services:
api:
port: 8080
# Even for single services, stunl.yaml is useful:
# - Consistent tunnel name across the team
# - Auth settings checked into version control
# - No need to remember CLI flags
stunl validates your config on load and gives clear errors for any problems.
version must be "1"
http, tcp, udp, or websocket
public_port only on TCP/UDP services (range 10001–19999)
/
github, google, or microsoft
: @ or ,