Self-Host the rustunnel Tunnel Server on Ubuntu (systemd, TLS, PostgreSQL)
Self-host the open-source rustunnel server on a Linux VPS with systemd, Let’s Encrypt TLS, and PostgreSQL. Production-ready walkthrough — drop-in for ngrok or frp on any cloud provider including Hetzner, DigitalOcean, and Linode.
Use this file to discover all available pages before exploring further.
This guide walks through how to self-host the rustunnel tunnel server in production. rustunnel is open-source under AGPL, so you can run the same server we run on the managed cloud — on any VPS with a public IP and a wildcard DNS record. The example commands use Ubuntu 22.04, systemd, Let’s Encrypt TLS via Certbot, and PostgreSQL, but the same recipe works on any major cloud provider including Hetzner, DigitalOcean, Linode, and AWS Lightsail. For a Docker-based deployment, see the Docker Deployment guide instead.
rustunnel requires PostgreSQL for shared state (tokens, tunnel history, audit log).
apt install -y postgresql postgresql-contrib# Start and enable the servicesystemctl enable --now postgresql
Create a dedicated database and user:
sudo -u postgres psql <<'SQL'CREATE USER rustunnel WITH PASSWORD 'CHANGE_ME';CREATE DATABASE rustunnel OWNER rustunnel;GRANT ALL PRIVILEGES ON DATABASE rustunnel TO rustunnel;SQL
For managed PostgreSQL (e.g. AWS RDS, DigitalOcean Managed Databases, Supabase) skip the apt install step and just note down your connection URL for the config in the next step.
Schema migrations run automatically when the server starts — no manual SQL needed.
Create /etc/rustunnel/server.toml. Generate a strong admin token first:
openssl rand -hex 32
# /etc/rustunnel/server.toml[server]# Primary domain — must match your wildcard DNS record.domain = "edge.rustunnel.com"# Ports for incoming tunnel traffic.http_port = 80https_port = 443# Control-plane WebSocket port — clients connect here.control_port = 4040# Dashboard UI and REST API port.dashboard_port = 8443# Allowed CORS origin for the dashboard UI.# Set to the URL where you serve the dashboard (e.g. http://localhost:3000 for local dev).dashboard_origin = "http://localhost:3000"# ── TLS ─────────────────────────────────────────────────────────────────────[tls]# Paths written by Certbot (see step 7).cert_path = "/etc/letsencrypt/live/edge.rustunnel.com/fullchain.pem"key_path = "/etc/letsencrypt/live/edge.rustunnel.com/privkey.pem"# Set acme_enabled = true only if you want rustunnel to manage certs itself.# When using Certbot (recommended), leave this false.acme_enabled = false# ── Auth ─────────────────────────────────────────────────────────────────────[auth]# Strong random secret — used as the admin token and for client auth.# Generate with: openssl rand -hex 32admin_token = "your-admin-token-here"require_auth = true# ── Database ─────────────────────────────────────────────────────────────────[database]# PostgreSQL connection URL — the database and user must exist before starting# the server (see step 5 above). Schema migrations run automatically on first start.url = "postgresql://rustunnel:CHANGE_ME@localhost:5432/rustunnel"# Per-region SQLite file for captured HTTP request bodies.# The directory must be writable by the rustunnel user.captured_path = "/var/lib/rustunnel/captured.db"# ── Logging ──────────────────────────────────────────────────────────────────[logging]level = "info"format = "json"# Optional: append-only audit log (JSON-lines).# Records auth attempts, tunnel registrations, token creation/deletion.# Omit or comment out to disable.audit_log_path = "/var/lib/rustunnel/audit.log"# ── Limits ───────────────────────────────────────────────────────────────────[limits]max_tunnels_per_session = 10max_connections_per_tunnel = 100rate_limit_rps = 100ip_rate_limit_rps = 100request_body_max_bytes = 10485760 # 10 MB# Inclusive port range reserved for TCP tunnels.tcp_port_range = [20000, 20099]
Both the bare domain and the wildcard are required. The wildcard (*.edge.rustunnel.com) is what makes HTTP subdomain tunnels work.Create the Cloudflare credentials file:
cat > /etc/letsencrypt/cloudflare.ini <<'EOF'# Cloudflare API token with DNS:Edit permission for the zone.dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKENEOFchmod 600 /etc/letsencrypt/cloudflare.ini
Certbot installs a systemd timer for automatic renewal. rustunnel reads TLS certificates
from disk at startup, so it must be restarted after each renewal. Add a deploy hook to
do this automatically:
Port 9090 only needs to be open if you have an external Prometheus scraper. If Prometheus runs on the same host it reaches the metrics endpoint over the loopback network.
# Health check (dashboard is plain HTTP on dashboard_port)curl http://localhost:8443/api/status# Check bound portsss -tlnp | grep rustunnel-serve# Startup logsjournalctl -u rustunnel.service --no-pager | tail -30# Prometheus metricscurl -s http://localhost:9090/metrics
Port 4040 is the control-plane WebSocket — clients connect here. Hitting it with plain HTTP returns HTTP/0.9, which is expected. The dashboard REST API is on dashboard_port (8443 in the config above).