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
sudofor 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=plausible@yourdomain.com
# Security
DISABLE_REGISTRATION=invite_only
# Privacy
CLICKHOUSE_MAX_DATA_RETENTION_DAYS=30 # 30-day retention
IP_ANONYMIZATION=true # Disable IP tracking
⚠️ Important:
SECRET_KEY_BASEsigns 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
Go to Hetzner Cloud Console → Networking → Enable IPv6.
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.
Set
TOTP_VAULT_KEYin.env(already done in the environment file above).Log in to your Plausible dashboard at
https://analytics.yourdomain.com.Go to Settings → Security → Enable 2FA.
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.
Option A: Tailscale (Recommended)
- Install Tailscale:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
- Update Caddy to bind to your Tailscale IP only:
analytics.yourdomain.com {
bind 100.x.y.z # Your Tailscale IP
...
}
Option B: WireGuard
Set up WireGuard on your server.
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
| Issue | Cause | Fix |
|---|---|---|
| ClickHouse OOM | Insufficient RAM | Set Memory=1g in Quadlet file |
| Geolocation fails | Missing X-Forwarded-For
|
Ensure Caddy sets headers correctly |
| Caddy cert fails | DNS not propagated | Run dig +short analytics.yourdomain.com
|
| Plausible won't start |
SECRET_KEY_BASE changed |
Back up and restore the original key |
| IPv6 not working | Hetzner IPv6 not enabled | Enable in Cloud Console |
| Container dies after logout | Lingering not enabled | Run sudo loginctl enable-linger $USER
|
| Permission denied on volumes | SELinux context mismatch | Add :Z suffix to volume mounts |
Final Notes
🔒 Security Checklist Recap
[x] Firewall: only ports 22, 80, 443 open
[x] SSH: root login disabled, keys only
[x] Fail2ban: blocking brute-force attacks
[x] Rootless Podman: no
sudofor containers[x]
NoNewPrivileges=trueon all containers[x]
DropCapability=ALL+ minimalAddCapability[x] Memory and PID limits on all containers
[x] Plausible binds to
127.0.0.1only (not public)[x] HTTPS enforced via Caddy + security headers
[x] 2FA enabled on the dashboard
[x]
DISABLE_REGISTRATION=invite_only[x] 30-day data retention with IP anonymization
[x] 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
Test your setup: Visit
https://analytics.yourdomain.comand verify the dashboard loads.Monitor logs: Use
journalctl --user -u plausible -fto debug issues.Backup regularly: Use
pg_dumpfor PostgreSQL andclickhouse-clientfor ClickHouse.
Need help? Contact Renard Digital — we help businesses self-host with confidence.





