Skip to main content
Privacy Self-Hosting Security

How to Self-Host Plausible Analytics: A Privacy-Focused Guide for VPS

Step-by-step guide to self-hosting Plausible Analytics on Hetzner with Podman, Caddy, and advanced security hardening. GDPR-compliant, no Docker, full control.

7 min
self host plausible analytics

How to Self-Host Plausible Analytics: A Privacy-Focused Guide for VPS

Take back control of your analytics—without sacrificing privacy or security.

Google Analytics tracks your visitors. It also tracks you. Every click, every scroll, every session is logged, analyzed, and monetized. For small businesses, freelancers, and privacy-conscious teams, this is a non-starter.

Plausible Analytics offers a lightweight, open-source alternative. It’s GDPR-compliant by default, doesn’t use cookies, and collects zero personal data. But why stop there? Self-hosting Plausible gives you full control over your data, your security, and your privacy.

In this guide, we’ll walk through how to self-host Plausible Analytics on a Hetzner VPS using Podman (not Docker), Caddy for automatic HTTPS, and advanced security hardening — including 2FA, dual-stack (IPv4 + IPv6), and VPN-only dashboard access.


Why Self-Host Plausible?

Privacy First

  • No cookies: Unlike Google Analytics, Plausible doesn’t track users across sites.

  • No personal data: Only aggregates metrics (page views, referrers, devices).

  • GDPR-compliant: No need for cookie banners or consent pop-ups.

Security by Design

  • Full control: Your data stays on your server — no third-party access.

  • Rootless Podman: Run containers without sudo for better isolation.

  • Advanced hardening: Firewalls, fail2ban, SELinux, and more.

Cost-Effective

  • Free: No monthly fees (vs. Plausible’s $9+/month hosted plans).

  • Scalable: Start with a €4.50/month Hetzner VPS and upgrade as needed.


Prerequisites

Before we begin, ensure you have:

A Hetzner VPS (CX21: 2 vCPU, 4GB RAM, 40GB SSD recommended).

A domain name (e.g., analytics.yourdomain.com).

Basic Linux knowledge (SSH, terminal commands).

Podman 5.0+ (rootless, daemonless).


Step 1: Server Hardening (Advanced Security)

Firewall Rules

Restrict access to only essential ports (SSH, HTTP, HTTPS):

sudo apt update && sudo apt install -y ufw
sudo ufw allow 22/tcp      # SSH
sudo ufw allow 80/tcp      # HTTP (for Let's Encrypt)
sudo ufw allow 443/tcp     # HTTPS
sudo ufw deny all          # Block everything else
sudo ufw enable

Create a Dedicated User

Never run services as root. Create a dedicated user for Plausible:

sudo useradd -r -s /bin/false plausible

Secure SSH

Disable root login and enforce SSH keys:

sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart sshd

Install Fail2ban

Block brute-force attacks:

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

Step 2: Install Podman

Podman is a daemonless, rootless alternative to Docker. It’s more secure and integrates seamlessly with systemd.

sudo apt update && sudo apt install -y podman podman-docker

Configure Rootless Podman

Allow your user to run containers without sudo:

sudo usermod --add-subuids 100000-165535 $USER
sudo usermod --add-subgids 100000-165535 $USER

Enable Lingering

Ensure containers persist after logout:

sudo loginctl enable-linger $USER

Step 3: Deploy Plausible with Quadlet

Quadlet is Podman’s declarative way to manage containers with systemd. It’s cleaner and more maintainable than docker-compose.yml.

Create Quadlet Directory

mkdir -p ~/.config/containers/systemd/plausible

PostgreSQL Container

Create ~/.config/containers/systemd/plausible/plausible-postgres.container:

[Unit]
Description=Plausible PostgreSQL Database
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/postgres:16-alpine
ContainerName=plausible-postgres
Environment=POSTGRES_DB=plausible
Environment=POSTGRES_USER=plausible
EnvironmentFile=%h/volumes/plausible/.env
Volume=%h/volumes/plausible/postgres-data:/var/lib/postgresql/data:Z
HealthCmd=pg_isready -U plausible
HealthInterval=30s
AutoUpdate=registry
Network=plausible.network
NoNewPrivileges=true
DropCapability=ALL
AddCapability=CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_SETUID,CAP_SETGID
Memory=512m
PidsLimit=200

[Service]
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

ClickHouse Container

Create ~/.config/containers/systemd/plausible/plausible-clickhouse.container:

[Unit]
Description=Plausible ClickHouse Analytics Database
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/clickhouse/clickhouse-server:24.3-alpine
ContainerName=plausible-clickhouse
Volume=%h/volumes/plausible/clickhouse-data:/var/lib/clickhouse:Z
HealthCmd=wget --spider -q http://localhost:8123/ping
HealthInterval=30s
AutoUpdate=registry
Network=plausible.network
NoNewPrivileges=true
DropCapability=ALL
AddCapability=CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_SETUID,CAP_SETGID
Ulimit=nofile=262144:262144
Memory=1g
PidsLimit=500

[Service]
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

Plausible App Container

Create ~/.config/containers/systemd/plausible/plausible.container:

[Unit]
Description=Plausible Analytics Web Application
After=network-online.target
After=plausible-postgres.service
After=plausible-clickhouse.service
Wants=network-online.target

[Container]
Image=ghcr.io/plausible/community-edition:v3.2.1
ContainerName=plausible
PublishPort=127.0.0.1:8000:8000
EnvironmentFile=%h/volumes/plausible/.env
HealthCmd=curl -fsS http://localhost:8000/api/health || exit 1
HealthInterval=30s
AutoUpdate=registry
Network=plausible.network
NoNewPrivileges=true
DropCapability=ALL
AddCapability=CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_SETUID,CAP_SETGID
Memory=256m
PidsLimit=100

[Service]
Restart=always
RestartSec=5

[Install]
WantedBy=default.target

Network Configuration

Create ~/.config/containers/systemd/plausible/plausible.network:

[Network]
Description=Plausible internal network
Internal=false
DNSEnabled=true

Environment File

Create ~/volumes/plausible/.env:

# Required
BASE_URL=https://analytics.yourdomain.com
SECRET_KEY_BASE=$(openssl rand -base64 48)    # Generate once, back up securely
TOTP_VAULT_KEY=$(openssl rand -base64 32)     # For 2FA

# Database URLs
DATABASE_URL=postgres://plausible:${POSTGRES_PASSWORD}@plausible-postgres:5432/plausible
CLICKHOUSE_DATABASE_URL=http://plausible:secret@plausible-clickhouse:8123/plausible

# Postgres
POSTGRES_PASSWORD=your-strong-password-here

# SMTP (for password resets & email reports)
SMTP_HOST_ADDR=smtp.resend.com
SMTP_HOST_PORT=587
SMTP_USER_NAME=resend_api_key
SMTP_USER_PWD=re_xxxxxxxxxxxx
MAILER_EMAIL=[email protected]

# Security
DISABLE_REGISTRATION=invite_only

# Privacy
CLICKHOUSE_MAX_DATA_RETENTION_DAYS=30  # 30-day retention
IP_ANONYMIZATION=true                  # Disable IP tracking

⚠️ Important: SECRET_KEY_BASE signs all user sessions. If you change it after first boot, every existing session and password-reset link breaks. Back it up somewhere safe.

Start the Services

systemctl --user daemon-reload
systemctl --user start plausible-postgres plausible-clickhouse
systemctl --user start plausible
systemctl --user enable plausible-postgres plausible-clickhouse plausible

Verify everything is running:

systemctl --user status plausible
curl -I http://localhost:8000

Step 4: Reverse Proxy with Caddy (Automatic HTTPS)

Caddy automatically provisions Let’s Encrypt certificates and enforces HTTPS. No certbot, no cron jobs, no manual renewals.

Install Caddy

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy

Configure Caddy

Create /etc/caddy/Caddyfile:

analytics.yourdomain.com {
    encode gzip zstd

    reverse_proxy 127.0.0.1:8000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        Referrer-Policy "strict-origin-when-cross-origin"
        -Server
    }

    # Block bots
    @bots header_regexp User-Agent "(bot|crawl|spider|slurp|semrush|ahrefs|dotbot)"
    respond @bots 403
}

Start Caddy

sudo systemctl enable --now caddy

Step 5: Dual-Stack (IPv4 + IPv6)

Enable IPv6 in Hetzner Cloud Console

  1. Go to Hetzner Cloud ConsoleNetworkingEnable IPv6.

  2. Assign an IPv6 address to your server.

Configure Caddy for IPv6

Update your /etc/caddy/Caddyfile to listen on both IPv4 and IPv6:

analytics.yourdomain.com {
    bind ::
    ...
}

Verify IPv6 Connectivity

curl -6 https://analytics.yourdomain.com

Step 6: Enable 2FA for Plausible Dashboard

Plausible supports TOTP-based 2FA for the admin dashboard.

  1. Set TOTP_VAULT_KEY in .env (already done in the environment file above).

  2. Log in to your Plausible dashboard at https://analytics.yourdomain.com.

  3. Go to SettingsSecurityEnable 2FA.

  4. Scan the QR code with Google Authenticator, Authy, or any TOTP app.

Every login now requires a time-based code from your phone. Even if your password is compromised, your dashboard stays protected.


Step 7: Privacy Optimizations

IP Anonymization

Already enabled via IP_ANONYMIZATION=true in .env. Plausible truncates visitor IPs before storing them — making it impossible to identify individual users.

Data Retention

Already set to 30 days via CLICKHOUSE_MAX_DATA_RETENTION_DAYS=30. Older analytics data is automatically purged. No hoarding, no surprises.

Restrict Dashboard Access to VPN

For maximum security, restrict the Plausible dashboard to a VPN so only you (and your team) can access it.

  1. Install Tailscale:

    curl -fsSL https://tailscale.com/install.sh | sh
    sudo tailscale up
  2. Update Caddy to bind to your Tailscale IP only:

    analytics.yourdomain.com {
        bind 100.x.y.z  # Your Tailscale IP
        ...
    }

Option B: WireGuard

  1. Set up WireGuard on your server.

  2. Update Caddy to bind to the WireGuard interface IP.

The tracking script (/js/script.js) still works for all visitors — only the admin dashboard is locked behind the VPN.


Step 8: Common Pitfalls & Fixes

IssueCauseFix
ClickHouse OOMInsufficient RAMSet Memory=1g in Quadlet file
Geolocation failsMissing X-Forwarded-ForEnsure Caddy sets headers correctly
Caddy cert failsDNS not propagatedRun dig +short analytics.yourdomain.com
Plausible won’t startSECRET_KEY_BASE changedBack up and restore the original key
IPv6 not workingHetzner IPv6 not enabledEnable in Cloud Console
Container dies after logoutLingering not enabledRun sudo loginctl enable-linger $USER
Permission denied on volumesSELinux context mismatchAdd :Z suffix to volume mounts

Final Notes

🔒 Security Checklist Recap

  • Firewall: only ports 22, 80, 443 open

  • SSH: root login disabled, keys only

  • Fail2ban: blocking brute-force attacks

  • Rootless Podman: no sudo for containers

  • NoNewPrivileges=true on all containers

  • DropCapability=ALL + minimal AddCapability

  • Memory and PID limits on all containers

  • Plausible binds to 127.0.0.1 only (not public)

  • HTTPS enforced via Caddy + security headers

  • 2FA enabled on the dashboard

  • DISABLE_REGISTRATION=invite_only

  • 30-day data retention with IP anonymization

  • Dashboard locked behind VPN (optional but recommended)


🚀 Try Parlant.dev Beta

Want a simpler way to self-host privacy tools? Join the Parlant.dev beta — a managed, open-source platform that takes the hassle out of running your own infrastructure.


📣 Syndication

This article is ready for Dev.to, Medium, and LinkedIn. When syndicating, add this footer:

Originally published on Renard Digital.


Next Steps

  1. Test your setup: Visit https://analytics.yourdomain.com and verify the dashboard loads.

  2. Monitor logs: Use journalctl --user -u plausible -f to debug issues.

  3. Backup regularly: Use pg_dump for PostgreSQL and clickhouse-client for ClickHouse.

Need help? Contact Renard Digital — we help businesses self-host with confidence.

Tags

#Plausible Analytics #Podman #Hetzner #Caddy #GDPR #2FA #Dual-Stack

Got a web project?

Renard Digital supports you from A to Z: site, domain, email.

Get in touch