Rybbit - open-source and privacy-friendly alternative to Google Analytics that is 10x more intuitive.
docker pull clickhouse/clickhouse-server
docker run -d --name rybbit -p 80:80 --restart always clickhouse/clickhouse-server:26.3.17.4
podman run -d --name rybbit -p 80:80 --restart always clickhouse/clickhouse-server:26.3.17.4
skopeo inspect docker://docker.io/clickhouse/clickhouse-server:26.3.17.4
oras pull clickhouse/clickhouse-server:26.3.17.4
docker compose up -d
services:
caddy:
profiles: ["with-webserver"]
image: caddy:2.10.0
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp" # Needed for HTTP/3
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile # Mount Caddy config file
- caddy_data:/data # Mount persistent data volume for certs etc.
- caddy_config:/config # Mount persistent config volume
environment:
# Pass domain name for use in Caddyfile
# Email is configured via Caddyfile global options
- DOMAIN_NAME=${DOMAIN_NAME}
depends_on:
- backend
- client
clickhouse:
container_name: clickhouse
image: clickhouse/clickhouse-server:26.3.17.4
volumes:
- clickhouse-data:/var/lib/clickhouse
configs:
- source: clickhouse_network
target: /etc/clickhouse-server/config.d/network.xml
- source: clickhouse_logging
target: /etc/clickhouse-server/config.d/logging_rules.xml
- source: clickhouse_resource_limits
target: /etc/clickhouse-server/config.d/resource_limits.xml
# Profile (user-level) settings are only read from users.d, not config.d
- source: clickhouse_user_settings
target: /etc/clickhouse-server/users.d/user_settings.xml
environment:
- CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}
- CLICKHOUSE_USER=${CLICKHOUSE_USER:-default}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-frog}
# Lets the backend provision the least-privilege `rybbit_query` user
# (custom SQL queries / dashboards) with SQL at startup.
- CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
interval: 3s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
ports:
# Localhost-only: reach from outside via SSH tunnel (ssh -N -L 8123:localhost:8123 <host>)
- "127.0.0.1:8123:8123"
postgres:
image: postgres:17.4
container_name: postgres
environment:
- POSTGRES_USER=${POSTGRES_USER:-frog}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-frog}
- POSTGRES_DB=${POSTGRES_DB:-analytics}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 3s
timeout: 5s
retries: 5
start_period: 10s
restart: unless-stopped
ports:
# Localhost-only: reach from outside via SSH tunnel
- "127.0.0.1:5432:5432"
redis:
image: redis:8.6.4-alpine
container_name: redis
ports:
# Localhost-only: reach from outside via SSH tunnel
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-changeme}", "--no-auth-warning", "ping"]
interval: 3s
timeout: 5s
retries: 5
start_period: 5s
restart: unless-stopped
# Backs session tracking, sticky identity, and bot-anomaly counters.
# noeviction keeps those keys from being dropped under memory pressure;
# session keys carry a 30-minute TTL so memory stays bounded by active-user
# count. AOF (everysec) keeps that state durable across restarts.
command:
- redis-server
- --requirepass
- ${REDIS_PASSWORD:-changeme}
- --appendonly
- "yes"
- --appendfsync
- everysec
- --maxmemory-policy
- noeviction
backend:
image: ghcr.io/rybbit-io/rybbit-backend:${IMAGE_TAG:-latest}
container_name: backend
build:
context: .
dockerfile: ./server/Dockerfile
ports:
- "${HOST_BACKEND_PORT:-127.0.0.1:3001:3001}"
environment:
- NODE_ENV=production
- CLICKHOUSE_HOST=http://clickhouse:8123
- CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-frog}
- CLICKHOUSE_QUERY_PASSWORD=${CLICKHOUSE_QUERY_PASSWORD:-${CLICKHOUSE_PASSWORD:-frog}}
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_DB=${POSTGRES_DB:-analytics}
- POSTGRES_USER=${POSTGRES_USER:-frog}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-frog}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD:-changeme}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- BASE_URL=${BASE_URL}
- DISABLE_SIGNUP=${DISABLE_SIGNUP}
- DISABLE_TELEMETRY=${DISABLE_TELEMETRY}
- MAPBOX_TOKEN=${MAPBOX_TOKEN}
- CLUSTER_WORKERS=${CLUSTER_WORKERS:-4}
- DEPLOYMENT=${DEPLOYMENT}
- LITE_DASHBOARD=${LITE_DASHBOARD}
- DASHBOARD_CACHE_TTL_SECONDS=${DASHBOARD_CACHE_TTL_SECONDS:-30}
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
- OPENROUTER_MODEL=${OPENROUTER_MODEL}
depends_on:
clickhouse:
condition: service_healthy
postgres:
condition: service_started
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: 5
start_period: 10s
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
client:
image: ghcr.io/rybbit-io/rybbit-client:${IMAGE_TAG:-latest}
container_name: client
build:
context: .
dockerfile: ./client/Dockerfile
args:
NEXT_PUBLIC_BACKEND_URL: ${BASE_URL}
NEXT_PUBLIC_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
NEXT_PUBLIC_DEPLOYMENT: ${DEPLOYMENT}
NEXT_PUBLIC_LITE_DASHBOARD: ${LITE_DASHBOARD}
ports:
- "${HOST_CLIENT_PORT:-127.0.0.1:3002:3002}"
environment:
- NODE_ENV=production
- NEXT_PUBLIC_BACKEND_URL=${BASE_URL}
- NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_SIGNUP}
- NEXT_PUBLIC_DEPLOYMENT=${DEPLOYMENT}
- NEXT_PUBLIC_LITE_DASHBOARD=${LITE_DASHBOARD}
depends_on:
- backend
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
volumes:
clickhouse-data:
postgres-data:
redis-data:
caddy_data: # Persistent volume for Caddy's certificates and state
caddy_config: # Persistent volume for Caddy's configuration cache (optional but good practice)
configs:
clickhouse_network:
content: |
<clickhouse>
<listen_host>0.0.0.0</listen_host>
</clickhouse>
clickhouse_logging:
content: |
<clickhouse>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<query_thread_log remove="remove"/>
<query_log remove="remove"/>
<query_views_log remove="remove"/>
<query_metric_log remove="remove"/>
<error_log remove="remove"/>
<opentelemetry_span_log remove="remove"/>
<text_log remove="remove"/>
<trace_log remove="remove"/>
<metric_log remove="remove"/>
<asynchronous_metric_log remove="remove"/>
<session_log remove="remove"/>
<part_log remove="remove"/>
<latency_log remove="remove"/>
<processors_profile_log remove="remove"/>
</clickhouse>
clickhouse_resource_limits:
content: |
<clickhouse>
<!-- Leave headroom for the backend, Redis, Caddy, the kernel, and the
filesystem cache instead of allowing ClickHouse to exhaust RAM. -->
<max_server_memory_usage_to_ram_ratio>0.80</max_server_memory_usage_to_ram_ratio>
<!-- Share at most one query-processing thread per CPU core across all
concurrent queries. This throttles work rather than rejecting it. -->
<concurrent_threads_soft_limit_ratio_to_cores>1</concurrent_threads_soft_limit_ratio_to_cores>
<!-- Bound memory used by background merges and mutations separately. -->
<merges_mutations_memory_usage_to_ram_ratio>0.25</merges_mutations_memory_usage_to_ram_ratio>
</clickhouse>
clickhouse_user_settings:
content: |
<clickhouse>
<profiles>
<default>
<enable_json_type>1</enable_json_type>
<!-- Combine concurrent inserts into larger blocks inside ClickHouse.
Wait for the flush so the backend still receives insert errors. -->
<async_insert>1</async_insert>
<wait_for_async_insert>1</wait_for_async_insert>
<log_queries>0</log_queries>
<log_query_threads>0</log_query_threads>
<log_processors_profiles>0</log_processors_profiles>
<!-- Bound individual queries while preserving enough parallelism
for large analytics scans. Query-count limits remain unset so
ingestion is never rejected by admission control. -->
<max_memory_usage>32000000000</max_memory_usage>
<max_threads>16</max_threads>
</default>
</profiles>
</clickhouse>