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)
Tip: Even though Plausible and Umami do not require GDPR consent, mention them in your privacy policy. Transparency is always a good practice. A simple statement like "We use self-hosted Plausible Analytics, which collects no personal data and uses no cookies" is sufficient.

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 -->
Warning: Self-hosted analytics will show different numbers than Google Analytics due to different tracking methods and ad blocker handling. Plausible and Umami may show higher pageview counts because they are less likely to be blocked by privacy-focused browsers. Matomo may show lower counts if using cookieless mode. Do not expect exact parity.

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.