Self-Hosting Guides

Pangolin setup

Set up Rybbit with Pangolin as a self-hosted reverse proxy using Newt

This guide shows you how to set up Rybbit with Pangolin, a self-hosted reverse proxy, using a Newt container inside the Docker Compose stack. This approach avoids exposing ports directly on your host network.

Prerequisites

  • Pangolin already running and accessible (e.g. https://pangolin.yourdomain.com)
  • A Newt ID and Newt Secret from your Pangolin instance
  • A domain name pointed to your Pangolin server (e.g. tracking.yourdomain.com)
  • Docker and Docker Compose installed

You must generate a proper BETTER_AUTH_SECRET before starting. Without a valid 32+ character secret, you will not be able to create organizations or log in. Generate one with: openssl rand -hex 32

Setup Steps

Configure Environment Variables

Create a .env file in your Rybbit directory:

# Required: Your domain and base URL
DOMAIN_NAME=tracking.yourdomain.com
BASE_URL=https://tracking.yourdomain.com

# Required: Authentication secret (must be 32+ characters)
# Generate with: openssl rand -hex 32
BETTER_AUTH_SECRET=your_generated_secret_here

# Required: Disable built-in webserver since Pangolin handles routing
USE_WEBSERVER=false

# Database passwords: replace each with your own strong password
CLICKHOUSE_DB=analytics
CLICKHOUSE_PASSWORD=your_clickhouse_password

POSTGRES_DB=analytics
POSTGRES_USER=rybbit
POSTGRES_PASSWORD=your_postgres_password

REDIS_PASSWORD=your_redis_password

# Host port mappings in host_ip:host_port:container_port format
HOST_BACKEND_PORT=127.0.0.1:3001:3001
HOST_CLIENT_PORT=127.0.0.1:3002:3002

# Optional: Disable new user signups after creating admin account
# DISABLE_SIGNUP=true

# Optional: Mapbox token for globe visualizations
# MAPBOX_TOKEN=your_mapbox_token

Set the passwords before the first start: Postgres only reads POSTGRES_USER and POSTGRES_PASSWORD when it creates its data volume. If you reuse an existing postgres-data volume, keep the user and password it was created with.

Newt reaches the backend and client over the Docker network, so the host port mappings are only for access from the server itself. Binding them to 127.0.0.1 keeps them off the network. If ports 3001 or 3002 are already taken on your host, change the host port, e.g. 127.0.0.1:4001:3001. A value without an IP, such as 3001:3001, publishes the port on all interfaces.

Create Docker Compose File

Create a docker-compose.yml file in the same directory. This includes a Newt container that connects Pangolin to your Rybbit services:

services:
  newt:
    image: fosrl/newt
    container_name: newt-rybbit
    restart: unless-stopped
    environment:
      - PANGOLIN_ENDPOINT=https://pangolin.yourdomain.com
      - NEWT_ID=your_newt_id
      - NEWT_SECRET=your_newt_secret

  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
    ports:
      # Localhost-only: reach from outside via SSH tunnel
      - "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
    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
    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}
      - 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

  client:
    image: ghcr.io/rybbit-io/rybbit-client:${IMAGE_TAG:-latest}
    container_name: client
    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}
    depends_on:
      - backend
    restart: unless-stopped

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>

The newt service runs inside the same Docker network as your other containers. It connects to your Pangolin instance and routes traffic to backend and client by their container hostnames — no host port exposure needed for Pangolin to reach them.

Start Services

Start all services:

docker compose up -d

Verify everything is running:

docker compose ps

Check that the backend is healthy:

docker compose logs backend

Configure Pangolin

In your Pangolin dashboard, create a new site for your Rybbit domain and configure two resources:

  1. Client resource — routes the root domain to the Rybbit frontend:

    • Target: client (the Docker container hostname)
    • Port: 3002
  2. Backend resource — routes /api traffic to the Rybbit backend:

    • Target: backend (the Docker container hostname)
    • Port: 3001

    Route paths starting with /.well-known/oauth- and /.well-known/openid-configuration to the same backend target. MCP clients use them to discover OAuth login.

For both resources, add these custom headers in the Pangolin configuration:

HeaderValue
X-Forwarded-Protohttps
X-Forwarded-Hosttracking.yourdomain.com

Pangolin dashboard configuration

The X-Forwarded-Proto and X-Forwarded-Host headers are critical. Without them, API requests from the client to the backend will fail. This is the most common source of issues with this setup.

You do not need to enable "Advanced Mode" in Pangolin. The standard configuration with the correct headers is sufficient.

Access Rybbit

Navigate to https://tracking.yourdomain.com and create your admin account.

If you see 404 errors on /api routes, double-check that:

  • Your BETTER_AUTH_SECRET is set and is at least 32 characters
  • The X-Forwarded-Proto and X-Forwarded-Host headers are configured in Pangolin

Troubleshooting

Cannot create organizations or log in

This is almost always caused by a missing or invalid BETTER_AUTH_SECRET. Generate a proper secret:

openssl rand -hex 32

Add it to your .env file and restart the services:

docker compose down && docker compose up -d

404 errors on API routes

Verify that Pangolin is forwarding the correct headers. The X-Forwarded-Proto header must be https and X-Forwarded-Host must match your domain. Without these, the backend cannot properly handle requests.

Backend health check failing

Check the backend logs for errors:

docker compose logs -f backend

Ensure ClickHouse and PostgreSQL are healthy:

docker compose ps

Newt not connecting to Pangolin

Verify your Newt credentials and Pangolin endpoint:

docker compose logs -f newt

Make sure PANGOLIN_ENDPOINT, NEWT_ID, and NEWT_SECRET are correct in your docker-compose.yml.

Video Reference

For a visual walkthrough of setting up Rybbit with Pangolin, see the official Pangolin YouTube guide:

On this page