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.0
  • ghcr.io/rybbit-io/rybbit-client:v2.8.0
  • clickhouse/clickhouse-server:26.3.17.4
  • postgres:17.4
  • redis:8.6.4-alpine
  • Optional Caddy profile for ports 80 and 443

Docker Compose Configuration

I keep the reusable compose in the public Home-Lab layout:

Home-Lab Rybbit config

The 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

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_URL to 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