Stop Feeding Google: Self-Host Your Analytics with Plausible, Umami, or Matomo

Every page view you track with Google Analytics is a gift to Google’s advertising machine. Your visitors, your data, their profit. At some point you start asking: why am I doing this to myself — and to my users?

The answer most people reach eventually is self-hosting. But then comes the paralysis: Plausible, Umami, Matomo — which one? They all claim to be "privacy-first," they all run in Docker, and they all have slick marketing pages. The differences only become obvious after you’ve actually run them under real traffic.

This article cuts through the noise. I’ve run all three in production environments — from personal blogs to high-traffic SaaS dashboards. Here’s what actually matters.


Why Google Analytics is Not Just a Privacy Problem

Before picking a replacement, be honest about why you’re leaving. The privacy angle is real, but it’s not the only issue:

  • Performance tax: GA’s script is not small. It blocks rendering on slow connections.
  • Ad blockers: A growing share of your audience blocks GA entirely. Your data is already incomplete.
  • GDPR consent walls: If you’re serious about compliance, GA requires a consent banner that kills conversion rates. Most cookieless alternatives don’t.
  • Vendor lock-in: Your historical data lives in Google’s warehouse, not yours.

Self-hosted analytics solves all four. The tradeoff is operational overhead — you own the database, the backups, the uptime. Worth it for anyone running their own infrastructure already.


The Contenders at a Glance

Feature Plausible Umami Matomo
Cookieless by default Yes Yes Optional
GDPR consent required No No Depends on config
Resource usage Low Low High
Custom events Yes Yes Yes
Funnel analysis No No Yes
Goal tracking Basic Basic Advanced
Multi-site Yes (paid cloud, self-host free) Yes Yes
Database PostgreSQL + ClickHouse PostgreSQL or MySQL MySQL/MariaDB
UI quality Excellent Good Dated
GitHub plausible/analytics umami-software/umami matomo-org/matomo

Plausible: The Opinionated One

Plausible is what happens when developers get tired of bloated analytics and build exactly what they want. The script is under 1KB. The dashboard shows one page. No segments, no funnels, no pivot tables — just the numbers that matter: visitors, page views, bounce rate, top pages, referrers, and countries.

That simplicity is a feature, not a bug. Most site owners look at two metrics: "how many people came today" and "where did they come from." Plausible answers that in three seconds.

Docker Compose for Plausible

Plausible’s architecture is the most complex of the three — it uses both PostgreSQL for relational data and ClickHouse for event storage. That’s the price of performance at scale. For sites under a few million monthly page views, you won’t notice the difference, but the dual-database setup means more moving parts.

# docker-compose.yml — Plausible CE v2.x
version: "3.8"

services:
  plausible_db:
    image: postgres:16-alpine
    container_name: plausible_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: plausible_db
      POSTGRES_USER: plausible
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - plausible_db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible"]
      interval: 10s
      timeout: 5s
      retries: 5

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.3-alpine
    container_name: plausible_events_db
    restart: unless-stopped
    volumes:
      - plausible_events_data:/var/lib/clickhouse
      - ./clickhouse/logs.xml:/etc/clickhouse-server/config.d/logs.xml:ro
      - ./clickhouse/ipv4-only.xml:/etc/clickhouse-server/config.d/ipv4-only.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144

  plausible:
    image: ghcr.io/plausible/community-edition:v2.1
    container_name: plausible
    restart: unless-stopped
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_started
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      BASE_URL: https://stats.yourdomain.com
      SECRET_KEY_BASE: ${SECRET_KEY_BASE}   # generate: openssl rand -base64 64
      DATABASE_URL: postgres://plausible:${POSTGRES_PASSWORD}@plausible_db:5432/plausible_db
      CLICKHOUSE_DATABASE_URL: http://plausible_events_db:8123/plausible_events
      DISABLE_REGISTRATION: invite_only       # lock it down after first user
    command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"

volumes:
  plausible_db_data:
  plausible_events_data:

You’ll need two ClickHouse config files. Create clickhouse/logs.xml:

<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"/>
</clickhouse>

And clickhouse/ipv4-only.xml to avoid IPv6 bind issues on some hosts:

<clickhouse>
  <listen_host>0.0.0.0</listen_host>
</clickhouse>

Gotcha: The sleep 10 in the command is a lazy but necessary hack. Plausible’s migration runner doesn’t retry on ClickHouse connection failures during startup. Health checks on ClickHouse are tricky to configure correctly, so the sleep buys enough time for it to initialize. In production, replace this with a proper init container or a script that polls the ClickHouse HTTP endpoint.

Plausible Verdict

Best for: personal sites, blogs, small-to-medium SaaS products. If you want beautiful, fast, privacy-compliant analytics and you don’t need funnel analysis or custom reports, just run Plausible and stop thinking about it.


Umami: The Developer’s Pick

Umami sits in an interesting position: it’s simpler than Matomo but more flexible than Plausible. The codebase is clean Next.js — you can read it on a Sunday afternoon. It supports multiple users, multiple sites, custom events, and has a REST API that makes it trivial to pipe data elsewhere.

The dashboard isn’t quite as polished as Plausible, but it’s fast and functional. What makes Umami stand out is the custom events API — instrumenting button clicks, form submissions, or any user action is a single line of JavaScript:

umami.track('signup-button-click', { plan: 'pro' });

Docker Compose for Umami

Umami’s setup is the simplest of the three. One app container, one database. PostgreSQL or MySQL — your call.

# docker-compose.yml — Umami v2.x
version: "3.8"

services:
  umami_db:
    image: postgres:16-alpine
    container_name: umami_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - umami_db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 10

  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    restart: unless-stopped
    depends_on:
      umami_db:
        condition: service_healthy
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DATABASE_URL: postgresql://umami:${DB_PASSWORD}@umami_db:5432/umami
      # Generate with: openssl rand -hex 32
      APP_SECRET: ${APP_SECRET}
      # Disable telemetry back to Umami (yes, ironic, but it exists)
      DISABLE_TELEMETRY: 1

volumes:
  umami_db_data:

After first boot, log in with admin / umami and change the password immediately. This is documented but people skip it.

Gotcha: Umami’s Docker image is tagged by database type (postgresql-latest vs mysql-latest). Pulling the wrong tag gives you a container that silently fails to connect to your database. Always double-check the tag.

Gotcha: The DISABLE_TELEMETRY variable isn’t mentioned prominently in the official docs, but Umami does phone home usage stats by default. Set it to 1.

Umami Verdict

Best for: developers who want an extensible, hackable analytics platform. The REST API and clean event tracking make it excellent for product teams that want to pipe events into other systems or build custom reporting on top. Also a good choice if you’re already comfortable with Next.js and want to fork/extend it.


Matomo: The Enterprise Workhorse

Matomo (formerly Piwik) is what you reach for when you need to replace Google Analytics feature-for-feature. Funnel visualization, A/B testing, heatmaps, session recordings, e-commerce tracking, media analytics — it’s all there, either built-in or via plugins.

The downside is that Matomo is heavy. It’s a PHP/MySQL application that hasn’t fully shed its 2008 architectural roots. It will use significantly more RAM and CPU than Plausible or Umami, and the UI feels like it was designed by committee. But the feature depth is real, and for regulated industries or enterprise reporting requirements, nothing else comes close in the open-source space.

Docker Compose for Matomo

# docker-compose.yml — Matomo 5.x
version: "3.8"

services:
  matomo_db:
    image: mariadb:10.11
    container_name: matomo_db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: matomo
      MYSQL_USER: matomo
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_AUTO_UPGRADE: "1"
    volumes:
      - matomo_db_data:/var/lib/mysql
    command: >
      --max-allowed-packet=64MB
      --innodb-buffer-pool-size=256M
      --innodb-log-file-size=64M
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  matomo:
    image: matomo:5-apache
    container_name: matomo
    restart: unless-stopped
    depends_on:
      matomo_db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - matomo_data:/var/www/html
      # Persist config so upgrades don't reset it
      - ./matomo_config:/var/www/html/config
    environment:
      MATOMO_DATABASE_HOST: matomo_db
      MATOMO_DATABASE_DBNAME: matomo
      MATOMO_DATABASE_USERNAME: matomo
      MATOMO_DATABASE_PASSWORD: ${DB_PASSWORD}

volumes:
  matomo_db_data:
  matomo_data:

Matomo requires a web-based installer on first run. Navigate to https://cd-linux.club after the containers start and walk through the setup wizard. This is the old-school way — no CLI bootstrap, unlike the other two.

Gotcha: The default max_allowed_packet in MariaDB is too small for Matomo’s bulk archiving queries. Setting it to at least 64MB in the command args above prevents cryptic import failures when running the archiving cron.

Gotcha: Matomo has an archiving process that must run on a schedule. Without it, your reports get stale and the "live" dashboard lies to you. After setup, add this to your host crontab:

# Run Matomo archiving every hour
5 * * * * docker exec matomo /var/www/html/console core:archive --url=https://matomo.yourdomain.com >> /var/log/matomo-archive.log 2>&1

Gotcha: Matomo with cookies enabled (the default) technically requires GDPR consent in the EU. The cookieless mode exists but you have to explicitly enable it under Settings → Privacy → Users opt-out. It’s buried, and the defaults are not consent-free.

Matomo Verdict

Best for: e-commerce sites, enterprise teams, anyone coming from GA360 who needs like-for-like features. If you need conversion funnels, e-commerce revenue tracking, or session recordings, Matomo is the only self-hosted option that delivers. Expect to spend more time on maintenance and allocate at least 2GB RAM for a comfortable production setup.


Nginx Reverse Proxy Config

All three setups bind to localhost. You’ll want TLS termination in front of them. Here’s a minimal Nginx block that works for any of the three:

server {
    listen 443 ssl http2;
    server_name stats.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/stats.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/stats.yourdomain.com/privkey.pem;

    # Plausible: proxy_pass http://127.0.0.1:8000;
    # Umami:     proxy_pass http://127.0.0.1:3000;
    # Matomo:    proxy_pass http://127.0.0.1:8080;
    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;
        # Needed for Plausible's real-time updates
        proxy_buffering off;
    }
}

The X-Forwarded-For header is critical. Without it, all your visitors show up as 127.0.0.1 in the geolocation data.


Production-Ready: Backup Strategy

Analytics data has actual business value. Set up automated backups before you forget.

#!/usr/bin/env bash
# backup-analytics.sh — runs daily via cron
set -euo pipefail

BACKUP_DIR="/backups/analytics"
DATE=$(date +%Y%m%d-%H%M)

mkdir -p "$BACKUP_DIR"

# PostgreSQL (Plausible or Umami)
docker exec plausible_db pg_dump -U plausible plausible_db \
  | gzip > "$BACKUP_DIR/plausible-pg-$DATE.sql.gz"

# ClickHouse (Plausible only) — backup via filesystem snapshot
docker exec plausible_events_db clickhouse-backup create "backup-$DATE"

# Rotate: keep 14 days
find "$BACKUP_DIR" -name "*.gz" -mtime +14 -delete

echo "Backup complete: $DATE"

For Matomo, add a MariaDB dump:

docker exec matomo_db mysqldump -u matomo -p"${DB_PASSWORD}" matomo \
  | gzip > "$BACKUP_DIR/matomo-mysql-$DATE.sql.gz"

The Decision Framework

Stop overthinking it. Here’s the three-question filter:

Question 1: Do you need funnels, A/B testing, e-commerce revenue, or session recordings?
— Yes → Matomo. Accept the operational overhead.
— No → keep going.

Question 2: Do you need a REST API, custom event pipelines, or multi-user team access with per-site permissions?
— Yes → Umami.
— No → keep going.

Question 3: Do you just want to know how many people visited your site and where they came from, without touching configuration again for a year?
— Yes → Plausible.

The majority of sites running on self-hosted infrastructure land on Plausible or Umami. Matomo is the right call for specific use cases, not the default.


The One Gotcha Nobody Talks About

All three of these will fail to capture traffic from users behind aggressive privacy tools — uBlock Origin in hard mode, Brave shields, or DNS-level blockers like NextDNS. The tracking scripts are served from your own domain, which helps against third-party blockers, but nothing fully escapes a determined privacy user.

The honest answer: accept the gap. A 5–10% undercount is the price of not spying on your users. Any analytics tool, self-hosted or not, has this problem. The difference is that your users’ data stays on your server, not in an advertising platform’s data warehouse.

That’s the actual win here — not perfect data, but data sovereignty.

Leave a comment

👁 Views: 24,119 · Unique visitors: 34,689