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)
- Expose the client service on host port 3002 (or your custom port)
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
# Custom image tags
IMAGE_TAG=latest
# Port mapping (only needed for custom ports or --no-webserver)
HOST_BACKEND_PORT="3001:3001"
HOST_CLIENT_PORT="3002:3002"ClickHouse Timezone
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.1 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 or 2.9.1 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.