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.
A small VM is plenty to start. Everything below assumes a Linux host you have root on.
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.
Clone the repo and compile the qnt-server crate in
release mode.
git clone https://github.com/vikasswaminh/21tunnel
cd 21tunnel 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.
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.
createdb qnt
The connection string goes in [database].url, or
falls back to the DATABASE_URL environment variable
if you leave it unset.
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.
/etc/qnt/server.tomlA 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.
The repo ships a production unit at
deploy/qnt-server.service. Install it, reload, and
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).
Restart=on-failure, RestartSec=5s, with a crash-loop guard of StartLimitBurst=10 over StartLimitIntervalSec=120.KillSignal=SIGINT — the binary only catches SIGINT for graceful shutdown — with TimeoutStopSec=30s.LimitNOFILE=65536. The default 1024 starts dropping connections at roughly 50 tunnels.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.
Tail the journal and confirm the server came up clean. Migrations apply here on the first run.
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) 0.0.0.0:4430.0.0.0:80800.0.0.0:9090Metrics are exposed on a localhost-only endpoint and are not reachable off the box.
Sign up through the dashboard once, then promote that account directly in Postgres.
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.
The server's up. Wire authentication and billing, or head back to the admin hub.