Skip to content

Self-hosting with Docker

The self-hosted Restura is a single Node container that serves the SPA and the /api/* proxy on one port. Same Hono app that runs in the Cloudflare Worker — just with Node adapters for CONNECT proxying and native WebSockets.

Terminal window
docker run -p 3000:3000 \
-e WORKER_PROXY_TOKEN=$(openssl rand -hex 32) \
ghcr.io/dipjyotimetia/restura:latest

Open http://localhost:3000. All user data lives in the browser’s IndexedDB — the server is stateless.

services:
restura:
image: ghcr.io/dipjyotimetia/restura:latest
ports:
- '3000:3000'
environment:
WORKER_PROXY_TOKEN: ${WORKER_PROXY_TOKEN:?required}
ENVIRONMENT: production
restart: unless-stopped

The repo ships a docker-compose.yml you can copy.

Env varPurposeDefault
PORTListen port3000
HOSTBind address0.0.0.0
WORKER_PROXY_TOKENShared token for X-Worker-Token header from the clientrequired in prod
REQUIRE_CF_ACCESSIf true, trust the Cf-Access-Authenticated-User-Email header from your reverse proxyfalse
ENVIRONMENTdevelopment allows localhost upstream; anything else enforces SSRF guardproduction
ALLOW_PRIVATE_IPSAllow RFC 1918 / link-local upstreams (still blocks cloud-metadata)false
ALLOWED_ORIGINComma-separated Origin allowlist* (recommend setting)
RESTURA_STATIC_ROOTPath to the SPA bundle/app/dist/web

You have two options:

  1. Shared token — set WORKER_PROXY_TOKEN and have the client send it in the X-Worker-Token header. Configure the URL in VITE_WORKER_URL at build time, or in the in-app settings.
  2. Reverse-proxy auth — set REQUIRE_CF_ACCESS=true and put Cloudflare Access (or any IdP that injects an authenticated email header) in front. The server reads the email from the proxy header and trusts it.

By default the SSRF guard blocks any RFC 1918 / link-local upstream — even from your self-hosted instance. If you genuinely want to proxy to internal services, set ALLOW_PRIVATE_IPS=true. The cloud-metadata blacklist (169.254.169.254) stays enforced regardless.

The WebSocket ticket store is per-replica in-memory. Tickets expire in 30 seconds. If you run multiple replicas behind a load balancer, you need either:

  • Sticky session routing for the /api/ws-ticket/api/ws flow, or
  • A single replica (fine for most internal deployments).

This will be revisited in a future release.

Per-replica in-memory token bucket. For multi-replica deployments, put a rate limiter (Cloudflare, nginx, Envoy) in front.