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.

Public hostname: Pass --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.
Best Practice: Use the traffic inspector in the dashboard to capture and debug requests in real time.
Local protocol: The agent ↔ local service hop is HTTP/1.1 by default. Set tunnel.local_http_version to http2 (h2c) or http3 (QUIC, experimental — local target must serve HTTPS) for backends that require it.
Forward HTTP(S)
# 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.

Public hostname: Pass --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.
Fixed ports: If you need the public port pinned (e.g. firewall allow-list), upgrade to a plan with the dedicated_ports entitlement and reserve one from the dashboard.
Preserve the client IP: Add --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.
Warning: For TCP services, enable access controls (see Security) and avoid exposing production databases. Never expose privileged ports without strong authentication.
Expose raw TCP
# 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.

Public hostname: Pass --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.
When to use: Forward DNS (port 53), expose a WireGuard server (51820), tunnel game-server traffic, or experiment with a QUIC backend. Sessions are tracked by client tuple with an idle timeout.
Warning: UDP is unauthenticated by default. Pair with access controls for anything sensitive — especially anything resembling open DNS resolvers, which attract reflection attacks.
Expose UDP services
# 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.

Verify HTTP/3: Check the response with curl -sI --http3 https://your-app.ngris.com (curl built with HTTP/3 support) or inspect alt-svc in DevTools.
Confirm HTTP/3 negotiation
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 503 with Retry-After: 1 until 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.
Before or after the response starts. The edge sends these statuses only while the response hasn't started. Once your service has started answering, a failed upload cuts the response off instead.
Slow clients & large uploads
# 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.

  1. Add your domain in the dashboard (e.g., dev.example.com).
  2. Create a DNS CNAME from dev.example.com → cname.ngris.io (the exact target is shown in your dashboard).
  3. Start the endpoint and attach the domain.
Certificates: When you connect the domain with DNS-provider credentials (auto mode), Ngris provisions & renews TLS automatically via ACME (Let's Encrypt, DNS-01). If you add the CNAME yourself (manual mode), you upload your own certificate. See Custom domains.
Wildcard certificates: an auto-mode domain gets one certificate for the domain and its wildcard — add staging.example.com and it also covers *.staging.example.com. Custom domains are on every current plan.
Attach a custom domain
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.

No server address: The tunnel-server endpoint is not set in config — it is resolved at runtime via the discovery service baked into the binary. Use observability.region to express a region preference (e.g. us-va, eu-fi).
Config Keys: WAF rules and the protections set on an endpoint in the dashboard are configured in the dashboard or API. The agent's own edge settings — 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.
Environment Variables: The supported environment variables are the ones in the Environment variables block — for example 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.
Multi-tunnel mode: When tunnels: is non-empty it replaces the single tunnel: block above. The TUI is disabled in this mode.
config.yaml — minimal
# 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"
config.yaml — full reference
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"
Environment variables
# 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 from config
# 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
Was this helpful?