Skip to main content

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:

PortEnv varDefaultWhat it's for
HTTPSRABA_HTTPS_PORT7222The dashboard, /api/*, and every HTTP-tunneled project (routed by Host header). What visitors and the dashboard actually hit.
Plain HTTP redirectRABA_HTTP_PORT8080301-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.
TunnelRABA_TUNNEL_PORT4443Not 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 rangeRABA_TCP_UDP_PORT_RANGE_START / _END20000-21000One 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 have RABA_DNS_API_TOKEN/RABA_DNS_ZONE_ID/RABA_ACME_EMAIL all 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_DOMAIN in .env after 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_EMAIL all set, none of this runs at all — the server falls back to RABA_TLS_CERT_PATH/RABA_TLS_KEY_PATH (a manually-obtained cert, e.g. via certbot) 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.