Advanced self-hosting
Advanced configuration options for self-hosting Rybbit
This guide covers advanced configuration options for self-hosting Rybbit.
Setup Script Options
The setup script supports several options:
./setup.sh <domain_name> [options]Available options:
--no-webserver: Disable the built-in Caddy webserver--backend-port <port>: Set custom host port for backend (default: 3001)--client-port <port>: Set custom host port for client (default: 3002)--mapbox-token <token>: Set Mapbox API token (optional but recommended for globe visualizations)--help: Show help message
Examples:
# With Mapbox token
./setup.sh tracking.example.com --mapbox-token YOUR_MAPBOX_TOKEN
# Custom ports with built-in webserver
./setup.sh tracking.example.com --backend-port 8080 --client-port 8081
# Custom ports with your own webserver
./setup.sh tracking.example.com --no-webserver --backend-port 8080 --client-port 8081When you specify custom ports, only the host port mapping changes. Inside the Docker containers, the services still use ports 3001 and 3002.
Using Your Own Web Server
If you prefer to use your own web server instead of the built-in Caddy server, you can use the --no-webserver flag:
./setup.sh your.domain.name --no-webserverThis will:
- Not start the Caddy container
- Expose the backend service on host port 3001 (or your custom port) on all interfaces
- Expose the client service on host port 3002 (or your custom port) on all interfaces
Both ports serve plain HTTP. If your web server runs on the same host, bind them to localhost by editing these lines in .env, then run docker compose up -d. If you set custom ports, keep your host port, e.g. 127.0.0.1:8080:3001:
HOST_BACKEND_PORT="127.0.0.1:3001:3001"
HOST_CLIENT_PORT="127.0.0.1:3002:3002"If your web server runs elsewhere, restrict the ports with your hosting provider's firewall. Ports published by Docker bypass host firewalls such as ufw.
For detailed configuration examples with Nginx, Traefik, NPM, and other reverse proxies, see our Manual Docker Compose Setup guide.
Environment Variables
The setup script creates a minimal .env file with only the essential variables:
DOMAIN_NAME=your.domain.com
BASE_URL=https://your.domain.com
BETTER_AUTH_SECRET=generated_secret
DISABLE_SIGNUP=falseOptional variables that can be added:
# Mapbox token for 3D map visualizations
MAPBOX_TOKEN=your_mapbox_token
# Database configuration (uses defaults if not specified)
CLICKHOUSE_PASSWORD=frog
POSTGRES_USER=frog
POSTGRES_PASSWORD=frog
POSTGRES_DB=analytics
CLICKHOUSE_DB=analytics
REDIS_PASSWORD=changeme
# Image tag to run: latest, a release like v2.9.2, edge, or sha-<commit>
IMAGE_TAG=latest
# Port mapping (only needed for custom ports or --no-webserver)
HOST_BACKEND_PORT="127.0.0.1:3001:3001"
HOST_CLIENT_PORT="127.0.0.1:3002:3002"setup.sh doesn't generate database or Redis passwords, so the defaults above apply until you set your own. Set them before the first start: Postgres only reads POSTGRES_USER and POSTGRES_PASSWORD when it creates its data volume. See Image tags for what each IMAGE_TAG value points to.
docker-compose.yml also passes these to the backend:
| Variable | Default | What it does |
|---|---|---|
CLUSTER_WORKERS | 4 | Number of backend worker processes. 0 runs a single process. |
DASHBOARD_CACHE_TTL_SECONDS | 30 | How long dashboard query results are cached in Redis, in seconds. The maximum is 30; 0 disables the cache. Not in 2.9.2; applies to edge images and the next release. |
CLICKHOUSE_QUERY_PASSWORD | CLICKHOUSE_PASSWORD | Password for the read-only rybbit_query ClickHouse user the backend creates for custom SQL queries, such as the MCP run_query tool. |
DISABLE_TELEMETRY | unset | Set to true to turn off anonymous usage telemetry. |
OPENROUTER_API_KEY, OPENROUTER_MODEL | unset | An OpenRouter key and model for generating custom SQL queries from plain-language questions. |
LITE_DASHBOARD | unset | For very high-traffic installs. Set to true and the backend keeps hourly rollup tables and serves the site list from them. The rollups only see new events: on an install that already has data, run docker compose run --rm --no-deps backend node dist/scripts/backfillLiteDashboardMVs.js --cutoff "<YYYY-MM-DD HH:MM:SS>" once, with the cutoff at the moment you enabled it. |
DEPLOYMENT | unset | Shows the Query and Dashboards pages and the admin panel in the client. |
LITE_DASHBOARD and DEPLOYMENT are compiled into the client when its image is built. The prebuilt images are built without them, so with prebuilt images LITE_DASHBOARD only changes the backend and DEPLOYMENT does nothing. To use them fully, build the images yourself with docker compose build after setting them in .env.
Variables that aren't in docker-compose.yml
The backend's environment list in docker-compose.yml is explicit, so a variable you add to .env only reaches the backend if that list mentions it. To set anything else, add it in a docker-compose.override.yml file next to docker-compose.yml. Docker Compose loads it automatically and merges its environment entries with the main file:
services:
backend:
environment:
- LOG_LEVEL=warn
- SESSION_REPLAY_MAX_MB_PER_SESSION=100Then run your usual start command (for example docker compose --profile with-webserver up -d) to recreate the backend. Variables you might set this way:
| Variable | Default | What it does |
|---|---|---|
SESSION_REPLAY_MAX_MB_PER_SESSION | 50 | Stops storing replay data for a session once it reaches this many MB. 0 removes the cap. Not in 2.9.2; applies to edge images and the next release. |
LOG_LEVEL | info | Backend log level, such as debug, info, warn, or error. |
DISABLE_STICKY_IDENTITY | unset | Set to true to stop keeping a visitor's ID stable when their IP address rotates during a visit. |
DISABLE_REDIS_ANOMALY | unset | Set to true to keep bot-detection rate counters in each process instead of Redis. |
update.sh leaves docker-compose.override.yml in place, while it stashes edits made directly to docker-compose.yml.
ClickHouse Timezone
This section applies to the first release after 2.9.2 and to edge images. The current latest images (2.9.2) don't include the UTC change yet.
Rybbit stores every timestamp as a UTC instant and converts to your dashboard timezone when it queries. Every time column is declared DateTime('UTC'), and the backend writes timestamps with an explicit UTC offset, so the ClickHouse server's own <timezone> setting does not change what gets stored. Leave it at the default (UTC) unless you have a reason not to; the backend logs a warning at startup if it is anything else.
Upgrading from 2.9.2 or earlier on a non-UTC ClickHouse
Before this change the backend wrote timezone-less strings, which ClickHouse read in the server's timezone. If your ClickHouse was configured for anything but UTC, two things need attention after upgrading:
-
Month partitions. Existing rows keep the month partition computed in the old timezone. The backend converts the columns to UTC on startup without rewriting data, and afterwards rows that fall within the old offset of a month boundary (for Europe/Berlin, the last one or two hours of each month) sit in a partition ClickHouse no longer expects. A query whose whole time window falls inside those hours will miss them. To fix it, stop the backend (
docker compose stop backend) and rebuild each partitioned table with a copy and swap. Forevents:-- How many rows are affected (0 means there is nothing to do): SELECT count() FROM events WHERE _partition_id != toString(toYYYYMM(timestamp, 'UTC')); CREATE TABLE events_rebuilt AS events; INSERT INTO events_rebuilt SELECT * FROM events; -- Check both tables have the same count(), then: EXCHANGE TABLES events AND events_rebuilt; DROP TABLE events_rebuilt; -- once you are satisfiedRepeat for
bot_eventsandbot_observations(partitioned ontimestamp), andsession_replay_events(timestamp),session_replay_metadataandsession_replay_metadata_v2(start_time). WithLITE_DASHBOARD=true, do the same forsessions_mv_target(start_time) and the*_hourly_mv_targettables (event_hour);session_hourly_mv_targetis rebuilt by its view every hour and can be skipped. OnReplacingMergeTreeandAggregatingMergeTreetables comparecount()withFINAL, since background merges can change the raw count. Materialized views stay attached to the swapped-in table. The copy rewrites every row, so run it off-peak. Step 2's copy also re-partitions every row it writes, so a table you fix there can skip this step. Start the backend when you are done, unless you continue with step 2. -
Shifted timestamps. Keep the backend stopped for this step too: any event that arrives between the copy and the swap ends up only in the table you drop. Rows written by rybbit-backend 2.9.0, 2.9.1, or 2.9.2 while ClickHouse was not in UTC were stored shifted by the server's offset (the dashboard looked frozen at the upgrade time). Rows written by 2.8.0 or earlier were shifted only if the backend container's
TZdiffered from the ClickHouse timezone. Rybbit cannot correct these automatically: a shifted row and a correct row can carry the same timestamp, so only you know which rows the old backend wrote. If you want them fixed, use the same copy-and-swap on theeventstable with your own bounds, then rebuild the rollups from it:CREATE TABLE events_fixed AS events; INSERT INTO events_fixed SELECT * REPLACE ( -- Re-read the stored wall-clock, rendered in the old server timezone, as UTC. if(<written by the old backend>, toDateTime(toString(toTimeZone(timestamp, 'Europe/Berlin')), 'UTC'), timestamp) AS timestamp, if(<written by the old backend>, toDateTime64(toString(toTimeZone(timestamp_ms, 'Europe/Berlin')), 3, 'UTC'), timestamp_ms) AS timestamp_ms ) FROM events; -- Check counts and a few known rows, then: EXCHANGE TABLES events AND events_fixed;Replace
<written by the old backend>with a predicate that is true only for rows the old backend wrote, for example atimestamprange that ends before the upgraded backend started, minus the server offset. Run it once: applying it twice shifts rows twice. Do it before importing historical data from another tool. Afterwards, if you run withLITE_DASHBOARD=true, rebuild its rollups withdocker compose run --rm --no-deps backend node dist/scripts/backfillLiteDashboardMVs.js --truncate(the rollup tables only exist with that setting). Dropevents_fixed, and only then start the backend. The same applies tobot_events,bot_observationsand the twosession_replay_metadatatables; replay event timestamps were always written as epoch numbers and are correct as they are.