Endpoints & Tunnels
HTTP / HTTPS
Forward public HTTPS to a local HTTP or HTTPS service. The public edge terminates TLS and serves your traffic over HTTP/1.1, HTTP/2, and HTTP/3 (QUIC) — clients pick whichever they support.
New to this? The guide on how to expose localhost to the internet walks through getting a public URL for a local port from scratch.
--url <hostname> to attach a reserved subdomain or custom domain. Omit --url and the server provisions an ephemeral endpoint — a randomly generated hostname that lives for the duration of the agent process.
tunnel.local_http_version to http2 (h2c) or http3 (QUIC, experimental — local target must serve HTTPS) for backends that require it.
# Reserved hostname
ngris http 8080 --url blog.ngris.com
# No --url → ephemeral endpoint
# (random hostname, valid for this run)
ngris http 8080
# Local HTTPS (self-signed OK — verified by default,
# set local_service_insecure_tls: true to skip)
ngris http https://localhost:8443 --url secure-app.ngris.com
TCP
Expose raw TCP services like databases or SSH (use with care). The server automatically assigns a free TCP port on the regional ingress (e.g. tcp://eu-fi.ngris.com:17032) and forwards bytes verbatim to your local listener. You don't pick the public port — only the hostname.
--url <hostname> to bind to a reserved hostname (the server still picks the port). Omit --url and the server provisions an ephemeral endpoint — a randomly generated tcp://<random>.ngris.com:<assigned-port> that lives for the duration of the run.
dedicated_ports entitlement and reserve one from the dashboard.
--proxy-protocol v2 (or v1) and the agent prepends a PROXY protocol header to the backend connection, carrying the original client's IP and port — so your database, SSH, or TLS-passthrough backend logs the real client instead of the agent. Your backend must be configured to expect it (e.g. Postgres behind pgbouncer, or proxy_protocol on in nginx/HAProxy); otherwise it reads the header as junk and drops the connection. HTTP tunnels don't need this — the edge already forwards the client IP as X-Forwarded-For.
# Reserved hostname, server-assigned port
ngris tcp 5432 --url mydb.ngris.com
# SSH on a reserved hostname
ngris tcp 22 --url ssh.ngris.com
# No --url → ephemeral
# (random hostname + server-assigned port, valid for this run)
ngris tcp 5432
# Preserve the real client IP to the backend
# (backend must be configured to expect PROXY protocol)
ngris tcp 5432 --url mydb.ngris.com --proxy-protocol v2
UDP
Tunnel datagram services like DNS, WireGuard, or QUIC-based protocols. Like TCP, the server automatically assigns a free UDP port on the regional ingress (udp://<hostname>:<assigned-port>) — you supply only the hostname. Packets are forwarded to your local listener and the response is shipped back to the originating client.
--url <hostname> to bind to a reserved hostname (the server still picks the port). Omit --url and the server provisions an ephemeral endpoint — a randomly generated udp://<random>.ngris.com:<assigned-port> that lives for the duration of the run.
# Reserved hostname, server-assigned port
ngris udp 53 --url dns.ngris.com
# WireGuard server
ngris udp 51820 --url wg.ngris.com
# No --url → ephemeral
# (random hostname + server-assigned port, valid for this run)
ngris udp 5353
# Test it — replace host:port with the assigned public URL
dig @dns.ngris.com -p 17032 example.com
HTTP/3 & QUIC at the edge
HTTP/3 is enabled automatically for every HTTPS endpoint — no flags, no separate command. The public edge advertises HTTP/3 via the alt-svc response header on the first HTTP/1.1 or HTTP/2 request; modern browsers and clients then upgrade to QUIC on subsequent connections.
Real client IPs survive the HTTP/3 hop via PROXY protocol v2 over UDP, so log lines and the traffic inspector see the original client address regardless of the negotiated HTTP version.
curl -sI --http3 https://your-app.ngris.com (curl built with HTTP/3 support) or inspect alt-svc in DevTools.
curl -sI --http3 https://your-app.ngris.com
# Without --http3 you can still see the advertisement:
curl -sI https://your-app.ngris.com | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
Large transfers & timeouts
The edge paces every HTTP response and every request body between the public client and your agent, one request at a time (flow control). A big download to a slow client and a big upload to a slow service both finish, and one slow client never holds up other requests. Every connected agent supports it: older agents are refused.
Downloads to slow clients
- The agent sends a response only as fast as its client takes it. A large download to a slow client completes; before, a response more than 32 MiB ahead of its client was reset.
- When a client falls behind, the agent keeps reading your service at full speed into a spool — memory first, then a temporary file — and sends from it as the client catches up. Your service is done with the response as soon as it has written it. Where the spool lives and how big it can get: CLI Reference → Files on disk.
- When the spool can take no more, the agent stops reading your service for that response until its client catches up. At most 25 responses per agent wait like this, and at most 3 per client IP (for IPv6, per /64); past that the response fails and its client sees the connection reset.
- A response ends only when no bytes reach its client for 60 seconds, or when its client takes less than 64 KiB in 60 seconds while data is waiting for it. A slow stream the client keeps up with, such as server-sent events, is never cut. Before, a response had 60 seconds in total.
- Your service has 60 seconds, after the edge has passed it the whole request, to start its response.
- If an account has a lot of response data waiting at the edge for slow clients, new requests to its endpoints get
503withRetry-After: 1until some of it reaches its clients.
Uploads to slow services
The edge reads a request body from the client only as fast as your agent passes it to your service (upload credit), so a large upload to a slow service completes; meanwhile the client is slowed down by TCP, HTTP/2 or HTTP/3 flow control. The edge answers Expect: 100-continue itself, so clients such as curl start sending the body at once.
| Status | When the edge sends it |
|---|---|
408 Request Timeout |
The client sends its body slower than 64 KiB per minute (time the edge holds the upload back for your service doesn't count), or sends nothing for 60 seconds in the middle of the body. |
400 Bad Request |
The body is shorter than its Content-Length, or breaks off. |
502 Bad Gateway |
The agent gave no upload credit for 2 minutes: your service stopped reading the body. |
# A slow client: the download completes at the client's pace
curl --limit-rate 200k -o big.iso https://your-app.ngris.com/big.iso
# A large upload: curl sends Expect: 100-continue,
# the edge answers it at once and paces the body
curl -T big.iso https://your-app.ngris.com/upload
Custom Domains
Map your domain to an endpoint with automated certificates.
- Add your domain in the dashboard (e.g., dev.example.com).
- Create a DNS CNAME from dev.example.com → cname.ngris.io (the exact target is shown in your dashboard).
- Start the endpoint and attach the domain.
staging.example.com and it also covers *.staging.example.com. Custom domains are on every current plan.
ngris http 3000 --url dev.example.com
Config File
Use a config.yaml for repeatable setups across environments: your personal one (~/.config/ngris/config.yaml on Linux, the file ngris auth saves to), or any file you pass with --config. Older versions also read config.yaml from the working directory. Every key shown below is optional — defaults are baked in; CLI flags and environment variables override the file.
observability.region to express a region preference (e.g. us-va, eu-fi).
basic_auth, mutual_tls / client_ca, traffic_policy_file and the traffic_policy shorthands such as cidr_allow and rate_limit — can also go in this file or on their flags; they need a token, not a key (see Key authentication). See Endpoint authentication from the agent CLI and Configure from the agent CLI.
API_TOKEN (authentication.token), AGENT_REGION (observability.region), AGENT_LOG_LEVEL (logging.level). Set the other settings, such as tunnel.chunk_size, tunnel.local_http_version or the header rules under traffic_policy, in the config file or, where one exists, with a flag. There is no NGRIS_AGENT_ prefix. AGENT_NO_TUI=true turns the terminal UI off.
tunnels: is non-empty it replaces the single tunnel: block above. The TUI is disabled in this mode.
# config.yaml — your personal config, or pass it with --config
authentication:
token: "your-api-token"
observability:
region: "us-va" # region preference for discovery
tunnel:
type: "http" # http | tcp | udp
local_address: "localhost:3000"
url: "myapp.ngris.com" # empty → ephemeral endpoint
name: "My Service Tunnel"
authentication:
token: "your-api-token"
# Key authentication (Preview) — see Getting Started → Key authentication
# method: key # token or key
# private_key_file: ~/.config/ngris/agent_key
logging:
level: "info" # debug | info | warn | error | fatal
file: "./logs/agent.log"
format: "text" # text | json
control_plane_verbose: false
# Agent → tunnel-server TLS (the server certificate is always verified)
tls:
ca_cert_file: "" # custom CA (PEM) to trust for the server, beside the system store
ca_cert_only: false # pin: trust ONLY ca_cert_file
insecure_skip_verify: false # deprecated and ignored: verification can't be turned off
# Allow self-signed certs on a local HTTPS backend
local_service_insecure_tls: false
observability:
region: "us-va"
tunnel:
type: "http" # http | tcp | udp
local_address: "localhost:80" # port, host:port, or file://<dir>
url: "" # empty → ephemeral endpoint
name: "My Service Tunnel"
http_port: 9090 # local stats server port
verbose: false
max_requests_log: 1000
# Timeouts
tcp_dial_timeout: 10s
tcp_idle_timeout: 5m # 0 disables
tcp_keepalive: 30s
control_read_timeout: 60s
local_service_timeout: 60s
# Local protocol + transfer tuning
local_http_version: "http1.1" # http1.1 | http2 (h2c) | http3 (QUIC)
chunk_size: 65536 # 64 KiB
max_concurrent_requests: 100
# Static file serving (when local_address is file://<dir>)
disable_directory_listing: false
index_filename: "index.html"
file_server_write: false # WebFM read-write — no built-in auth
use_webfm_ui: false
# Load balancing across multiple agents on the same tunnel
load_balancing:
weight: 1
max_fails: 0 # 0 disables
fail_timeout: 30s
# Routing identity (server-side routing rules)
agent_id: ""
agent_name: ""
agent_tags: "" # e.g. "api,v2,production"
# Multi-tunnel: when non-empty REPLACES the single `tunnel:` above
tunnels:
- type: "http"
local_address: "localhost:3000"
url: "app.ngris.com"
name: "web"
- type: "tcp"
local_address: "localhost:22"
url: "ssh.ngris.com"
name: "ssh"
- type: "udp"
local_address: "localhost:53"
name: "dns"
# config key environment variable
authentication.token API_TOKEN
observability.region AGENT_REGION
observability.dashboard_url NGRIS_DASHBOARD_URL
logging.level / file / format AGENT_LOG_LEVEL / AGENT_LOG_FILE / AGENT_LOG_FORMAT
logging.control_plane_verbose AGENT_LOG_CONTROL_PLANE_VERBOSE
tunnel.type TUNNEL_TYPE
tunnel.local_address LOCAL_ADDR
tunnel.url URL
tunnel.name TUNNEL_NAME
tunnel.http_port HTTP_PORT
tunnel.verbose VERBOSE
tunnel.max_requests_log MAX_REQUESTS_LOG
tunnel.tcp_dial_timeout TCP_DIAL_TIMEOUT
tunnel.tcp_idle_timeout TCP_IDLE_TIMEOUT
tunnel.tcp_keepalive TCP_KEEPALIVE
tunnel.control_read_timeout CONTROL_READ_TIMEOUT
tunnel.control_write_timeout CONTROL_WRITE_TIMEOUT
tunnel.proxy_protocol AGENT_PROXY_PROTOCOL
tunnel.load_balancing.weight LB_WEIGHT
tunnel.load_balancing.max_fails LB_MAX_FAILS
tunnel.load_balancing.fail_timeout LB_FAIL_TIMEOUT
tunnel.agent_id / agent_name / agent_tags
AGENT_ID / AGENT_NAME / AGENT_TAGS
tls.ca_cert_file AGENT_TLS_CA_CERT_FILE
tls.ca_cert_only AGENT_TLS_CA_CERT_ONLY
tls.insecure_skip_verify AGENT_TLS_SKIP_VERIFY (deprecated, ignored)
local_service_insecure_tls AGENT_LOCAL_SERVICE_INSECURE_TLS
local_service_ca_cert AGENT_LOCAL_SERVICE_CA_CERT
local_service_client_cert AGENT_LOCAL_SERVICE_CLIENT_CERT
local_service_client_key AGENT_LOCAL_SERVICE_CLIENT_KEY
local_service_server_name AGENT_LOCAL_SERVICE_SERVER_NAME
local_service_host AGENT_LOCAL_SERVICE_HOST
traffic_policy_file TRAFFIC_POLICY_FILE
traffic_policy.cidr_allow TRAFFIC_POLICY_CIDR_ALLOW
traffic_policy.cidr_deny TRAFFIC_POLICY_CIDR_DENY
traffic_policy.rate_limit TRAFFIC_POLICY_RATE_LIMIT
traffic_policy.compression TRAFFIC_POLICY_COMPRESSION
traffic_policy.force_https TRAFFIC_POLICY_FORCE_HTTPS
traffic_policy.circuit_breaker TRAFFIC_POLICY_CIRCUIT_BREAKER
traffic_policy.verify_webhook TRAFFIC_POLICY_VERIFY_WEBHOOK
observability.tracing_enabled AGENT_TRACING_ENABLED
observability.tracing_endpoint AGENT_TRACING_ENDPOINT
observability.tracing_insecure AGENT_TRACING_INSECURE
observability.tracing_sample_ratio AGENT_TRACING_SAMPLE_RATIO
# e.g.
API_TOKEN={your-auth-token} AGENT_REGION=us-va ngris http 3000
# Launch all tunnels defined in config.yaml
# The agent loads your personal config.yaml; for a project file pass --config
ngris
ngris --config ./config.yaml
# Or run a single tunnel with CLI flags
ngris http 3000 --url myapp