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.