Self-Hosted Web Analytics: Privacy-First Alternatives to Google Analytics
Google Analytics tracks your visitors across the web, feeds data into Google's advertising ecosystem, and requires cookie consent banners under GDPR. It adds 45+ KB of JavaScript to every page load, and GA4's interface has drawn widespread criticism for its complexity. Self-hosted analytics platforms solve all of these problems: your data stays on your server, tracking scripts are lightweight, and many operate without cookies, eliminating the need for consent banners entirely.
This guide covers four self-hosted analytics platforms, each with a different approach to balancing simplicity and depth.
Platform Comparison
| Feature | Plausible | Umami | Matomo | Shynet |
|---|---|---|---|---|
| Language | Elixir | Node.js | PHP | Python (Django) |
| Script size | ~1 KB | ~2 KB | ~22 KB | 0 KB (no JS option) |
| Cookies | None | None | Optional (cookieless mode) | None |
| GDPR consent needed | No | No | Depends on config | No |
| Dashboard | Single-page, clean | Clean, customizable | Full-featured (complex) | Minimal |
| Event tracking | Yes (goals) | Yes (custom events) | Yes (full event system) | Basic |
| Ecommerce tracking | Revenue goals | No | Yes (full) | No |
| Heatmaps / Sessions | No | No | Yes (premium plugin) | No |
| API | REST API | REST API | Comprehensive REST API | REST API |
| RAM usage | ~200 MB | ~150 MB | ~300 MB | ~100 MB |
| License | AGPL-3.0 | MIT | GPL-3.0 | Apache-2.0 |
Docker Deployment: Plausible
Plausible provides the cleanest dashboard and smallest tracking script. It uses ClickHouse for efficient time-series storage:
# docker-compose.yml for Plausible
version: "3.8"
services:
plausible:
image: ghcr.io/plausible/community-edition:latest
container_name: plausible
restart: unless-stopped
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
environment:
- BASE_URL=https://analytics.example.com
- SECRET_KEY_BASE=${SECRET_KEY} # Generate: openssl rand -base64 48
- TOTP_VAULT_KEY=${TOTP_KEY} # Generate: openssl rand -base64 32
- DATABASE_URL=postgres://plausible:${DB_PASSWORD}@plausible-db:5432/plausible
- CLICKHOUSE_DATABASE_URL=http://plausible-events-db:8123/plausible_events
# SMTP for email reports
- [email protected]
- SMTP_HOST_ADDR=smtp.example.com
- SMTP_HOST_PORT=587
- [email protected]
- SMTP_USER_PWD=${SMTP_PASSWORD}
- SMTP_HOST_SSL_ENABLED=true
# Disable registration after initial setup
- DISABLE_REGISTRATION=invite_only
ports:
- "8000:8000"
depends_on:
- plausible-db
- plausible-events-db
plausible-db:
image: postgres:16-alpine
container_name: plausible-db
restart: unless-stopped
environment:
- POSTGRES_DB=plausible
- POSTGRES_USER=plausible
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- plausible_db:/var/lib/postgresql/data
plausible-events-db:
image: clickhouse/clickhouse-server:latest
container_name: plausible-events-db
restart: unless-stopped
volumes:
- plausible_events:/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
volumes:
plausible_db:
plausible_events:
Adding Plausible to Your Website
<!-- Add to your site's <head> section -->
<script defer data-domain="yoursite.com"
src="https://analytics.example.com/js/script.js"></script>
<!-- With custom event tracking -->
<script defer data-domain="yoursite.com"
src="https://analytics.example.com/js/script.tagged-events.js"></script>
<!-- Track a custom event -->
<script>
plausible('Signup', {props: {plan: 'premium'}});
</script>
<!-- Track outbound links automatically -->
<script defer data-domain="yoursite.com"
src="https://analytics.example.com/js/script.outbound-links.js"></script>
Docker Deployment: Umami
# docker-compose.yml for Umami
version: "3.8"
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://umami:${DB_PASSWORD}@umami-db:5432/umami
- DATABASE_TYPE=postgresql
- APP_SECRET=${APP_SECRET}
- TRACKER_SCRIPT_NAME=custom-script-name # Rename to avoid ad blockers
ports:
- "3000:3000"
depends_on:
umami-db:
condition: service_healthy
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:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 5s
timeout: 5s
retries: 5
volumes:
umami_db:
Adding Umami to Your Website
<!-- Basic tracking script -->
<script async src="https://analytics.example.com/script.js"
data-website-id="your-website-id"></script>
<!-- Track custom events -->
<button onclick="umami.track('signup-click', {plan: 'premium'})">
Sign Up
</button>
<!-- Track via data attributes -->
<a href="/pricing" data-umami-event="pricing-link">View Pricing</a>
Docker Deployment: Matomo
# docker-compose.yml for Matomo
version: "3.8"
services:
matomo:
image: matomo:latest
container_name: matomo
restart: unless-stopped
environment:
- MATOMO_DATABASE_HOST=matomo-db
- MATOMO_DATABASE_DBNAME=matomo
- MATOMO_DATABASE_USERNAME=matomo
- MATOMO_DATABASE_PASSWORD=${DB_PASSWORD}
volumes:
- matomo_data:/var/www/html
ports:
- "8080:80"
depends_on:
- matomo-db
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}
volumes:
- matomo_db:/var/lib/mysql
volumes:
matomo_data:
matomo_db:
Docker Deployment: Shynet
# docker-compose.yml for Shynet
version: "3.8"
services:
shynet:
image: milesmcc/shynet:latest
container_name: shynet
restart: unless-stopped
environment:
- DB_NAME=shynet
- DB_USER=shynet
- DB_PASSWORD=${DB_PASSWORD}
- DB_HOST=shynet-db
- DB_PORT=5432
- DJANGO_SECRET_KEY=${SECRET_KEY}
- ALLOWED_HOSTS=analytics.example.com
- SERVER_USAGE_ID=none
- ACCOUNT_SIGNUPS_ENABLED=false
ports:
- "8080:8080"
depends_on:
- shynet-db
shynet-db:
image: postgres:16-alpine
container_name: shynet-db
restart: unless-stopped
environment:
- POSTGRES_DB=shynet
- POSTGRES_USER=shynet
- POSTGRES_PASSWORD=${DB_PASSWORD}
volumes:
- shynet_db:/var/lib/postgresql/data
volumes:
shynet_db:
Shynet can track visitors without any JavaScript at all, using a 1x1 pixel tracking image:
<!-- JavaScript tracking (more accurate) -->
<script src="https://analytics.example.com/ingress/your-service-uuid/script.js"></script>
<!-- No-JS tracking (pixel) -->
<noscript>
<img src="https://analytics.example.com/ingress/your-service-uuid/pixel.gif">
</noscript>
GDPR Compliance
The main advantage of privacy-first analytics is GDPR compliance without cookie consent banners:
| Requirement | Google Analytics | Plausible / Umami |
|---|---|---|
| Cookie consent banner | Required | Not required (no cookies) |
| Data processing agreement | Required (DPA with Google) | Not required (data stays on your server) |
| Right to erasure | Complex (Google's process) | Simple (data is anonymous by design) |
| Cross-site tracking | Yes (Google ecosystem) | None |
| Data transfer to US | Yes (Schrems II concerns) | No (your server, your jurisdiction) |
| Personal data collected | IP, device fingerprint, cookies | None (hashed, non-reversible) |
Performance Impact
Script size directly impacts your website's performance:
# Script size comparison (gzipped)
# Google Analytics (GA4): ~45 KB
# Matomo: ~22 KB
# Umami: ~2 KB
# Plausible: ~1 KB
# Shynet (pixel mode): 0 KB (1x1 GIF = ~43 bytes)
# Test the impact with Lighthouse:
# 1. Run Lighthouse without analytics script
# 2. Add the analytics script
# 3. Run Lighthouse again
# 4. Compare Performance scores
# Plausible and Umami typically add 0-2ms to page load time
# Google Analytics can add 50-200ms due to third-party DNS + download
Event Tracking
Custom event tracking lets you measure specific user actions:
// Plausible custom events
plausible('Download', {props: {file: 'whitepaper.pdf'}});
plausible('Purchase', {props: {plan: 'pro', revenue: '49.99'}});
// Umami custom events
umami.track('button-click', {id: 'cta-hero', text: 'Get Started'});
umami.track('form-submit', {form: 'contact', status: 'success'});
// Matomo custom events
_paq.push(['trackEvent', 'Downloads', 'PDF', 'Whitepaper']);
_paq.push(['trackEvent', 'Form', 'Submit', 'Contact Form']);
// Matomo ecommerce tracking
_paq.push(['addEcommerceItem', 'SKU123', 'Product Name', 'Category', 49.99, 1]);
_paq.push(['trackEcommerceOrder', 'ORDER-123', 49.99]);
Migration from Google Analytics
Migrating from GA to a self-hosted solution involves two steps: importing historical data (where supported) and switching the tracking code.
Data Import
# Plausible supports Google Analytics data import
# In Plausible admin: Settings > Imports & Exports > Google Analytics
# This imports historical pageview data via the GA API
# Matomo also supports GA data import
# Via the Matomo admin panel: Settings > Import Google Analytics
# Umami and Shynet do not support GA data import
# Start fresh - historical comparison is not critical for most sites
Parallel Tracking (Transition Period)
<!-- Run both during transition (2-4 weeks recommended) -->
<head>
<!-- Google Analytics (remove after transition) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
<!-- Self-hosted analytics (keep) -->
<script defer data-domain="yoursite.com"
src="https://analytics.example.com/js/script.js"></script>
</head>
<!-- After 2-4 weeks, compare data between platforms -->
<!-- Then remove Google Analytics entirely -->
Avoiding Ad Blockers
Ad blockers may block analytics scripts even when self-hosted. Mitigation strategies:
# 1. Serve the script from your own domain via reverse proxy
# nginx.conf
location /js/script.js {
proxy_pass https://analytics.example.com/js/script.js;
proxy_set_header Host analytics.example.com;
}
# 2. Rename the script (Umami supports this natively)
# TRACKER_SCRIPT_NAME=custom-name
# Then: <script src="/custom-name.js">
# 3. Use server-side tracking (Plausible API)
curl -X POST https://analytics.example.com/api/event \
-H "User-Agent: $USER_AGENT" \
-H "X-Forwarded-For: $CLIENT_IP" \
-H "Content-Type: application/json" \
-d '{
"domain": "yoursite.com",
"name": "pageview",
"url": "https://yoursite.com/page"
}'
Which One Should You Choose?
- Choose Plausible if you want a clean, simple dashboard that shows you exactly what you need without overwhelming detail. Best for blogs, marketing sites, and small businesses that want privacy-friendly analytics with minimal setup.
- Choose Umami if you want a clean, developer-friendly analytics platform with a good balance of simplicity and customization. Best for developers and small teams who want more control than Plausible without Matomo's complexity.
- Choose Matomo if you need a full Google Analytics replacement with ecommerce tracking, heatmaps, session recordings, and detailed behavioral analytics. Best for businesses that need comprehensive analytics and are willing to accept higher resource usage.
- Choose Shynet if you want the absolute lightest possible tracking with no-JavaScript support. Best for privacy-focused projects and sites where every kilobyte matters.
All of these platforms run as Docker containers and can be managed alongside your other self-hosted services. Tools like usulnet help you monitor container health and resource usage across your analytics and web infrastructure, ensuring your tracking stays online.