In this guide I want to go over how I Was able to deploy Rybbit: the self-hosted analytics dashboard. This has been a major improvement in how I track and monitor viewership of my site. as well as who is connecting to my sites.

I use Rybbit to track all of my Next JS sites, and so far it has been working great. However as Rybbit has recently upgraded to version 2.4.0 I wanted to showcase how I achieved this, since others including myself at first had difficulty getting it all setup and working properly.

Where the Challenged began:

Originally I was having issues with being able to actually create an organization. I saw 404’s in the request logs for anything that was pointed to /api , so I concluded that my reverse proxy must be to blame. It was not. So I was troubleshooting the routes the project took and noticed that only within very recent versions did some api endpoints begin with /1/api rather than /api so my custom Pangolin regex re-write to /api wouldn’t work correctly.

Where I am a giant fucking dumbass

I mistakenly decided to worry about generating my production credentials after getting setup. This is wrong. Do not do this. The part where it wants you to generate a 32 character string is required to be able to create organizations. I assumed I could at least demo it out without making full time credentials but a quick generation of a 32 character string using openssl and suddenly I was able to log in, and create organizations.

There is an official YouTube video from Pangolin on how to deploy this, and what I used as a reference while going through it.

https://www.youtube.com/watch?v=zk-ZhkNdyC8

Working Configuration

Here is my environment variables

Code
DOMAIN_NAME=tracking.mydomain.me
BASE_URL=https://tracking.mydomain.me
BETTER_AUTH_SECRET=THISNEEDSTOBEA32CHARATERSTRINGVERYIMPORTANT
USE_WEBSERVER=false
CLICKHOUSE_DB=analytics
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=frog
POSTGRES_DB=analytics
POSTGRES_USER=frog
POSTGRES_PASSWORD=frog
HOST_BACKEND_PORT=3001:3001 # pangolin needs it like this 
HOST_CLIENT_PORT=3002:3002  # pangolin needs it like this 

For this do not worry if you already run services on ports 3001/3002 , this is only for the internal docker network, and not your host network, as you will see in the docker-compose.yml there is a newt we install within the docker network to give us access in


Here is the docker-compose.yml used.

Code
services:
  newt:
    image: fosrl/newt
    container_name: newt-rybbit
    restart: unless-stopped
    environment:
      - PANGOLIN_ENDPOINT=https://pangolin.mydomain.me
      - NEWT_ID=newtID
      - NEWT_SECRET=newtsecret
  clickhouse:
    container_name: clickhouse
    image: clickhouse/clickhouse-server:25.4.2
    volumes:
      - clickhouse-data:/var/lib/clickhouse
    configs:
      - source: clickhouse_network
        target: /etc/clickhouse-server/config.d/network.xml
      - source: clickhouse_json
        target: /etc/clickhouse-server/config.d/enable_json.xml
      - source: clickhouse_logging
        target: /etc/clickhouse-server/config.d/logging_rules.xml
      - source: clickhouse_user_logging
        target: /etc/clickhouse-server/config.d/user_logging.xml
    environment:
      - CLICKHOUSE_DB=${CLICKHOUSE_DB:-analytics}
      - CLICKHOUSE_USER=${CLICKHOUSE_USER:-default}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-frog}
    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:
      - "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:
      - "5432:5432"

  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}
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_DB=${POSTGRES_DB:-analytics}
      - POSTGRES_USER=${POSTGRES_USER:-frog}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-frog}
      - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
      - BASE_URL=${BASE_URL}
      - DISABLE_SIGNUP=${DISABLE_SIGNUP}
      - DISABLE_TELEMETRY=${DISABLE_TELEMETRY}
      - MAPBOX_TOKEN=${MAPBOX_TOKEN}
    depends_on:
      clickhouse:
        condition: service_healthy
      postgres:
        condition: service_started
    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:
  caddy_data:
  caddy_config:

configs:
  clickhouse_network:
    content: |
      <clickhouse>
          <listen_host>0.0.0.0</listen_host>
      </clickhouse>

  clickhouse_json:
    content: |
      <clickhouse>
          <settings>
              <enable_json_type>1</enable_json_type>
          </settings>
      </clickhouse>

  clickhouse_logging:
    content: |
      <clickhouse>
        <logger>
            <level>warning</level>
            <console>true</console>
        </logger>
        <query_thread_log remove="remove"/>
        <query_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_user_logging:
    content: |
      <clickhouse>
        <profiles>
          <default>
            <log_queries>0</log_queries>
            <log_query_threads>0</log_query_threads>
            <log_processors_profiles>0</log_processors_profiles>
          </default>
        </profiles>
      </clickhouse>

Important things to Note

  • Notice the hostname's of the docker containers for backend and client, we will address our docker containers by their hostname in the Pangolin configuration

Pangolin Configuration

Here is what needs to be set on Pangolin. Here take note of the lack of “Advanced Mode” this is actually not needed, I use it just fine with it disabled. What is important however is the Headers. Notice the HTTPS x-forwarded-for and the x-forwarded-host which is what allowed me to stop getting errors when communicating to the back end.

Wrapping up:

So you just need to follow the docker-compose along with the Pangolin config I shared, and you should be good, but if it doesn’t work I honestly do not know what to tell you, This was a pain getting setup, but I think the headers is really what solved the issues with back end communication. I have tried all the approaches using nginx-proxy manager, whatever else is recommended when self hosting and it all did not work. I also wish Pangolin themselves provided written documentation into how to do this.