Self-Hosting Guides

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 rybbit

Create 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=changeme

For the other settings Docker Compose reads, see Environment variables.

To generate a secure BETTER_AUTH_SECRET, you can use:

openssl rand -hex 32

Choose 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 -d

This 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 -d

By 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/* to http://localhost:3001
  • Proxy requests to /.well-known/oauth-* and /.well-known/openid-configuration* to http://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 -d

Verify Setup

Check that all services are running:

docker compose ps

Monitor logs:

docker compose logs -f

Create 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 build and ports properties 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:

  1. Create a new project and resource using docker-compose.yaml file provided above
  2. Make sure "Escape special characters in labels" option is unchecked when adding docker-compose file so that env variables in labels are read properly
  3. Set the environment variables in Coolify's interface
  4. Add a domain for client service in Coolify's interface
  5. 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:

  1. 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"
  2. Create a proxy host pointing to your server IP:3002

  3. Add custom locations /api, /.well-known/oauth-, and /.well-known/openid-configuration pointing to your server IP:3001

Database Persistence

Data is automatically persisted in Docker volumes:

  • rybbit_clickhouse-data: Analytics data
  • rybbit_postgres-data: User accounts and site configurations
  • rybbit_redis-data: Active sessions and other short-lived state

Advanced Configurations

For specific reverse proxy setups, see our detailed guides:

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 ps

Port conflicts

If you get port binding errors, either:

  1. Change the host ports in your .env file
  2. Stop the conflicting service
  3. Use docker-compose.override.yml to customize ports

Database connection issues

Ensure the database services are healthy:

docker compose ps

The 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.

On this page