How to Self-Host Plausible Analytics with Docker Compose and ClickHouse: Lightweight Privacy-First Web Analytics

Cloud-Engineer analysiert datenschutzfreundliche Web-Statistiken mit Plausible Analytics und ClickHouse
How to Self-Host Plausible Analytics with Docker Compose and ClickHouse: Lightweight Privacy-First Web Analytics 3

Modern web analytics is plagued by heavyweight tracking scripts, invasive cross-site profiling, and labyrinthine compliance requirements under GDPR, CCPA, and PECR. Traditional platforms like Google Analytics 4 (GA4) inject multi-megabyte scripts, demand intrusive cookie consent banners, and route sensitive user metadata through centralized third-party servers. For engineering teams and privacy-conscious operators, this represents a severe performance bottleneck and compliance liability.

Plausible Analytics provides a lightweight, open-source, and privacy-focused alternative. Its tracking script is under 1 KB—over 45 times smaller than GA4—and collects zero personal data, completely eliminating the legal requirement for cookie banners. Behind the scenes, Plausible pairs PostgreSQL for transactional data (sites, user accounts, and billing) with ClickHouse—an ultra-fast columnar analytical database capable of processing millions of event rows per second on minimal CPU and RAM.

Architecture & Data Pipeline

Understanding the interplay between Plausible’s microservices is essential for building a reliable, production-ready deployment:

+-------------------------------------------------------------------------------+
|                                CLIENT BROWSER                                 |
|         Fetches lightweight script (<1KB) & dispatches beacon POST             |
+-------------------------------------------------------------------------------+
                                      |
                           HTTPS (Port 443 / TLS)
                                      v
+-------------------------------------------------------------------------------+
|                       REVERSE PROXY (Caddy / Nginx / Traefik)                 |
|               Terminates TLS, sets X-Forwarded-For, proxies to :8000          |
+-------------------------------------------------------------------------------+
                                      |
                           Internal Bridge Network
                                      v
+-------------------------------------------------------------------------------+
|                       PLAUSIBLE ANALYTICS CORE ENGINE                         |
|                 (Elixir / Phoenix Application - Port 8000)                    |
|                                                                               |
|         +-----------------------+           +-----------------------+         |
|         |  PostgreSQL 16 Engine |           |  ClickHouse Database  |         |
|         |  Users, Sites, Goals  |           |  Raw Analytics Events |         |
|         |  (Transactional DB)   |           |  (Columnar Storage)   |         |
|         +-----------------------+           +-----------------------+         |
+-------------------------------------------------------------------------------+

Step 1: System Requirements & Host Preparation

ClickHouse is exceptionally efficient at analytical aggregation, but requires baseline system headroom during schema migrations and bulk writes. For moderate traffic (up to 1,000,000 monthly pageviews), the following baseline is recommended:

  • Compute: 2 vCPUs (x86_64 or ARM64)
  • Memory: 4 GB RAM (with 2 GB swap space)
  • Storage: 20 GB fast NVMe or SSD storage
  • Operating System: Ubuntu 24.04 LTS, Debian 12, or AlmaLinux 9

Create the project directory structure on your host machine:

sudo mkdir -p /opt/plausible/data/db-data
sudo mkdir -p /opt/plausible/data/event-data
sudo mkdir -p /opt/plausible/data/event-logs
sudo mkdir -p /opt/plausible/config

cd /opt/plausible

Step 2: Configuring ClickHouse Custom Parameters

Plausible requires custom ClickHouse XML configuration files to disable unnecessary logging tables and configure user access. Create /opt/plausible/config/clickhouse-config.xml:

<clickhouse>
    <logger>
        <level>warning</level>
        <console>1</console>
    </logger>
    <query_log remove="1"/>
    <trace_log remove="1"/>
    <text_log remove="1"/>
    <metric_log remove="1"/>
    <asynchronous_metric_log remove="1"/>
    <session_log remove="1"/>
    <part_log remove="1"/>
</clickhouse>

Next, configure ClickHouse user access rules in /opt/plausible/config/clickhouse-user-config.xml:

<clickhouse>
    <profiles>
        <default>
            <max_memory_usage>2000000000</max_memory_usage>
            <use_uncompressed_cache>0</use_uncompressed_cache>
            <load_balancing>random</load_balancing>
        </default>
    </profiles>
    <users>
        <default>
            <password remove="1"/>
            <password>plausible_clickhouse_secret</password>
            <networks>
                <ip>::/0</ip>
            </networks>
            <profile>default</profile>
            <quota>default</quota>
        </default>
    </users>
</clickhouse>

Step 3: Generating Application Secrets & Environment File

Plausible relies on a 64-byte base64 secret key for session cookie encryption. Generate this string directly using OpenSSL:

# Generate a 64-byte secret key base
openssl rand -base64 64 | tr -d '\n' ; echo

Create the environment file /opt/plausible/plausible-conf.env with strict permissions:

# Operational Environment Configuration
BASE_URL=https://analytics.example.com
SECRET_KEY_BASE=PASTE_YOUR_GENERATED_64_BYTE_BASE64_KEY_HERE

# Database Connections
DATABASE_URL=postgres://plausible:postgres_secure_pass@plausible_db:5432/plausible_db
CLICKHOUSE_DATABASE_URL=http://default:plausible_clickhouse_secret@plausible_events_db:8123/plausible_events_db

# Security & Access Controls
DISABLE_REGISTRATION=invite_only

# Mail Delivery (SMTP Configuration for password resets & weekly digest reports)
MAILER_EMAIL=notifications@example.com
SMTP_HOST_ADDR=smtp.sendgrid.net
SMTP_HOST_PORT=587
SMTP_USER_NAME=apikey
SMTP_USER_PWD=YOUR_SMTP_API_KEY
SMTP_HOST_SSL_ENABLED=false
sudo chmod 600 /opt/plausible/plausible-conf.env

Step 4: Production-Ready Docker Compose Stack

Construct /opt/plausible/docker-compose.yml. The stack defines isolated volumes, health checks, and restart policies to guarantee zero downtime:

services:
  mail:
    image: bytemark/smtp
    restart: unless-stopped
    environment:
      - RELAY_HOST=${SMTP_HOST_ADDR}
      - RELAY_PORT=${SMTP_HOST_PORT}
      - RELAY_USER=${SMTP_USER_NAME}
      - RELAY_PASSWORD=${SMTP_USER_PWD}
    networks:
      - plausible-net

  plausible_db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=plausible
      - POSTGRES_PASSWORD=postgres_secure_pass
      - POSTGRES_DB=plausible_db
    volumes:
      - /opt/plausible/data/db-data:/var/lib/postgresql/data
    networks:
      - plausible-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible_db"]
      interval: 10s
      timeout: 5s
      retries: 5

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.3-alpine
    restart: unless-stopped
    volumes:
      - /opt/plausible/data/event-data:/var/lib/clickhouse
      - /opt/plausible/data/event-logs:/var/log/clickhouse-server
      - /opt/plausible/config/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
      - /opt/plausible/config/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
    networks:
      - plausible-net
    ulimits:
      nofile:
        soft: 262144
        hard: 262144

  plausible:
    image: plausible/analytics:v2.1.4
    restart: unless-stopped
    command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_started
    env_file:
      - /opt/plausible/plausible-conf.env
    ports:
      - "127.0.0.1:8000:8000"
    networks:
      - plausible-net

networks:
  plausible-net:
    name: plausible-net
    driver: bridge

Step 5: Launching & Provisioning the First Admin Account

Launch the stack and track the database migration logs:

cd /opt/plausible
docker compose up -d

# Follow the startup logs
docker compose logs -f plausible

Once you see Running Plausible.Web.Endpoint with Bandit, the server is listening. Create your primary administrative account using the CLI entrypoint:

docker compose exec plausible /entrypoint.sh db admin \
  "Admin User" \
  "admin@example.com" \
  "YourStrongPassword123!"

Step 6: Reverse Proxy Integration (Caddy & Nginx)

Never expose Plausible’s port 8000 directly to the internet. Always terminate SSL via a hardened reverse proxy.

Option A: Hardened Caddy Configuration

analytics.example.com {
    encode gzip zstd

    reverse_proxy 127.0.0.1:8000 {
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    # Strict Security Headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
}

Option B: Nginx Virtual Host

server {
    listen 443 ssl http2;
    server_name analytics.example.com;

    ssl_certificate /etc/letsencrypt/live/analytics.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/analytics.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

Step 7: Embedding the Tracking Script

Log in to your Plausible dashboard at https://analytics.example.com, click + Add Website, and register your domain name (e.g., example.com). Add the generated snippet into the <head> section of your site:

<!-- Plausible Analytics: Zero-Cookie, Lightweight Privacy Tracking -->
<script defer data-domain="example.com" src="https://analytics.example.com/js/script.js"></script>

Because Plausible does not collect identifiers, place persistent device cookies, or track visitors across disparate websites, no cookie banner or user consent dialog is legally required under the EU ePrivacy Directive.

Troubleshooting Common Deployment Issues

1. ClickHouse Fails with “Cannot allocate memory” or Crashing Under Load

Cause: ClickHouse attempts to reserve significant memory buffers by default. On low-memory VPS instances (under 4 GB RAM), the Linux OOM killer terminates the process.

Solution: Ensure clickhouse-user-config.xml specifies <max_memory_usage>2000000000</max_memory_usage> (capping memory to 2 GB). Additionally, verify that ulimits.nofile is set to at least 262144 in docker-compose.yml.

2. Browser AdBlockers Intercepting Tracking Events

Cause: Aggressive browser extensions (like uBlock Origin) maintain heuristics that match /js/script.js and /api/event queries on popular domains.

Solution: Plausible supports custom script extensions and URL proxying. You can proxy the script through your website’s main domain (e.g. https://example.com/stats/telemetry.js) via Nginx or Cloudflare Workers, ensuring 100% telemetry capture for legitimate users.

3. CSRF Verification Failures or “Invalid Base URL”

Cause: The BASE_URL defined in plausible-conf.env does not precisely match the protocol and domain in the visitor’s browser bar (e.g., mismatch between http:// and https:// or missing port numbers).

Solution: Ensure BASE_URL strictly reflects your public HTTPS URL (e.g. BASE_URL=https://analytics.example.com) without any trailing slashes, and confirm your reverse proxy passes X-Forwarded-Proto https upstream.

Conclusion

Self-hosting Plausible Analytics with Docker Compose and ClickHouse gives you total data sovereignty over your web metrics. You get clean, real-time analytics with sub-second dashboard rendering, eliminate third-party tracking liability, and deliver a faster, privacy-respecting browsing experience to your audience.