DOCS · ADMIN · INSTALL

Stand it up.

Clone, build qnt-server, point it at Postgres, and run it as a hardened systemd unit. The 25 schema migrations apply themselves on first boot — there's nothing to run by hand.

1 Prerequisites

A small VM is plenty to start. Everything below assumes a Linux host you have root on.

What you need

  • A VM with 1 vCPU / 2 GB RAM or better.
  • PostgreSQL 15+ reachable from the host.
  • Rust 1.75+ toolchain to build the binary.
  • A domain you control, for the control plane and tunnel subdomains.

The build takes roughly 4 minutes on 2 vCPU. You can build on a bigger box and ship only the release binary if you prefer.

2 Clone & build

Clone the repo and compile the qnt-server crate in release mode.

Clone the repository

git clone https://github.com/vikasswaminh/21tunnel
cd 21tunnel

Build the control plane

Compile just the server binary:

cargo build --release -p qnt-server

Needs Rust 1.75+. On 2 vCPU expect ~4 min. The binary lands at target/release/qnt-server.

3 Postgres, JWT key & config

Create the database, drop a signing key, and write a server.toml. The 25 migrations in crates/qnt-db/migrations run automatically and idempotently on first boot — you never run them yourself.

Create the database

createdb qnt

The connection string goes in [database].url, or falls back to the DATABASE_URL environment variable if you leave it unset.

Generate the JWT signing key

Write 48 bytes of randomness to /etc/qnt/jwt.key and lock it down to 0600:

head -c 48 /dev/urandom | base64 | tr -d '\n' > /etc/qnt/jwt.key && chmod 600 /etc/qnt/jwt.key

The path is referenced from config as [web_auth].jwt_secret_path.

Write /etc/qnt/server.toml

A minimal, annotated starting point:

[server]
bind   = "0.0.0.0:443"      # agent transport: TLS + yamux
domain = "tunnel.example.com"

# [server.tls], [server.quic], [server.limits],
# [server.subdomain] and [server.auth] sub-tables are
# available for further tuning.

[database]
enabled         = true                   # required to connect
url             = "postgres://localhost/qnt"
# max_connections has a sane default if omitted

[web_auth]
jwt_secret_path = "/etc/qnt/jwt.key"

[database].enabled = true is required — without it the server will not connect to Postgres.

4 Run as a systemd unit

The repo ships a production unit at deploy/qnt-server.service. Install it, reload, and enable.

Install & enable

cp deploy/qnt-server.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now qnt-server

It launches /opt/ngrok-qt-quic/target/release/qnt-server --config /etc/qnt/server.toml --log-level info from WorkingDirectory=/opt/ngrok-qt-quic, and optionally reads /etc/qnt/qnt-server.env (the leading - means it's ignored if missing).

What the unit gives you

  • Auto-restart: Restart=on-failure, RestartSec=5s, with a crash-loop guard of StartLimitBurst=10 over StartLimitIntervalSec=120.
  • Graceful drain: KillSignal=SIGINT — the binary only catches SIGINT for graceful shutdown — with TimeoutStopSec=30s.
  • FD ceiling: LimitNOFILE=65536. The default 1024 starts dropping connections at roughly 50 tunnels.
  • Hardening: NoNewPrivileges=true, PrivateTmp=true, ProtectSystem=full, ProtectHome=true, MemoryDenyWriteExecute=true.

In v1 the service runs as root; a dedicated qnt user is a follow-up.

5 First boot

Tail the journal and confirm the server came up clean. Migrations apply here on the first run.

Follow the logs

journalctl -u qnt-server -f

You're looking for lines like these:

[INFO] web auth: JWT signing key loaded
[INFO] audit retention worker started retention_days=90
[INFO] API server accepting local_addr=Some(127.0.0.1:9090)

The three listeners

  • Agent transport (TLS + yamux): 0.0.0.0:443
  • Public HTTP proxy: 0.0.0.0:8080
  • Control-plane API: 0.0.0.0:9090

Metrics are exposed on a localhost-only endpoint and are not reachable off the box.

6 Seed a superadmin

Sign up through the dashboard once, then promote that account directly in Postgres.

Promote your account

After your first signup, run:

psql qnt -c "UPDATE users SET is_superadmin=TRUE, email_verified=TRUE WHERE email='you@example.com';"

Swap in the email you signed up with. This both grants superadmin and marks the address verified.

Next

The server's up. Wire authentication and billing, or head back to the admin hub.