Plausible Analytics Docker Setup: A Privacy-First Google Analytics Alternative

Learn how to self-host Plausible Analytics using Docker Compose. A complete privacy-focused analytics deployment guide with Clickhouse configuration.

ClickHouse Configuration

Plausible Analytics relies on ClickHouse for storing and querying analytics events. ClickHouse is configured to optimize memory usage and performance. Below is the ClickHouse configuration file, /etc/clickhouse-server/config.d/user.xml or custom config file, but typically we override configurations using XML files in clickhouse/clickhouse-config.xml and clickhouse/clickhouse-user-config.xml.

clickhouse-config.xml

<clickhouse>
    <logger>
        <level>warning</level>
        <console>true</console>
    </logger>

    <!-- Reduce memory consumption for small VPS instances -->
    <max_server_memory_usage_to_ram_ratio>0.9</max_server_memory_usage_to_ram_ratio>

    <!-- Disable telemetry and sharing -->
    <send_crash_reports>
        <enabled>false</enabled>
    </send_crash_reports>
</clickhouse>

clickhouse-user-config.xml

<clickhouse>
    <profiles>
        <default>
            <!-- Cap memory usage per query (adjust based on VPS RAM, e.g., 2GB here) -->
            <max_memory_usage>2000000000</max_memory_usage>
            <max_bytes_before_external_group_by>1000000000</max_bytes_before_external_group_by>
            <max_bytes_before_external_sort>1000000000</max_bytes_before_external_sort>
        </default>
    </profiles>
</clickhouse>

Database Settings and Environment Variables

Plausible requires a PostgreSQL database for application metadata (users, sites, goals) and a ClickHouse database for raw pageviews and events. The main configuration is driven via environment variables. Create a .env file in your deployment directory:

# Plausible Base Configuration
BASE_URL=https://plausible.example.com
PORT=8000
SECRET_KEY_BASE=YOUR_SUPER_SECRET_KEY_BASE_64_CHARS
TOTP_VAULT_KEY=YOUR_TOTP_VAULT_KEY_32_CHARS

# Admin User Initialization
ADMIN_USER_EMAIL=admin@example.com
ADMIN_USER_NAME=admin
ADMIN_USER_PWD=YOUR_SECURE_ADMIN_PASSWORD

# ClickHouse DB Connection
CLICKHOUSE_DATABASE_URL=http://clickhouse:8123/plausible_events_db

# PostgreSQL DB Connection
DATABASE_URL=postgres://postgres:YOUR_POSTGRES_PASSWORD@plausible_db:5432/plausible_db

# Disable public registrations after creating admin
DISABLE_REGISTRATION=invite_only

Generate the SECRET_KEY_BASE and TOTP_VAULT_KEY using openssl commands:

openssl rand -base64 48 | tr -d '\n' # For SECRET_KEY_BASE
openssl rand -base64 32 | tr -d '\n' # For TOTP_VAULT_KEY

Docker Compose Configuration

The following docker-compose.yml configures Plausible, its PostgreSQL metadata store, ClickHouse event store, and a Redis instance for caching.

version: '3.8'

services:
  plausible_db:
    image: postgres:14-alpine
    container_name: plausible_db
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=plausible_db
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=YOUR_POSTGRES_PASSWORD
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  clickhouse:
    image: clickhouse/clickhouse-server:24.3-alpine
    container_name: plausible_clickhouse
    restart: always
    volumes:
      - clickhouse_data:/var/lib/clickhouse
      - ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
      - ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  plausible:
    image: plausible/analytics:v2.1.4
    container_name: plausible_app
    restart: always
    command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    depends_on:
      plausible_db:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
    ports:
      - "127.0.0.1:8000:8000"
    env_file:
      - .env

volumes:
  db_data:
    driver: local
  clickhouse_data:
    driver: local

Reverse Proxy SSL Setup

To secure the Plausible deployment, Nginx acts as a reverse proxy terminated with an SSL/TLS certificate managed by Certbot (Let's Encrypt).

Nginx Server Configuration

Save the configuration below as /etc/nginx/sites-available/plausible.conf and link it to /etc/nginx/sites-enabled/.

server {
    listen 80;
    listen [::]:80;
    server_name plausible.example.com;

    # Certbot challenge path
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    # Redirect all HTTP requests to HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name plausible.example.com;

    # SSL Settings
    ssl_certificate /etc/letsencrypt/live/plausible.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/plausible.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers on;

    # Security Headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;

    # Proxy Plausible Requests
    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;

        # Enable buffering for heavy reports
        proxy_buffering on;
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;
    }
}

Let's Encrypt SSL Provisioning

Generate the SSL certificates using Certbot in webroot mode:

sudo certbot certonly --webroot -w /var/www/html -d plausible.example.com --email admin@example.com --agree-tos --no-eff-email

Enable automatic renewal via systemd timers or cron:

echo "0 0 */12 * * root systemctl reload nginx" | sudo tee -a /etc/crontab