Rybbit has grown from a simple web analytics alternative into a broad web and product analytics stack.
It still covers the basics: sessions, users, pageviews, referrers, channels, devices, countries, and realtime activity. The current v2.x line also adds deeper product workflows: session replay, journeys, funnels, goals, custom events with JSON properties, error tracking, public dashboards, organizations, teams, and feature flags.
What is Rybbit?
Rybbit is an open-source, privacy-friendly analytics platform for websites and products.
Rybbit is a good fit when you want a Google Analytics replacement that you can run yourself, but you also want product analytics features that are usually split across several tools.
The repository includes the main web app, backend, shared package, docs app, React Native tracking package, ClickHouse migration notes, and backup tooling.
Why Self-Host Rybbit?
- Own your analytics data: Events are stored in your ClickHouse/Postgres stack.
- Avoid cookie-heavy tracking: Rybbit is built around privacy-friendly analytics.
- Track product workflows: Funnels, journeys, goals, sessions, users, and replays live in the same UI.
- Use one stack: Next.js client, Fastify backend, ClickHouse, Postgres, and Redis are all covered by Docker Compose.
Tech Overview of Rybbit
The current application is a TypeScript-heavy monorepo.
The frontend is a Next.js 16 app running on port 3002. It uses React 19, Tailwind, Radix UI, TanStack Query/Table/Virtual, Nivo, D3, Mapbox GL, rrweb-player, Zustand, Jotai, and Better Auth client pieces.
The backend is a Fastify 5 service running on port 3001. It uses Drizzle ORM with Postgres for application metadata, ClickHouse for analytics events and rollups, Redis for sessions/identity/rate-limit state, Better Auth for authentication and organizations, and Puppeteer/Chromium for PDF reporting.
Architecture & Components
- Client: Authenticated analytics UI, site dashboards, reports, organization settings, session replay player, funnels, journeys, goals, maps, and import tools.
- Backend: Fastify API, tracking endpoint, analytics queries, auth routes, MCP routes, Google Search Console integration, Stripe/AppSumo cloud-only paths, PDF reports, imports, and admin routes.
- ClickHouse: Raw events, bot events, session replay events, replay metadata, materialized views, and hourly rollups.
- Postgres: Users, organizations, teams, sites, dashboards, goals, funnels, feature flags, experiments, API keys, invitations, imports, and account/auth tables.
- Redis: Session tracking, identity resolution, API rate-limit state, and durable session-adjacent state with AOF enabled in the official compose file.
- Shared package: Types and shared dashboard config consumed by client/server.
Key Technologies
- Frontend: Next.js, React, Tailwind, Radix UI, Nivo, D3, Mapbox GL.
- Backend: Fastify, TypeScript, Drizzle, Better Auth, Pino, Puppeteer.
- Databases: ClickHouse for high-volume analytics, Postgres for application metadata.
- Cache/state: Redis with password auth and AOF persistence.
- Testing: Vitest tests live close to source files across analytics, auth, CORS, ClickHouse, imports, sessions, feature flags, and frontend utilities.
Self-Hosting Rybbit with Docker
Rybbit ships official images on GHCR and an official docker-compose.yml.
The current repo compose uses:
ghcr.io/rybbit-io/rybbit-backend:v2.8.0ghcr.io/rybbit-io/rybbit-client:v2.8.0clickhouse/clickhouse-server:26.3.17.4postgres:17.4redis:8.6.4-alpine- Optional Caddy profile for ports
80and443
Pre-Requisites - Docker! 🐋
Install Docker on your system before proceeding:
- Linux: Official Docker Engine install guide
- Windows / Mac: Docker Desktop
Verify installation: docker --version && docker compose version
Docker Compose Configuration
I keep the reusable compose in the public Home-Lab layout:
Home-Lab Rybbit configThe site snippet uses the same compose file with pinned Rybbit v2.8.0 images:
services:
clickhouse:
image: clickhouse/clickhouse-server:26.3.17.4
restart: unless-stopped
environment:
CLICKHOUSE_DB: ${CLICKHOUSE_DB:?Set CLICKHOUSE_DB in .env}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:?Set CLICKHOUSE_USER in .env}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in .env}
volumes:
- clickhouse-data:/var/lib/clickhouse
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
interval: 3s
timeout: 5s
retries: 10
start_period: 10s
postgres:
image: postgres:17.4
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:?Set POSTGRES_DB in .env}
POSTGRES_USER: ${POSTGRES_USER:?Set POSTGRES_USER in .env}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 3s
timeout: 5s
retries: 10
start_period: 10s
redis:
image: redis:8.6.4-alpine
restart: unless-stopped
command:
- redis-server
- --requirepass
- ${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}
- --appendonly
- "yes"
- --appendfsync
- everysec
- --maxmemory-policy
- noeviction
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}", "--no-auth-warning", "ping"]
interval: 3s
timeout: 5s
retries: 10
start_period: 5s
backend:
image: ghcr.io/rybbit-io/rybbit-backend:v2.8.0
restart: unless-stopped
environment:
NODE_ENV: production
CLICKHOUSE_HOST: http://clickhouse:8123
CLICKHOUSE_DB: ${CLICKHOUSE_DB:?Set CLICKHOUSE_DB in .env}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:?Set CLICKHOUSE_USER in .env}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?Set CLICKHOUSE_PASSWORD in .env}
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
POSTGRES_DB: ${POSTGRES_DB:?Set POSTGRES_DB in .env}
POSTGRES_USER: ${POSTGRES_USER:?Set POSTGRES_USER in .env}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: ${REDIS_PASSWORD:?Set REDIS_PASSWORD in .env}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in .env}
BASE_URL: ${BASE_URL:?Set BASE_URL in .env}
DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY:-true}
CLUSTER_WORKERS: ${CLUSTER_WORKERS:-0}
DEPLOYMENT: self-hosted
LITE_DASHBOARD: ${LITE_DASHBOARD:-false}
MAPBOX_TOKEN: ${MAPBOX_TOKEN:-}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-}
OPENROUTER_MODEL: ${OPENROUTER_MODEL:-}
ports:
- "${RYBBIT_BACKEND_PORT:-3001}:3001"
depends_on:
clickhouse:
condition: service_healthy
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3001/api/health"]
interval: 3s
timeout: 5s
retries: 10
start_period: 15s
client:
image: ghcr.io/rybbit-io/rybbit-client:v2.8.0
restart: unless-stopped
environment:
NODE_ENV: production
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:?Set NEXT_PUBLIC_BACKEND_URL in .env}
NEXT_PUBLIC_DISABLE_SIGNUP: ${DISABLE_SIGNUP:-false}
NEXT_PUBLIC_DEPLOYMENT: self-hosted
NEXT_PUBLIC_LITE_DASHBOARD: ${LITE_DASHBOARD:-false}
ports:
- "${RYBBIT_CLIENT_PORT:-3002}:3002"
depends_on:
backend:
condition: service_healthy
volumes:
clickhouse-data:
postgres-data:
redis-data:
For production, change every secret, keep database ports private, put the app behind HTTPS, and set BASE_URL to the real public URL.
Example .env:
BASE_URL=http://localhost:3002
NEXT_PUBLIC_BACKEND_URL=http://localhost:3001
DISABLE_SIGNUP=false
DISABLE_TELEMETRY=true
CLUSTER_WORKERS=0
CLICKHOUSE_DB=analytics
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=<long-random-password>
POSTGRES_DB=analytics
POSTGRES_USER=rybbit
POSTGRES_PASSWORD=<long-random-password>
REDIS_PASSWORD=<long-random-password>
BETTER_AUTH_SECRET=<long-random-secret>
Start it:
docker compose config
docker compose up -d
Open:
http://localhost:3002
The backend health endpoint is:
http://localhost:3001/api/health
Field Note - Local Docker Trial
I tested Rybbit locally on 2026-08-27 while other containers, including a Chartbrew trial, were already running.
To avoid collisions, I used an isolated Compose project named rybbit_foss_trial, pinned the Rybbit images to v2.8.0, avoided the official fixed container names, and remapped ports:
- UI:
http://localhost:4522 - API:
http://localhost:4521 - ClickHouse HTTP:
8124 - ClickHouse native:
9002 - Postgres:
5439 - Redis:
6390
Because this Docker daemon had already exhausted the default network address pools, the trial used Docker’s existing bridge network and connected backend-to-dependencies through host.docker.internal.
The trial succeeded:
GET http://localhost:4521/api/health -> OK
HEAD http://localhost:4522/ -> HTTP/1.1 200 OK
Backend logs showed Drizzle migrations applied successfully, Redis connected, GeoIP databases loaded, and Fastify listening on 3001.
Idle memory during the trial was roughly:
- Client: 76 MiB
- Backend: 260 MiB
- ClickHouse: 202 MiB
- Postgres: 28 MiB
- Redis: 6 MiB
The trial was available at http://localhost:4522 during inspection. It is not assumed to still be running later.
Security and Operations Notes
Rybbit is analytics infrastructure, so treat the deployment carefully:
- Use a real random
BETTER_AUTH_SECRET; Better Auth warned when the trial used a short placeholder. - Disable public signup after your admin/user setup if the instance is private.
- Keep ClickHouse, Postgres, and Redis private to the host or internal network.
- Use HTTPS in production and set
BASE_URLto the exact public URL. - Back up both Postgres and ClickHouse. The repo includes backup material under
ops/backups/. - Review AGPL obligations before offering a modified hosted version.
Conclusion
Rybbit is a strong option when you want one self-hosted analytics app to cover both traffic analytics and product behavior.
It is broader than lightweight pageview counters because it includes journeys, funnels, session replay, errors, public dashboards, organizations, teams, imports, API keys, and feature flags. It is also more focused than a general BI platform: the default product experience is for understanding websites and apps quickly.
For alternatives, compare it with Chartbrew if you want dashboards over arbitrary databases/APIs, Rill if you prefer code-first BI on DuckDB, and WrenAI if you want natural-language analytics over SQL data.
FAQ
Is Rybbit open source?
Yes. Rybbit is licensed under AGPL-3.0.
That is a strong copyleft license designed for network services. If you modify and offer it over a network, review the license obligations carefully.
What is the current Rybbit stack?
Does Rybbit need both ClickHouse and Postgres?
Can I run it without Caddy?
Yes for local testing or when you already have a reverse proxy.
The official compose file has a Caddy service under the with-webserver profile. Without that profile, you can expose the client/backend directly or put them behind Traefik, Nginx Proxy Manager, Caddy, Pangolin, Cloudflare Tunnel, or another reverse proxy.
What should I check after first boot?
Confirm BASE_URL and NEXT_PUBLIC_BACKEND_URL, create the first user, add a site, copy the tracking script, and send a test pageview.
Then decide whether public signup should stay enabled.
Comments