Manual Docker Compose setup
Manual setup guide for Rybbit using Docker Compose without the setup script
This guide is for users who want to manually set up Rybbit using Docker Compose without the setup script. This is useful if you already have a reverse proxy setup (like Nginx, Traefik, or Coolify), or if you want more control over the configuration.
Prerequisites
- Docker and Docker Compose installed
- A domain name pointed to your server
- Basic knowledge of Docker and environment variables
Setup Steps
Clone the Repository
git clone https://github.com/rybbit-io/rybbit.git
cd rybbitCreate Environment File
Create a .env file in the root directory with the following content:
# Required: Your domain and base URL
DOMAIN_NAME=your.domain.com
BASE_URL=https://your.domain.com
# Required: Authentication secret (generate a random 32+ character string)
BETTER_AUTH_SECRET=your-very-long-random-secret-string-here
# Optional: Disable new user signups after creating admin account
DISABLE_SIGNUP=false
# Optional but recommended: Mapbox token for globe visualizations
MAPBOX_TOKEN=your_mapbox_token
# Optional: Custom host ports (defaults bind to localhost only)
# HOST_BACKEND_PORT="127.0.0.1:3001:3001"
# HOST_CLIENT_PORT="127.0.0.1:3002:3002"
# Optional: Database credentials (defaults work, but change them for production)
# CLICKHOUSE_PASSWORD=frog
# POSTGRES_USER=frog
# POSTGRES_PASSWORD=frog
# POSTGRES_DB=analytics
# CLICKHOUSE_DB=analytics
# REDIS_PASSWORD=changemeFor the other settings Docker Compose reads, see Environment variables.
To generate a secure BETTER_AUTH_SECRET, you can use:
openssl rand -hex 32Choose Your Setup Method
If you don't have a reverse proxy and want automatic SSL certificates, start the stack with the with-webserver profile, which adds the Caddy service:
docker compose --profile with-webserver up -dThis will:
- Start all services including Caddy
- Automatically obtain SSL certificates
- Make your app available at
https://your.domain.com
If you have your own reverse proxy (Nginx, Traefik, Coolify, etc.), start the stack without the with-webserver profile. Caddy only runs when that profile is enabled:
docker compose up -dBy default the backend and client are published on 127.0.0.1:3001 and 127.0.0.1:3002, so a reverse proxy running on the same host can reach them and nothing else can. You don't need to change any ports for that setup.
If your proxy runs on another machine, publish the ports on all interfaces in .env:
HOST_BACKEND_PORT="3001:3001"
HOST_CLIENT_PORT="3002:3002"This exposes the backend and client over plain HTTP to anyone who can reach the server. Restrict access with a firewall at your hosting provider. Ports published by Docker bypass host firewalls such as ufw.
To change ports with a docker-compose.override.yml file instead, use the !override tag, which needs Docker Compose 2.24.4 or later. Without it, Docker Compose adds your ports to the default 127.0.0.1 ones instead of replacing them:
services:
backend:
ports: !override
- "127.0.0.1:8080:3001"
client:
ports: !override
- "127.0.0.1:8081:3002"Configure Your Reverse Proxy
If you're using your own reverse proxy, configure it to:
- Proxy requests to
/api/*tohttp://localhost:3001 - Proxy requests to
/.well-known/oauth-*and/.well-known/openid-configuration*tohttp://localhost:3001. MCP clients use these to discover OAuth login. - Proxy all other requests to
http://localhost:3002
Example configurations:
server {
listen 80;
server_name your.domain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name your.domain.com;
# Your SSL configuration here
location /api/ {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# OAuth discovery for MCP clients
location ~ ^/\.well-known/(oauth-|openid-configuration) {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://localhost:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}services:
backend:
labels:
- "traefik.enable=true"
- "traefik.http.routers.rybbit-api.rule=Host(`your.domain.com`) && (PathPrefix(`/api`) || PathPrefix(`/.well-known/oauth-`) || PathPrefix(`/.well-known/openid-configuration`))"
- "traefik.http.routers.rybbit-api.tls.certresolver=letsencrypt"
- "traefik.http.services.rybbit-api.loadbalancer.server.port=3001"
client:
labels:
- "traefik.enable=true"
- "traefik.http.routers.rybbit-client.rule=Host(`your.domain.com`)"
- "traefik.http.routers.rybbit-client.tls.certresolver=letsencrypt"
- "traefik.http.services.rybbit-client.loadbalancer.server.port=3002"Start the Services
# With the built-in Caddy webserver
docker compose --profile with-webserver up -d
# Without Caddy (your own reverse proxy)
docker compose up -dVerify Setup
Check that all services are running:
docker compose psMonitor logs:
docker compose logs -fCreate Admin Account
Navigate to https://your.domain.com/signup and create your admin account.
Service Architecture
Rybbit consists of these services:
- client: Next.js frontend (port 3002)
- backend: Node.js API server (port 3001)
- postgres: User data and configuration
- clickhouse: Analytics data storage
- redis: Shared state for session tracking, visitor identity, bot detection counters, rate limits, the dashboard cache, and session replay and import budgets
- caddy: Reverse proxy with automatic SSL (optional)
Common Configurations
Using Custom Ports
If you need different host ports (e.g., if 3001/3002 are already in use):
HOST_BACKEND_PORT="8080:3001"
HOST_CLIENT_PORT="8081:3002"Using with Coolify
Since Coolify uses Traefik instead of Caddy as proxy, some modifications have to be made in a docker-compose.yaml file to avoid conflicts. Specifically, you need to:
- remove Caddy service and volumes
- add network labels to all services
- remove
buildandportsproperties from backend and client services
Here's a modified version of docker-compose that works on Coolify:
services:
clickhouse:
container_name: clickhouse
image: clickhouse/clickhouse-server:26.3.62.3
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
labels:
- traefik.enable=false
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
labels:
- traefik.enable=false
redis:
image: redis:8.6.4-alpine
container_name: redis
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
command:
- redis-server
- --requirepass
- ${REDIS_PASSWORD:-changeme}
- --appendonly
- "yes"
- --appendfsync
- everysec
- --maxmemory-policy
- noeviction
labels:
- traefik.enable=false
backend:
image: ghcr.io/rybbit-io/rybbit-backend:${IMAGE_TAG:-latest}
container_name: backend
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}
- DOMAIN_NAME=${DOMAIN_NAME}
- DISABLE_SIGNUP=${DISABLE_SIGNUP}
- DISABLE_TELEMETRY=${DISABLE_TELEMETRY}
- MAPBOX_TOKEN=${MAPBOX_TOKEN}
- CLUSTER_WORKERS=${CLUSTER_WORKERS:-4}
- DASHBOARD_CACHE_TTL_SECONDS=${DASHBOARD_CACHE_TTL_SECONDS:-30}
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
labels:
- traefik.enable=true
- traefik.docker.network=coolify
- traefik.http.routers.rybbit-backend.entrypoints=https
- traefik.http.routers.rybbit-backend.tls=true
- traefik.http.routers.rybbit-backend.rule=Host(`${DOMAIN_NAME}`) && (Path(`/api`) || PathPrefix(`/api/`) || PathPrefix(`/.well-known/oauth-`) || PathPrefix(`/.well-known/openid-configuration`))
- traefik.http.services.rybbit-backend.loadbalancer.server.port=3001
- traefik.http.routers.rybbit-backend.priority=100
- traefik.http.middlewares.rybbit-forward-headers.headers.customrequestheaders.X-Forwarded-Proto=https
- traefik.http.middlewares.rybbit-forward-headers.headers.customrequestheaders.X-Forwarded-Host=${DOMAIN_NAME}
- traefik.http.routers.rybbit-backend.middlewares=rybbit-forward-headers
client:
image: ghcr.io/rybbit-io/rybbit-client:${IMAGE_TAG:-latest}
container_name: client
environment:
- NODE_ENV=production
- NEXT_PUBLIC_BACKEND_URL=${BASE_URL}
- NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_SIGNUP}
- DOMAIN_NAME=${DOMAIN_NAME}
depends_on:
- backend
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.docker.network=coolify
- traefik.http.routers.rybbit-client.entrypoints=https
- traefik.http.routers.rybbit-client.tls=true
- traefik.http.routers.rybbit-client.rule=Host(`${DOMAIN_NAME}`)
- traefik.http.services.rybbit-client.loadbalancer.server.port=3002
- traefik.http.routers.rybbit-client.priority=10
volumes:
clickhouse-data:
postgres-data:
redis-data:
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>To deploy Rybbit on Coolify, you need to:
- Create a new project and resource using
docker-compose.yamlfile provided above - Make sure "Escape special characters in labels" option is unchecked when adding docker-compose file so that env variables in labels are read properly
- Set the environment variables in Coolify's interface
- Add a domain for
clientservice in Coolify's interface - Deploy all services
Note: you don't need to add a domain for backend service in Coolify since client and backend services share the same domain.
Using with Nginx Proxy Manager
The Nginx Proxy Manager guide connects NPM to Rybbit over a Docker network, so no ports are published. If you'd rather point NPM at host ports:
-
Set up port exposure in your .env (see the warning under Without Caddy about publishing on all interfaces):
HOST_BACKEND_PORT="3001:3001" HOST_CLIENT_PORT="3002:3002" -
Create a proxy host pointing to your server IP:3002
-
Add custom locations
/api,/.well-known/oauth-, and/.well-known/openid-configurationpointing to your server IP:3001
Database Persistence
Data is automatically persisted in Docker volumes:
rybbit_clickhouse-data: Analytics datarybbit_postgres-data: User accounts and site configurationsrybbit_redis-data: Active sessions and other short-lived state
Advanced Configurations
For specific reverse proxy setups, see our detailed guides:
- Custom Nginx Setup - Complete Nginx configuration with SSL
- Nginx Proxy Manager - NPM setup without port exposure
These guides provide step-by-step instructions for integrating Rybbit with your existing infrastructure.
Troubleshooting
Services won't start
# Check logs
docker compose logs
# Check service status
docker compose psPort conflicts
If you get port binding errors, either:
- Change the host ports in your .env file
- Stop the conflicting service
- Use docker-compose.override.yml to customize ports
Database connection issues
Ensure the database services are healthy:
docker compose psThe clickhouse, postgres, and redis services should show "healthy" status. The backend waits for ClickHouse and Redis to be healthy before it starts.
For more advanced configurations and setup script options, see our Advanced Self-Hosting Guide.