Skip to main content
rustunnel is fully self-hostable. This guide covers a production deployment on Ubuntu 22.04 using systemd, Let’s Encrypt TLS via Certbot, and PostgreSQL. For a Docker-based deployment see the Docker Deployment guide.

Requirements

To build

RequirementVersionNotes
Rust toolchain1.76+Install via rustup
pkg-configanyRequired by reqwest (TLS)
libssl-devanyapt install libssl-dev
Node.js + npm18+Only needed to rebuild the dashboard UI

To run

RequirementNotes
Linux (Ubuntu 22.04+)systemd service included
Public IP + DNSWildcard DNS *.tunnel.yourdomain.com → server IP required for HTTP tunnels
TLS certificatePEM format — Let’s Encrypt recommended
PostgreSQLStores API tokens and tunnel audit log

Step 1 — Install dependencies

apt update && apt install -y \
  pkg-config libssl-dev curl git \
  certbot python3-certbot-dns-cloudflare
Install Rust (as the build user, not root):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"

Step 2 — Build release binaries

git clone https://github.com/joaoh82/rustunnel.git
cd rustunnel
cargo build --release -p rustunnel-server -p rustunnel-client
Binaries will be at:
  • target/release/rustunnel-server
  • target/release/rustunnel

Step 3 — Create system user and directories

useradd --system --no-create-home --shell /usr/sbin/nologin rustunnel

mkdir -p /etc/rustunnel /var/lib/rustunnel
chown rustunnel:rustunnel /var/lib/rustunnel
chmod 750 /var/lib/rustunnel

Step 4 — Install the server binary

install -Dm755 target/release/rustunnel-server /usr/local/bin/rustunnel-server

# Optionally install the client system-wide
install -Dm755 target/release/rustunnel /usr/local/bin/rustunnel
Or use the Makefile shortcut (runs build + install + systemd setup):
sudo make deploy

Step 5 — Create the server config file

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    = 80
https_port   = 443

# Control-plane WebSocket port — clients connect here.
control_port = 4040

# Dashboard UI and REST API port.
dashboard_port = 8443

# ── TLS ─────────────────────────────────────────────────────────────────────
[tls]
# Paths written by Certbot (see step 6).
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.
admin_token  = "your-admin-token-here"
require_auth = true

# ── Database ─────────────────────────────────────────────────────────────────
[database]
# SQLite file. The directory must be writable by the rustunnel user.
path = "/var/lib/rustunnel/rustunnel.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    = 10
max_connections_per_tunnel = 100
rate_limit_rps             = 100
ip_rate_limit_rps          = 100
request_body_max_bytes     = 10485760   # 10 MB

# Inclusive port range reserved for TCP tunnels.
tcp_port_range = [20000, 20099]
Secure the file:
chown root:rustunnel /etc/rustunnel/server.toml
chmod 640 /etc/rustunnel/server.toml

Step 6 — TLS certificates (Let’s Encrypt + Cloudflare)

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_TOKEN
EOF

chmod 600 /etc/letsencrypt/cloudflare.ini
Request the certificate:
certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "edge.rustunnel.com" \
  -d "*.edge.rustunnel.com" \
  --agree-tos \
  --email your@email.com
Certbot writes the PEM files to:
/etc/letsencrypt/live/edge.rustunnel.com/fullchain.pem
/etc/letsencrypt/live/edge.rustunnel.com/privkey.pem
Certbot installs a systemd timer for automatic renewal — no further action needed. Allow the rustunnel service user to read the certificates:
chmod 755 /etc/letsencrypt/{live,archive}
chmod 640 /etc/letsencrypt/live/edge.rustunnel.com/*.pem
chgrp rustunnel /etc/letsencrypt/live/edge.rustunnel.com/*.pem
chgrp rustunnel /etc/letsencrypt/archive/edge.rustunnel.com/*.pem
chmod 640 /etc/letsencrypt/archive/edge.rustunnel.com/*.pem

Step 7 — Set up the systemd service

install -Dm644 deploy/rustunnel.service /etc/systemd/system/rustunnel.service

systemctl daemon-reload
systemctl enable --now rustunnel.service

# Check it started
systemctl status rustunnel.service
journalctl -u rustunnel.service -f

Step 8 — Open firewall ports

ufw allow 80/tcp    comment "rustunnel HTTP edge"
ufw allow 443/tcp   comment "rustunnel HTTPS edge"
ufw allow 4040/tcp  comment "rustunnel control plane"
ufw allow 8443/tcp  comment "rustunnel dashboard"
ufw allow 9090/tcp  comment "rustunnel Prometheus metrics"
ufw allow 20000:20099/tcp comment "rustunnel TCP tunnels"
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.

Step 9 — Verify the server is running

# Health check
curl http://localhost:8443/api/status

# Check bound ports
ss -tlnp | grep rustunnel-serve

# Startup logs
journalctl -u rustunnel.service --no-pager | tail -30

# Prometheus metrics
curl -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 is on dashboard_port (8443 by default).

Connecting a client

rustunnel http 3000 \
  --server edge.rustunnel.com:4040 \
  --token YOUR_ADMIN_TOKEN

Updating the server

Pull the latest code, rebuild, install, and restart in one command:
cd ~/rustunnel && sudo make update-server
This runs: git pullcargo build --releaseinstallsystemctl restartsystemctl status.

Port reference

PortProtocolPurpose
80TCPHTTP edge — redirects to HTTPS; ACME HTTP-01 challenge
443TCPHTTPS edge — TLS-terminated tunnel ingress
4040TCPControl-plane WebSocket — clients connect here
8443TCPDashboard UI and REST API
9090TCPPrometheus metrics (/metrics)
20000–20099TCPTCP tunnel range (configurable via tcp_port_range)

Config file reference

KeyTypeDefaultDescription
server.domainstringBase domain for tunnel URLs
server.http_portu16HTTP edge port
server.https_portu16HTTPS edge port
server.control_portu16WebSocket control-plane port
server.dashboard_portu164040Dashboard port
tls.cert_pathstringPath to TLS certificate (PEM)
tls.key_pathstringPath to TLS private key (PEM)
tls.acme_enabledboolfalseEnable built-in ACME renewal
tls.acme_emailstring""Contact email for ACME
tls.acme_stagingboolfalseUse Let’s Encrypt staging CA
tls.cloudflare_api_tokenstring""Cloudflare DNS API token (prefer env var CLOUDFLARE_API_TOKEN)
tls.cloudflare_zone_idstring""Cloudflare Zone ID (prefer env var CLOUDFLARE_ZONE_ID)
auth.admin_tokenstringMaster auth token
auth.require_authboolReject unauthenticated clients
database.pathstringSQLite file path (:memory: for tests)
logging.levelstringtrace / debug / info / warn / error
logging.formatstringjson or pretty
logging.audit_log_pathstringnullAudit log path (JSON-lines); omit to disable
limits.max_tunnels_per_sessionusizeMax tunnels per connected client
limits.max_connections_per_tunnelusizeMax concurrent connections per tunnel
limits.rate_limit_rpsu32Per-tunnel request rate cap (req/s)
limits.ip_rate_limit_rpsu32100Per-source-IP rate cap (req/s); 0 = disabled
limits.request_body_max_bytesusizeMax proxied request body size (bytes)
limits.tcp_port_range[u16, u16]Inclusive [low, high] TCP tunnel port range
region.idstring"default"Region identifier (e.g. "eu", "us", "ap")
region.namestring"Default"Human-readable region name shown in the dashboard
region.locationstring""Physical location label (e.g. "Helsinki, FI")

Monitoring

A Prometheus metrics endpoint is available at :9090/metrics:
rustunnel_active_sessions      # gauge: connected clients
rustunnel_active_tunnels_http  # gauge: active HTTP tunnels
rustunnel_active_tunnels_tcp   # gauge: active TCP tunnels
Start the full monitoring stack (Prometheus + Grafana):
make docker-run-monitoring
# Grafana:    http://localhost:3000  (admin / changeme)
# Prometheus: http://localhost:9090
The default Grafana password is changeme. Set GRAFANA_PASSWORD before starting the stack in production.
export GRAFANA_PASSWORD=your-strong-password
make docker-run-monitoring