> ## Documentation Index
> Fetch the complete documentation index at: https://rustunnel.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

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](/docs/guides/docker-deployment) guide instead.

## Requirements

### To build

| Requirement    | Version | Notes                                   |
| -------------- | ------- | --------------------------------------- |
| Rust toolchain | 1.76+   | Install via [rustup](https://rustup.rs) |
| `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

```bash theme={null}
apt update && apt install -y \
  pkg-config libssl-dev curl git \
  certbot python3-certbot-dns-cloudflare
```

Install Rust (as the build user, not root):

```bash theme={null}
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
```

***

## Step 2 — Build release binaries

```bash theme={null}
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

```bash theme={null}
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

```bash theme={null}
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):

```bash theme={null}
sudo make deploy
```

***

## Step 5 — Set up PostgreSQL

rustunnel requires PostgreSQL for shared state (tokens, tunnel history, audit log).

```bash theme={null}
apt install -y postgresql postgresql-contrib

# Start and enable the service
systemctl enable --now postgresql
```

Create a dedicated database and user:

```bash theme={null}
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
```

<Tip>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.</Tip>

Schema migrations run automatically when the server starts — no manual SQL needed.

***

## Step 6 — Create the server config file

Create `/etc/rustunnel/server.toml`. Generate a strong admin token first:

```bash theme={null}
openssl rand -hex 32
```

```toml theme={null}
# /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

# 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 32
admin_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    = 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:

```bash theme={null}
chown root:rustunnel /etc/rustunnel/server.toml
chmod 640 /etc/rustunnel/server.toml
```

***

## Step 7 — 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:

```bash theme={null}
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:

```bash theme={null}
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:

```text theme={null}
/etc/letsencrypt/live/edge.rustunnel.com/fullchain.pem
/etc/letsencrypt/live/edge.rustunnel.com/privkey.pem
```

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:

```bash theme={null}
cat > /etc/letsencrypt/renewal-hooks/deploy/restart-rustunnel.sh <<'EOF'
#!/bin/sh
systemctl restart rustunnel.service
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-rustunnel.sh
```

Allow the `rustunnel` service user to read the certificates:

```bash theme={null}
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 8 — Set up the systemd service

```bash theme={null}
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 9 — Open firewall ports

```bash theme={null}
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"
```

<Note>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.</Note>

***

## Step 10 — Verify the server is running

```bash theme={null}
# Health check (dashboard is plain HTTP on dashboard_port)
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
```

<Note>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).</Note>

***

## Connecting a client

```bash theme={null}
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:

```bash theme={null}
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 REST API port                                          |
| `server.dashboard_origin`           | string      | `""`                             | Allowed CORS origin for the dashboard UI                         |
| `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.url`                      | string      | —                                | PostgreSQL connection URL (required)                             |
| `database.captured_path`            | string      | `/var/lib/rustunnel/captured.db` | Per-region SQLite file for captured HTTP request bodies          |
| `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`:

```text theme={null}
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):

```bash theme={null}
make docker-run-monitoring
# Grafana:    http://localhost:3000  (admin / changeme)
# Prometheus: http://localhost:9090
```

<Warning>The default Grafana password is `changeme`. Set `GRAFANA_PASSWORD` before starting the stack in production.</Warning>

```bash theme={null}
export GRAFANA_PASSWORD=your-strong-password
make docker-run-monitoring
```

***

## Next steps

<CardGroup cols={2}>
  <Card title="Client Guide" icon="terminal" href="/docs/guides/client-guide">
    Configure the rustunnel CLI to connect to your self-hosted server.
  </Card>

  <Card title="Architecture" icon="diagram-project" href="/docs/guides/architecture">
    Understand the control plane, data plane, and yamux multiplexing under the hood.
  </Card>

  <Card title="Load Balancing & Health Checks" icon="scale-balanced" href="/docs/reference/load-balancing">
    Run multiple backends behind one subdomain with automatic failover.
  </Card>

  <Card title="MCP Server" icon="robot" href="/docs/guides/mcp-server">
    Let AI agents manage tunnels on your self-hosted server via MCP.
  </Card>
</CardGroup>
