Server reference

The server is the part of tunnel-me that runs on a public machine — a VPS, a small cloud instance, or any always-on host with a reachable IP address. It owns your domain: visitors open name.your-domain.com, and the server forwards that request through the tunnel to the machine where your service actually runs.

The server is one command:

tunnel-me serve

(run does the same thing.)

Command line and environment

Every setting is a command-line flag and every flag has a matching environment variable with the TUNNEL_ prefix. Environment variables are convenient in Docker and systemd. For example, --domain can also be set as TUNNEL_DOMAIN.

Flag Environment variable Default What it does
--bind TUNNEL_BIND :8080 Address and port to listen on
--domain TUNNEL_DOMAIN localhost Your base domain. Requests to this domain (or to anything that is not one of its subdomains) show the admin panel
--public-url TUNNEL_PUBLIC_URL http://localhost:8080 The address visitors actually use. It appears in the connect commands shown in the panel and in sign-in redirects, so it must be reachable from the outside
--db TUNNEL_DB tunnel.db Path to the database file. Created on first start
--tls.enabled TUNNEL_TLS_ENABLED off Let the server handle HTTPS itself
--tls.cert TUNNEL_TLS_CERT /etc/tls/tls.crt TLS certificate file
--tls.key TUNNEL_TLS_KEY /etc/tls/tls.key TLS private key file
--binaries-dir TUNNEL_BINARIES_DIR /opt/tunnel-me/binaries Where the server finds client binaries for the one-line installer
--stats-interval TUNNEL_STATS_INTERVAL 10s How often live connections are measured for the charts
--stats-buffer TUNNEL_STATS_BUFFER 8192 Queue size for measurements before they are written to the database
--auth TUNNEL_AUTH static Who can open the admin panel. Values: basic, file, static, oidc, webauth — see Authentication
--graceful-timeout GRACEFUL_TIMEOUT 3s How long running requests may finish when the server shuts down
--cors TUNNEL_CORS off Allow browser scripts from any website to call the API. Dangerous — only useful for development

The sub-flags of --auth.* are listed in Authentication.

How the server routes requests

One port answers everything. Routing is decided by the hostname in the request:

Visitor opens What they get
tunnels.example.com (the base domain) or any name that is not a subdomain of it The admin panel, plus the HTTP API at /api/v1
myapp.tunnels.example.com (a subdomain) The tunnel whose sub-domain is myapp
/boot/... The one-line installer and client binaries
/agent The connection endpoint agents use (you never open this in a browser)

This is why the wildcard DNS record and (if you terminate TLS yourself) a wildcard certificate are needed — every tunnel is just another subdomain. See Getting started for the DNS setup and Reverse proxy for HTTPS.

TLS

You have two options for HTTPS:

If TLS terminates in front of the server and you use OIDC sign-in, also pass --auth.oidc.trust-proxy so sign-ins work with the proxy in the middle.

The database

Everything you configure — tunnels, tokens, users, groups, providers, statistics — lives in a single SQLite database file (--db, default tunnel.db in the working directory). It is created on first start and upgraded automatically when you run a newer version.

To back up, stop the server (or use any SQLite-aware backup tool) and copy the file. To move the server, copy the file.

Usage statistics

While a connection is live, the server measures its ping and traffic every --stats-interval (default 10 seconds). The panel's charts show speed (in/out), ping, and the number of connected agents over the last hour, 24 hours, 7 days, or 30 days.

Charts always group measurements in one-minute-or-wider buckets, so keep --stats-interval at one minute or below — sampling less often than that adds nothing. Old measurements are cleaned up automatically: a tunnel keeps its last month, or its 1000 newest samples, whichever holds.

One-line installer support

The installer script that the panel offers (Agents → your tunnel → Overview) downloads the client binary from the server itself:

Supported platforms for the installer: Linux and macOS on x86_64 and arm64. The official Docker image already ships with client binaries for all of them, so the installer works out of the box there.

Protecting the admin panel

The default (--auth static) performs no credential check at all. Out of the box anyone who reaches the server can open the panel. Before exposing the port, pick a real method — Authentication walks through all five.

Quick summary:

--auth value Who gets in
static (default) Everyone — no check performed
basic Whoever knows the username/password from --auth.basic.* (browser login prompt)
file Users from an htpasswd file (--auth.file.filename)
oidc Anyone your identity provider signs in (--auth.oidc.*)
webauth Requests carrying the header named by --auth.web.header — set by an authenticating proxy in front

Protecting the panel never protects tunnels — each tunnel has its own sign-in setting, configured in the panel.

Running with Docker

The official image contains both the server and the client binaries:

docker run -d --name tunnel-me \
  -p 8080:8080 \
  -v ./data:/data \
  --restart unless-stopped \
  ghcr.io/reddec/tunnel-me serve \
    --domain tunnels.example.com \
    --public-url https://tunnels.example.com \
    --db /data/tunnel.db \
    --auth basic \
    --auth.basic.username me \
    --auth.basic.password change-me-now

The same thing as a compose file:

services:
  tunnel-me:
    image: ghcr.io/reddec/tunnel-me
    command: >-
      serve
      --domain tunnels.example.com
      --public-url https://tunnels.example.com
      --db /data/tunnel.db
      --auth basic
      --auth.basic.username me
      --auth.basic.password change-me-now
    ports:
      - "8080:8080"
    volumes:
      - ./data:/data
    restart: unless-stopped

The database lives in the mounted ./data directory — back that up.