Introduction
Website analytics are essential for understanding user behavior, but Google Analytics comes with several pain points:
- Privacy concerns: Google collects vast amounts of user data, often violating GDPR and other privacy regulations
- Performance impact: GA’s bloated script (~45KB+) slows down page load times
- Cost: While GA4 is free for basic use, privacy-friendly SaaS analytics tools (Fathom, Simple Analytics) charge $10-30/month
- Ad blocker blocking: Google Analytics is widely blocked by ad blockers, skewing data accuracy
Plausible Analytics is an open-source, lightweight, privacy-first website analytics tool with these advantages:
| Feature | Plausible | Google Analytics |
|---|---|---|
| Script size | < 1KB | 45KB+ |
| GDPR compliance | ✅ Native (cookie-free) | ⚠️ Requires configuration |
| Self-hostable | ✅ Open source & free | ❌ SaaS only |
| Ad blocker blocking | ⚠️ Partially blocked | ❌ Heavily blocked |
| Data ownership | ✅ Full control | ❌ Google owns it |
| Monthly cost (self-hosted) | ~$3-5 (VPS cost) | Free / Paid |
This guide walks you through deploying Plausible on a VPS with Docker, using PostgreSQL + ClickHouse dual-database architecture, with Nginx/Caddy reverse proxy and HTTPS.
Prerequisites
- A VPS (recommended minimum: 1 vCPU, 1GB RAM, 20GB SSD)
- A domain name (e.g.,
analytics.yourdomain.com) - Docker and Docker Compose (v2+)
- Basic Linux command-line knowledge
1. Prepare the Environment
Install Docker
# Install Docker (skip if already installed)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Install Docker Compose v2
sudo apt-get update
sudo apt-get install -y docker-compose-plugin
# Verify installation
docker --version
docker compose version
Set Up Directory Structure
# Create Plausible directory
mkdir -p ~/plausible && cd ~/plausible
# Directory layout
# ~/plausible/
# ├── docker-compose.yml
# ├── plausible-conf.env
# ├── clickhouse/
# │ └── (data directory, auto-created)
# └── plausible/
# └── (data directory, auto-created)
2. Configure Docker Compose
Plausible requires three services:
- Plausible — Main application (built with Elixir)
- PostgreSQL — Stores metadata (users, site configurations)
- ClickHouse — Stores analytics event data (column-oriented, optimized for analytics)
Create docker-compose.yml:
version: '3.8'
services:
plausible:
image: plausible/analytics:v2.1
restart: unless-stopped
command: sh -c "sleep 10 && /entrypoint.sh db migrate && /entrypoint.sh run"
ports:
- "127.0.0.1:8000:8000"
depends_on:
- plausible_db
- plausible_events_db
env_file:
- plausible-conf.env
volumes:
- plausible_data:/var/lib/plausible/data
- ./plausible/geoip:/var/lib/plausible/geoip:ro
plausible_db:
image: postgres:16-alpine
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=plausible
- POSTGRES_USER=plausible
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plausible_secret_password}
plausible_events_db:
image: clickhouse/clickhouse-server:24.3-alpine
restart: unless-stopped
volumes:
- event_data:/var/lib/clickhouse
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
environment:
- CLICKHOUSE_DB=plausible_events
- CLICKHOUSE_USER=plausible
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-plausible_secret_password}
volumes:
db_data:
event_data:
plausible_data:
ClickHouse Logging Configuration
Create clickhouse/clickhouse-config.xml to reduce log verbosity:
mkdir -p clickhouse
<clickhouse>
<logger>
<level>warning</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<size>10M</size>
<count>3</count>
</logger>
</clickhouse>
3. Configure Environment Variables
Create plausible-conf.env:
cat > plausible-conf.env << 'EOF'
# ── Base Configuration ──
BASE_URL=https://analytics.yourdomain.com
SECRET_KEY_BASE=$(openssl rand -base64 48)
DISABLE_REGISTRATION=true
# ── Database Configuration ──
DATABASE_URL=postgres://plausible:plausible_secret_password@plausible_db:5432/plausible
CLICKHOUSE_DATABASE_URL=http://plausible:plausible_secret_password@plausible_events_db:8123/plausible_events
# ── Performance Tuning ──
CLICKHOUSE_QUERY_TIMEOUT_MS=30000
DATABASE_POOL_SIZE=15
# ── Email (optional, for reports) ──
# MAILER_ADAPTER=Bamboo.SMTPAdapter
# SMTP_HOST_ADDR=smtp.example.com
# SMTP_HOST_PORT=587
# SMTP_USER_NAME=user@example.com
# SMTP_USER_PWD=password
# SMTP_HOST_SSL_ENABLED=true
# MAILER_EMAIL=analytics@yourdomain.com
EOF
⚠️ Important: Replace
BASE_URLwith your actual domain. ReplacePOSTGRES_PASSWORDandCLICKHOUSE_PASSWORDwith strong passwords.
Generate Keys
# Generate SECRET_KEY_BASE
openssl rand -base64 48
# Generate database passwords
openssl rand -base64 24
4. Start Plausible
# Start all services
docker compose up -d
# Check startup logs
docker compose logs -f
# Verify service status
docker compose ps
# Test that Plausible is running
curl -s http://127.0.0.1:8000 | head -5
After startup, visit http://your-vps-ip:8000 — you should see the Plausible interface.
Create Admin Account
The first visit will prompt you to register. With DISABLE_REGISTRATION=true, no one else can register afterward.
# Or create admin via CLI
docker compose exec plausible /entrypoint.sh db create-admin \
--email admin@yourdomain.com \
--name "Admin" \
--password "your-strong-password"
5. Configure Reverse Proxy and HTTPS
Option A: Caddy (Recommended, Auto HTTPS)
Caddy automatically provisions Let’s Encrypt certificates with zero config.
# Install Caddy
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
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-get update
sudo apt-get install caddy
Create /etc/caddy/Caddyfile:
analytics.yourdomain.com {
reverse_proxy 127.0.0.1:8000
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
}
# Logging
log {
output file /var/log/caddy/analytics.log
}
}
# Validate configuration
sudo caddy validate --config /etc/caddy/Caddyfile
# Reload Caddy
sudo systemctl reload caddy
# Check certificate status
sudo caddy cert-info analytics.yourdomain.com
Option B: Nginx
# /etc/nginx/sites-available/analytics.yourdomain.com
upstream plausible {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name analytics.yourdomain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name analytics.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/analytics.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/analytics.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://plausible;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
client_max_body_size 10m;
}
}
# Enable site
sudo ln -s /etc/nginx/sites-available/analytics.yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
6. Add Your Site and Deploy Tracking Script
Add a Site in Plausible
- Visit
https://analytics.yourdomain.com - Log in with your admin account
- Click “Add website”
- Enter your domain (e.g.,
selfvps.net) - Copy the tracking script
Deploy the Tracking Script
Add the following code to your website’s <head> tag:
<script defer data-domain="yourdomain.com" src="https://analytics.yourdomain.com/js/script.js"></script>
For Hugo blogs, add to layouts/partials/head.html:
{{ if not .Site.IsServer }}
<script defer data-domain="{{ .Site.BaseURL | urlize }}" src="https://analytics.yourdomain.com/js/script.js"></script>
{{ end }}
Custom Event Tracking
Plausible supports custom events and goal tracking:
// Track button clicks
plausible('Signup', { props: { method: 'Email' }});
// Track 404 pages
plausible('404', { props: { path: document.location.pathname }});
// Track file downloads
document.querySelectorAll('a[href$=".pdf"]').forEach(link => {
link.addEventListener('click', () => {
plausible('Download', { props: { file: link.href }});
});
});
7. Performance Optimization
System-Level Tuning
# ── System-level optimizations ──
# 1. Increase file descriptor limit
echo "fs.file-max = 100000" | sudo tee -a /etc/sysctl.conf
# 2. Network optimizations
cat >> /etc/sysctl.conf << 'EOF'
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fastopen = 3
EOF
sudo sysctl -p
# 3. Set up swap (recommended for 1GB RAM VPS)
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Docker Resource Limits
Set resource limits in docker-compose.yml:
services:
plausible:
# ... other config ...
deploy:
resources:
limits:
memory: 512M
cpus: '0.75'
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
plausible_events_db:
# ... other config ...
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
ClickHouse Performance Tuning
Create clickhouse/clickhouse-performance.xml:
<clickhouse>
<max_memory_usage>300000000</max_memory_usage>
<max_server_memory_usage>400000000</max_server_memory_usage>
<max_concurrent_queries>10</max_concurrent_queries>
<merge_tree>
<max_suspicious_broken_parts>5</max_suspicious_broken_parts>
<parts_to_throw_insert>300</parts_to_throw_insert>
<parts_to_delay_insert>150</parts_to_delay_insert>
</merge_tree>
</clickhouse>
8. Data Backup & Recovery
Automated Backup Script
Create backup.sh:
#!/bin/bash
# Plausible data backup script
BACKUP_DIR="/root/backups/plausible"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30
mkdir -p $BACKUP_DIR
echo "==> Backing up PostgreSQL database..."
docker compose exec -T plausible_db pg_dumpall -U plausible | gzip > "$BACKUP_DIR/plausible_db_$DATE.sql.gz"
echo "==> Backing up ClickHouse data..."
docker compose exec -T plausible_events_db clickhouse-client --query "BACKUP TABLE plausible_events.* TO 'file:///var/lib/clickhouse/backup/backup_$DATE'" 2>/dev/null || {
echo "⚠️ ClickHouse backup failed, falling back to data directory copy..."
tar czf "$BACKUP_DIR/clickhouse_data_$DATE.tar.gz" -C /var/lib/docker/volumes/$(basename $(pwd))_event_data/_data .
}
echo "==> Cleaning old backups (retaining $RETENTION_DAYS days)..."
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
echo "✅ Backup complete! Files in: $BACKUP_DIR"
ls -lh $BACKUP_DIR
chmod +x backup.sh
# Schedule daily backup (3 AM)
(crontab -l 2>/dev/null; echo "0 3 * * * cd ~/plausible && ./backup.sh >> /var/log/plausible-backup.log 2>&1") | crontab -
Restore from Backup
# Stop services
docker compose down
# Restore PostgreSQL
gunzip -c plausible_db_20260605_030000.sql.gz | docker compose exec -T plausible_db psql -U plausible
# Restart services
docker compose up -d
9. Monitoring & Maintenance
Health Checks
# Health check endpoint
curl -s https://analytics.yourdomain.com/health
# Check resource usage
docker stats --no-stream plausible plausible_db plausible_events_db
# View logs
docker compose logs --tail=100 plausible
# Check database connectivity
docker compose exec plausible_db pg_isready -U plausible
Upgrading Plausible
# Backup data first
./backup.sh
# Pull latest images
docker compose pull
# Restart services
docker compose up -d
# Migration runs automatically on startup
docker compose logs -f plausible | grep "migration"
Integrate with Uptime Kuma
# In Uptime Kuma, add a monitor:
# URL: https://analytics.yourdomain.com/health
# Expected status: 200
# Notification: Telegram / Email
10. Troubleshooting
Q: Blank page or 502 error
# Check if services are running
docker compose ps
# View error logs
docker compose logs plausible | tail -50
# Check database connectivity
docker compose exec plausible_db psql -U plausible -d plausible -c "SELECT 1"
# Common cause: Plausible started before database was ready
docker compose restart plausible
Q: ClickHouse out of memory
# Check ClickHouse logs
docker compose logs plausible_events_db | grep -i "memory\|OOM\|error"
# Solution: Limit ClickHouse memory usage
# Edit max_memory_usage in clickhouse-performance.xml
Q: No data showing / tracking not working
# 1. Check if tracking script is deployed correctly
curl -s https://analytics.yourdomain.com/js/script.js | head -5
# 2. Check DNS resolution
dig analytics.yourdomain.com
# 3. Check if Plausible received events
docker compose logs plausible | grep "tracking\|event\|pageview"
# 4. Check for CSP restrictions
# Add to Nginx/Caddy:
# Content-Security-Policy: script-src 'self' https://analytics.yourdomain.com;
Q: Database disk space low
# Check Docker volume usage
docker system df -v
# Check ClickHouse data sizes
docker compose exec plausible_events_db clickhouse-client \
--query "SELECT table, formatReadableSize(sum(bytes)) FROM system.parts WHERE active GROUP BY table"
# Set data retention (add to plausible-conf.env)
# CLICKHOUSE_DATA_RETENTION_DAYS=90
Cost Analysis
| Item | Monthly Cost |
|---|---|
| VPS (1C1G, e.g., RackNerd $18/year) | $1.50 |
Domain (e.g., analytics.yourdomain.com) | $0 (subdomain) |
| Total | ~$1.50/month |
Compared to Plausible Cloud: $9/month (10K page views) → Self-hosting saves 83%!
💡 Pro tip: If you already have a VPS running other services, deploying Plausible on the same machine has near-zero marginal cost.
Summary
You’ve successfully self-hosted Plausible Analytics on your VPS:
✅ Deployed Plausible + PostgreSQL + ClickHouse with Docker Compose ✅ Configured Caddy/Nginx reverse proxy with automatic HTTPS ✅ Installed the tracking script on your website ✅ Set up automated backups and monitoring
Plausible is the best self-hosted alternative to Google Analytics. It protects user privacy while giving you full control over your data — exactly what self-hosting is all about.
Next Steps
- 📊 Set up email reports: Configure SMTP to receive weekly/monthly analytics reports
- 🔗 Integrate Slack/Telegram: Get real-time data push via webhooks
- 🎯 Configure goal tracking: Monitor conversions like signups, downloads, and purchases
- 🚀 Scale to multiple sites: Manage analytics for all your websites from a single instance
Resources
- Plausible Self-Hosting Docs
- Plausible on GitHub
- ClickHouse Documentation
- Caddy Documentation
- Docker Compose V2 Docs
Published on SelfVPS Guide. All rights reserved.
