Deployment
A deeper reference for running raba manually — full configuration, sharing a host with another service via a reverse proxy, and exactly when TLS certificates actually get issued and renewed. See Installation first if you just want the fastest path to a running instance.
Ports
Every port raba listens on, all configurable, all with non-privileged defaults so nothing needs root to bind them:
| Port | Env var | Default | What it's for |
|---|---|---|---|
| HTTPS | RABA_HTTPS_PORT | 7222 | The dashboard, /api/*, and every HTTP-tunneled project (routed by Host header). What visitors and the dashboard actually hit. |
| Plain HTTP redirect | RABA_HTTP_PORT | 8080 | 301-redirects http:// requests to the HTTPS port above — optional, only useful if you want http://<domain> (no explicit port) to work at all instead of failing at the TLS handshake level. |
| Tunnel | RABA_TUNNEL_PORT | 4443 | Not for visitors or browsers. This is where raba-cli (the Client running on your machine) connects to establish the persistent multiplexed tunnel connection — see Architecture. It has to be reachable from wherever you run raba http/tcp/udp, which usually means it needs to be open to the public internet too, not just localhost on the server. |
| TCP/UDP project range | RABA_TCP_UDP_PORT_RANGE_START / _END | 20000-21000 | One dedicated port is allocated automatically from this range per TCP/UDP project (there's no Host header to route raw TCP/UDP by, so each gets its own port instead). Must stay open on your firewall/security group across the whole range if you want TCP/UDP tunnels to actually work. |
If you're on Docker, docker-compose.yml maps all four to the same host ports by default —
change the env vars, and the compose file's port mappings follow automatically (they read the
same variables, not hardcoded). If you're Direct/bare-metal, make sure your firewall allows
all four categories, not just :443/:80.
The reserved tunnel subdomain
You can't create an HTTP project with the subdomain tunnel (e.g. tunnel.yourdomain.com) —
the server rejects it outright. This is not an active feature today, just a reservation:
it's set aside for a possible future capability (multiplexing the Client's tunnel connection
onto the HTTPS port itself, by SNI hostname, instead of needing a separate RABA_TUNNEL_PORT
at all) that hasn't been built yet. Reserving the name now, before any real project could ever
claim it, costs nothing; freeing it up later would mean breaking whoever already has it.
Docker Compose, manually
git clone https://github.com/codad5/raba.git
cd raba
cp docker/.env.example .env
Edit .env:
# Required
JWT_SECRET= # openssl rand -hex 32
RABA_DOMAIN=tunnel.example.com
# Optional -- automated TLS (see "TLS certificates" below). All three
# required together, or leave unset to fall back to a manual cert
# (RABA_TLS_CERT_PATH/RABA_TLS_KEY_PATH) or the self-signed dev cert.
RABA_DNS_API_TOKEN= # Cloudflare API token, DNS-edit scope on RABA_DOMAIN's zone
RABA_DNS_ZONE_ID= # that zone's ID
RABA_ACME_EMAIL= # contact address for Let's Encrypt renewal-failure notices
# Optional -- ports, only if you're not using the non-privileged defaults
RABA_HTTPS_PORT=7222
RABA_HTTP_PORT=8080
docker compose up -d --build
Full variable reference: docker/.env.example.
Direct (systemd), manually
cd server && cargo build --release
Or download the pre-built linux-x86_64 binary from the latest
GitHub release instead of building from source.
Write a systemd unit (adapt
install/templates/raba.service.template):
[Unit]
Description=raba tunneling server
After=network.target
[Service]
Type=simple
User=raba
WorkingDirectory=/opt/raba
EnvironmentFile=/opt/raba/.env
ExecStart=/opt/raba/raba-server
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE is what lets the binary bind :443/:80 without
running as root — granted fresh at every service start, so it survives binary updates with no
manual setcap re-run needed.
sudo systemctl daemon-reload
sudo systemctl enable --now raba
Sharing :443/:80 with another service
If raba isn't the only thing on this box — you're already running nginx, Caddy, or another
site — it can't bind :443/:80 directly, since only one process can hold a port. Put it
behind an SNI-passthrough proxy instead: the proxy doesn't terminate raba's TLS, it reads
just the SNI hostname from the unencrypted part of the TLS handshake and forwards the
encrypted bytes straight through to raba's own port, which does the actual TLS termination
itself. This only works for :443 — plain :80 has no SNI at all (there's no TLS handshake to
read one from), so a shared :80 needs an ordinary proxy_pass reverse-proxy block instead,
a structurally different kind of config.
nginx (:443 passthrough)
# /etc/nginx/stream.d/raba.conf, included from the top-level `stream {}` block
# (not `http {}` -- ssl_preread needs nginx's stream module).
server {
listen 443;
ssl_preread on;
proxy_pass 127.0.0.1:7222; # raba's actual RABA_HTTPS_PORT
}
sudo nginx -t && sudo systemctl reload nginx
nginx (:80, ordinary reverse proxy — not passthrough)
# Regular http {} block -- plain HTTP has no SNI, so this is a normal
# proxy_pass, not ssl_preread.
server {
listen 80;
location / {
proxy_pass http://127.0.0.1:8080; # raba's actual RABA_HTTP_PORT
}
}
Caddy (:443 passthrough)
Needs the third-party layer4 module (not built into
stock Caddy) — see
install/templates/Caddyfile.template
for the exact config block.
Using install-server.sh instead of doing this by hand? It asks which proxy you use and
generates the filled-in config file for you automatically — see
Installation.
Deploying behind a PaaS that manages TLS for you (Dokploy, Coolify, CapRover, etc.)
These platforms are all built the same way under the hood: a Traefik- or Caddy-managed reverse proxy that terminates TLS itself and forwards plain, decrypted HTTP to your container based on a domain you register in their UI. That's the opposite of what raba needs — raba wants the raw TLS handshake so it can do its own SNI-based certificate resolution and ACME automation. If one of these platforms terminates TLS in front of raba, the connection just fails: raba's listener receives decrypted HTTP where it expects a TLS handshake.
It gets worse for raba specifically: these platforms' domain UIs expect you to register a small,
fixed set of domains ahead of time (and typically don't support wildcards at all). raba's whole
model is the opposite — an instance domain with unlimited dynamic subdomains, plus however many
team custom domains get added later at runtime via raba team domain-set. The platform has no
way to know about a domain it was never told about in advance.
The fix: don't use the platform's domain/TLS feature for raba at all. Every one of these
tools also has a way to expose container ports directly, bypassing its reverse proxy entirely
(in Dokploy, that's Advanced → Ports, not Domains) — use that instead, mapping the same
ports listed in Ports above, exactly like docker-compose.yml already does. raba
keeps full control of TLS/SNI/domain routing exactly as designed; the platform just becomes a
container host, the same as any other Docker host in this guide.
TLS certificates — issuance and renewal
This is the part most deployment guides gloss over, so here's exactly what happens, verified against the actual renewal logic:
- A background task runs an issuance/renewal check immediately on server startup, then again every 24 hours — not a fixed daily clock time, just every 24 hours from whenever the process started.
- Each check looks for any domain (the instance's own, or a team's custom domain — both live in the same table and go through the same code path) that either has no cert yet, previously failed, or is within 30 days of its current cert's expiry. Let's Encrypt certs are always valid for 90 days, so in steady state a given domain gets re-issued roughly once every 60 days.
- The instance's own domain (
RABA_DOMAIN) is automatically marked verified on first boot — no DNS-resolution check required for it the way team custom domains need (raba team domain-verify), since you already control it directly by definition. This means if you haveRABA_DNS_API_TOKEN/RABA_DNS_ZONE_ID/RABA_ACME_EMAILall set, your instance's own cert gets issued on the very first startup, automatically, with no further action from you. - This first-boot seeding only happens once, on a genuinely empty database. Changing
RABA_DOMAINin.envafter the instance has already started once does not retroactively re-seed or re-verify a new domain row — it's a one-time setup value, not a live reconfiguration switch. - Without
RABA_DNS_API_TOKEN/RABA_DNS_ZONE_ID/RABA_ACME_EMAILall set, none of this runs at all — the server falls back toRABA_TLS_CERT_PATH/RABA_TLS_KEY_PATH(a manually-obtained cert, e.g. viacertbot) if set, or a self-signed development certificate (browsers will warn) otherwise. - Team custom domains work identically once verified — see Teams and custom domains for the DNS delegation model that lets raba issue certs for a team's domain without ever needing that team's own DNS provider credentials.