Operations

Self-host

Run your own routess instance with Docker Compose.

This walkthrough gets a routess instance running on your own machine or server in about five minutes. For Kubernetes, see Kubernetes.

What you need

  • Docker and Docker Compose v2 (modern Docker Desktop or Docker Engine ships with both).
  • A Google OAuth client for Google sign-in (the compose file requires it). Users can also sign in with email + password once the stack is up.
  • A Mapbox access token; the free tier is fine.
  • A Valhalla routing engine (VALHALLA_URL). Route calculation and surface analysis go through it; without it the routing endpoints return 503.
  • Long random strings for JWT_SECRET, ANALYTICS_SALT, PAT_PEPPER, and DB_PASSWORD. Generate with openssl rand -base64 32.

Optional, gracefully disabled when unset:

  • Resend for transactional email (without it, verification URLs are logged to stdout).
  • Sentry / GlitchTip for browser error reporting.
  • Umami for product analytics.
  • Admin role for accessing the in-app admin panel.

Set up Google OAuth

  1. Open the Google Cloud Console credentials page.
  2. Create an OAuth 2.0 Client ID of type Web application.
  3. Under Authorized JavaScript origins, add the URL your users will open (e.g. http://localhost:8080 or https://routess.example.com).
  4. Under Authorized redirect URIs, add <your-web-url>/auth/callback.
  5. Copy the Client ID and Client secret.

Get a Mapbox token

  1. Sign up at account.mapbox.com.
  2. Create a public access token (pk.…) under Access tokens.
  3. Restrict it to your domain when you go to production.

Run it

git clone https://github.com/robbeverhelst/routess.git
cd routess
cp docker/.env.selfhost.example .env
# edit .env and fill in the required values
docker compose -f docker/compose.selfhost.yaml --project-directory . up -d

Then open http://localhost:8080.

The stack starts four containers:

ServiceImageDefault port
Postgrespostgres:18(internal)
APIghcr.io/robbeverhelst/routess-api3000
Webghcr.io/robbeverhelst/routess-web8080
Docsghcr.io/robbeverhelst/routess-docs3002

Change the host ports via WEB_PORT, API_PORT, DOCS_PORT in .env.

Pin a version

Default tag is latest. For production, pin to a specific release:

ROUTESS_TAG=1.122.7 docker compose -f docker/compose.selfhost.yaml --project-directory . up -d

Or set ROUTESS_TAG in .env. See the releases page for available tags.

Environment variables

The .env file in docker/.env.selfhost.example is the source of truth. The variables you'll touch most often:

VariableRequiredDefaultPurpose
JWT_SECRETyes-Signs session and API tokens. Rotating logs everyone out.
ANALYTICS_SALTyes-Hashes user IDs in analytics events. Rotating resets the analytics pseudonyms.
PAT_PEPPERyes-Hashes personal access tokens at rest. Rotating invalidates every existing PAT.
DB_PASSWORDyes-Postgres password.
GOOGLE_CLIENT_ID/SECRETyes-Google OAuth credentials.
VITE_MAPBOX_ACCESS_TOKENyes-Mapbox access token used by the web app.
PUBLIC_WEB_URLnohttp://localhost:8080Where users open the app. Used as OAuth redirect base.
PUBLIC_API_URLnohttp://localhost:3000Where the web app reaches the API.
ADMIN_EMAILSnoemptyComma-separated emails granted the admin role on login. Empty disables the admin panel.
RESEND_API_KEYnoemptyResend API key. Empty falls back to logging verification URLs to stdout.
EMAIL_FROMnoroutess <[email protected]>Sender address used by Resend.
VALHALLA_URLyesemptyBase URL of a self-hosted Valhalla instance for routing and surface analysis. Empty makes the routing endpoints return 503.
VITE_UMAMI_URL / _WEBSITE_IDnoemptyUmami endpoint and site ID for the web app. Empty disables product analytics.
UMAMI_URL / _WEBSITE_IDnoemptySame Umami endpoint and site ID for the docs site (Next reads them server-side).
VITE_SENTRY_DSNnoemptySentry / GlitchTip DSN for browser error reporting.
VITE_SENTRY_ENVIRONMENTnoproductionEnvironment tag attached to browser error reports.
PUBLIC_DOCS_URLnohttp://localhost:3002Canonical origin of the docs site, used in its sitemap, robots.txt, and llms.txt links.

Advanced variables

The API understands more knobs than the compose file passes through. To use one, add it under the api service environment in docker/compose.selfhost.yaml (or your own override file):

VariableDefaultPurpose
LOG_LEVELinfoPino log level for the API (debug, info, warn, error).
SESSION_TTL_DAYS7How long a browser session stays valid.
SESSION_COOKIE_NAMEroutess_sessionName of the HTTP-only session cookie.
JWT_EXPIRES_IN7dJWT token lifetime.
FRONTEND_URLSemptyComma- or newline-separated list of allowed web origins; overrides FRONTEND_URL when set.
SWAGGER_ENABLEDfalse in prodServe interactive API docs at SWAGGER_PATH.
SWAGGER_PATH/apiPath for the Swagger UI when enabled.
TELEMETRY_ENABLEDtrueMaster switch for OpenTelemetry (metrics + traces).
METRICS_ENABLEDtruePrometheus metrics at /metrics on the API port. Returns 503 when disabled.
OTEL_EXPORTER_OTLP_ENDPOINTemptyOTLP collector base URL for traces (/v1/traces is appended). Empty disables trace export.
OTEL_EXPORTER_OTLP_HEADERSemptyExtra OTLP headers as a JSON object, e.g. {"authorization":"Bearer …"}.
GRAFANA_URLSemptyJSON object of dashboard links shown in the admin panel: {"apiOverview":"…","apiLatency":"…","runtime":"…"}.

For the web container:

VariableDefaultPurpose
VITE_SENTRY_TRACES_SAMPLE_RATE0.1Fraction of browser sessions traced when Sentry is enabled.
VITE_SENTRY_DEBUGfalseVerbose Sentry SDK logging.

Browser console logging defaults to warn. To debug a single browser, set localStorage["routess:log-level"] to debug, info, warn, error, or none and reload.

Billing (hosted product only)

Routess.com sells a Pro year pass (ADR 0039). A self-hosted instance leaves billing off, which is the default: every feature stays unlocked, everyone gets GENERATION_QUOTA_PER_DAY route generations a day, and no payment provider is contacted. Keep BILLING_ENABLED=false, or leave it unset.

For reference, these are the variables the hosted product sets. With BILLING_ENABLED=true the API refuses to start unless the launch moment and all three Stripe values are set.

VariableDefaultPurpose
BILLING_ENABLEDfalseSwitches the Pro pass, the paywall and the per-tier generation quota on.
BILLING_PROVIDERstripePayment provider. Only stripe is implemented.
BILLING_LAUNCHED_ATemptyISO timestamp of the billing launch. Accounts created before it get 3 months of Pro on their next login.
STRIPE_SECRET_KEYemptyStripe secret key (sk_live_… or a restricted rk_live_…).
STRIPE_WEBHOOK_SECRETemptySigning secret (whsec_…) of the webhook endpoint <api>/api/v1/billing/webhook.
STRIPE_PRICE_PRO_YEAR_PASSemptyStripe Price id of the one-off Pro year pass.
GENERATION_QUOTA_PER_DAY_ANONYMOUS1Daily route generations for signed-out visitors (per IP) while billing is on. 0 disables the cap.
GENERATION_QUOTA_PER_DAY_FREE3Same for signed-in users on the free plan.
GENERATION_QUOTA_PER_DAY_PRO50Same for Pro users.

Put it behind a reverse proxy

The default compose exposes ports directly. For TLS and a real domain, front the web and api services with a reverse proxy (Caddy, Traefik, ingress-nginx, Cloudflare Tunnel) and override the public URLs in .env:

PUBLIC_WEB_URL=https://routess.example.com
PUBLIC_API_URL=https://api.routess.example.com

PUBLIC_WEB_URL is also used as the OAuth redirect base, so it must match what you configured in Google Cloud.

Upgrade

Take a database backup first (see below); migrations are forward-only, so a snapshot is your rollback path. Then:

docker compose -f docker/compose.selfhost.yaml --project-directory . pull
docker compose -f docker/compose.selfhost.yaml --project-directory . up -d

The API runs migrations automatically on startup. There is no manual migration step. To roll back, restore the pre-upgrade dump and pin ROUTESS_TAG to the previous release.

Back up

The Postgres data lives in the postgres_data named volume. Standard pg_dump works:

docker compose -f docker/compose.selfhost.yaml --project-directory . exec postgres \
  pg_dump -U routess routess > routess-$(date +%F).sql

Restore into a fresh volume with psql:

docker compose -f docker/compose.selfhost.yaml --project-directory . exec -T postgres \
  psql -U routess routess < routess-2026-06-04.sql

Troubleshooting

  • Is the API healthy? curl http://localhost:3000/health/ready returns 200 when the API can reach the database; /health/live checks only that the process is up.
  • "address already in use" on :8080 or :3000: change WEB_PORT / API_PORT in .env.
  • Google sign-in returns redirect_uri_mismatch: the OAuth client's authorised redirect URI must exactly match <PUBLIC_WEB_URL>/auth/callback.
  • Map tiles don't load: verify VITE_MAPBOX_ACCESS_TOKEN is set; check browser devtools for 401s from api.mapbox.com.
  • Verification emails not arriving: set RESEND_API_KEY and EMAIL_FROM. Without them the API logs the verification URL to stdout; run docker compose logs api | grep '\[email:console\]'.

On this page