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
| Requirement | Version | Notes |
|---|
| Rust toolchain | 1.76+ | Install via rustup |
pkg-config | any | Required by reqwest (TLS) |
libssl-dev | any | apt install libssl-dev |
| Node.js + npm | 18+ | Only needed to rebuild the dashboard UI |
To run
| Requirement | Notes |
|---|
| Linux (Ubuntu 22.04+) | systemd service included |
| Public IP + DNS | Wildcard DNS *.tunnel.yourdomain.com → server IP required for HTTP tunnels |
| TLS certificate | PEM format — Let’s Encrypt recommended |
| PostgreSQL | Stores 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):
Step 5 — Create the server config file
Create /etc/rustunnel/server.toml. Generate a strong admin token first:
# /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 pull → cargo build --release → install → systemctl restart → systemctl status.
Port reference
| Port | Protocol | Purpose |
|---|
80 | TCP | HTTP edge — redirects to HTTPS; ACME HTTP-01 challenge |
443 | TCP | HTTPS edge — TLS-terminated tunnel ingress |
4040 | TCP | Control-plane WebSocket — clients connect here |
8443 | TCP | Dashboard UI and REST API |
9090 | TCP | Prometheus metrics (/metrics) |
20000–20099 | TCP | TCP tunnel range (configurable via tcp_port_range) |
Config file reference
| Key | Type | Default | Description |
|---|
server.domain | string | — | Base domain for tunnel URLs |
server.http_port | u16 | — | HTTP edge port |
server.https_port | u16 | — | HTTPS edge port |
server.control_port | u16 | — | WebSocket control-plane port |
server.dashboard_port | u16 | 4040 | Dashboard port |
tls.cert_path | string | — | Path to TLS certificate (PEM) |
tls.key_path | string | — | Path to TLS private key (PEM) |
tls.acme_enabled | bool | false | Enable built-in ACME renewal |
tls.acme_email | string | "" | Contact email for ACME |
tls.acme_staging | bool | false | Use Let’s Encrypt staging CA |
tls.cloudflare_api_token | string | "" | Cloudflare DNS API token (prefer env var CLOUDFLARE_API_TOKEN) |
tls.cloudflare_zone_id | string | "" | Cloudflare Zone ID (prefer env var CLOUDFLARE_ZONE_ID) |
auth.admin_token | string | — | Master auth token |
auth.require_auth | bool | — | Reject unauthenticated clients |
database.path | string | — | SQLite file path (:memory: for tests) |
logging.level | string | — | trace / debug / info / warn / error |
logging.format | string | — | json or pretty |
logging.audit_log_path | string | null | Audit log path (JSON-lines); omit to disable |
limits.max_tunnels_per_session | usize | — | Max tunnels per connected client |
limits.max_connections_per_tunnel | usize | — | Max concurrent connections per tunnel |
limits.rate_limit_rps | u32 | — | Per-tunnel request rate cap (req/s) |
limits.ip_rate_limit_rps | u32 | 100 | Per-source-IP rate cap (req/s); 0 = disabled |
limits.request_body_max_bytes | usize | — | Max proxied request body size (bytes) |
limits.tcp_port_range | [u16, u16] | — | Inclusive [low, high] TCP tunnel port range |
region.id | string | "default" | Region identifier (e.g. "eu", "us", "ap") |
region.name | string | "Default" | Human-readable region name shown in the dashboard |
region.location | string | "" | 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