VPS Tutorials
Self-Hosted Plausible Analytics on a VPS (2026): Docker, PostgreSQL, ClickHouse, Caddy HTTPS and Privacy-Friendly Analytics
Deploy Plausible Analytics on a VPS from scratch: run the app, PostgreSQL and ClickHouse with Docker Compose, configure Caddy HTTPS, SMTP, privacy-friendly analytics, backup and restore, upgrades and rollbacks, and common troubleshooting.

Self-Hosted Plausible Analytics on a VPS (2026): Docker, PostgreSQL, ClickHouse, Caddy HTTPS and Privacy-Friendly Analytics
If you want a lightweight alternative to Google Analytics and don't want your website's visitor data to leave your own VPS, Plausible Analytics is a good fit for small websites, indie developers and content sites. Its interface is clean, it doesn't build cross-site personal profiles by default, and its tracking script is lighter than traditional analytics tools.
However, self-hosting Plausible isn't just about running a single container. A production environment has to handle PostgreSQL, ClickHouse, persistent volumes, domain HTTPS, an admin email, backups, and upgrade rollbacks. This article uses Ubuntu 24.04 LTS, Docker Compose and Caddy as an example to deploy a Plausible Community Edition that can run long term.
| Tool | Better suited for | Main trade-offs |
|---|---|---|
| Plausible | Simple, fast, privacy-friendly website analytics | Fewer in-depth reports and less plugin ecosystem than Matomo |
| Umami | Only tracking visits, sources, pages and events, with low maintenance cost | Less advanced analytics and data processing |
| Matomo | Needing goals, funnels, ecommerce, site search and detailed visitor analytics | More complex database, archiving and upgrade maintenance |
If you only need lightweight analytics, you can start with the self-hosted Umami website analytics tutorial on a VPS. If your site already uses Matomo, you can compare against the Matomo setup tutorial on a VPS to assess migration costs. Plausible's advantage is that its default reports are clear enough, and after deployment you don't need to maintain extra browser archiving tasks.
Plausible uses the web app, PostgreSQL and ClickHouse at the same time. A small blog or business site can start with the following configuration:
| Scale | Recommended configuration | Suitable for |
|---|---|---|
| Up to 100,000 page views per month | 2 cores, 4GB RAM, 50GB SSD | Personal blogs, landing pages, small business sites |
| 100,000 to 1,000,000 per month | 4 cores, 8GB RAM, 150GB SSD or more | Multiple sites, sites running continuous ad campaigns |
| Over 1,000,000 | Load test first, then split databases or scale up | News sites, SaaS and high-traffic content sites |
Disk space is mainly consumed by ClickHouse event data and PostgreSQL configuration data. Before going live, prepare a dedicated domain, such as analytics.example.com, and point its A / AAAA records to the VPS. Open only SSH, 80 and 443 in the firewall, and do not map PostgreSQL's 5432 or ClickHouse's 8123 to the public internet.
Install Docker Engine and Compose v2 according to the official Docker documentation, then confirm the versions:
docker --version
docker compose version
Create the deployment directory and backup directory:
sudo mkdir -p /opt/plausible/backups
sudo chown -R "$USER":"$USER" /opt/plausible
cd /opt/plausible
Generate three random values and write them into .env:
openssl rand -hex 32
openssl rand -hex 32
openssl rand -hex 32
nano .env
chmod 600 .env
BASE_URL=https://analytics.example.com
SECRET_KEY_BASE=替换为第一组随机值
POSTGRES_PASSWORD=替换为第二组随机值
CLICKHOUSE_PASSWORD=替换为第三组随机值
DATABASE_URL=postgres://plausible:替换为第二组随机值@postgres:5432/plausible
CLICKHOUSE_DATABASE_URL=http://plausible:替换为第三组随机值@clickhouse:8123/plausible_events
BASE_URL must exactly match the final access address. Once SECRET_KEY_BASE is in production, don't change it casually, or existing sessions and encrypted data will become invalid.
Create /opt/plausible/compose.yaml:
services:
plausible:
image: ghcr.io/plausible/community-edition:v3.2.1
container_name: plausible
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "127.0.0.1:8000:8000"
env_file:
- .env
environment:
DISABLE_REGISTRATION: invite_only
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 10
start_period: 60s
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
postgres:
image: postgres:16-alpine
container_name: plausible-postgres
restart: unless-stopped
env_file:
- .env
environment:
POSTGRES_DB: plausible
POSTGRES_USER: plausible
volumes:
- plausible_postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U plausible -d plausible"]
interval: 10s
timeout: 5s
retries: 20
clickhouse:
image: clickhouse/clickhouse-server:24.8-alpine
container_name: plausible-clickhouse
restart: unless-stopped
env_file:
- .env
environment:
CLICKHOUSE_DB: plausible_events
CLICKHOUSE_USER: plausible
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1"
volumes:
- plausible_clickhouse:/var/lib/clickhouse
- plausible_clickhouse_logs:/var/log/clickhouse-server
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8123/ping | grep -q Ok"]
interval: 10s
timeout: 5s
retries: 20
volumes:
plausible_postgres:
name: plausible_postgres
plausible_clickhouse:
name: plausible_clickhouse
plausible_clickhouse_logs:
name: plausible_clickhouse_logs
The app port is bound only to 127.0.0.1:8000, and the databases stay entirely on the Compose internal network. The example pins Plausible Community Edition v3.2.1 and the ClickHouse 24.8 series; before any official upgrade, still check the Plausible release notes and compatibility matrix, and don't use latest directly in an unattended environment.
Start and check:
docker compose config >/dev/null
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 plausible
curl -I http://127.0.0.1:8000
sudo ss -lntp | grep ':8000'
If you see 0.0.0.0:8000, the port binding is wrong and should be changed back to 127.0.0.1 immediately.
Edit /etc/caddy/Caddyfile:
analytics.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8000
request_body {
max_size 16MB
}
}
Validate and reload:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://analytics.example.com
Caddy automatically obtains and renews certificates. If you use Cloudflare, keep DNS, proxy mode and origin port consistent, and don't configure conflicting forced redirects across Caddy, the CDN and the application layer. For more details, see Caddy reverse proxy and automatic HTTPS on a VPS.
Open https://analytics.example.com in your browser and create the first admin account. Use a dedicated email and a long password generated by a password manager. After that, add a website, fill in the site domain, report time zone and the events you need to collect, and keep public registration disabled.
Copy the JavaScript code provided by Plausible and place it before the closing head tag of your website template. If you use CSP, at least allow the script loading domain and the analytics reporting domain; if a caching plugin merges or defers the script, exclude the Plausible script first.
Visit the website in an incognito window, confirm in the browser Network tab that a request to /api/event appears, then verify in the Plausible real-time panel. The homepage loading doesn't mean the tracking script is working.
Inviting members, resetting passwords and alert notifications depend on email. Add the following to .env according to the SMTP service you use:
[email protected]
SMTP_HOST_ADDR=smtp.example.com
SMTP_HOST_PORT=587
[email protected]
SMTP_USER_PWD=替换为SMTP专用密码
SMTP_HOST_SSL_ENABLED=false
After changing it, rebuild the app container:
docker compose up -d --force-recreate plausible
docker compose logs --tail=100 plausible
Port 587 usually works with STARTTLS, and 465 usually uses direct TLS; refer to your email provider's documentation for specifics. Don't use your personal email account's main password.
Plausible doesn't rely on cookies or cross-site personal profiling by default, but self-hosting doesn't automatically mean compliance. Before going live, check:
- The privacy policy explains which pages, sources and device information are collected, and how long they are kept;
- Don't put personal information such as emails, names or order numbers into URLs, event names or custom properties;
- Only create the custom events you actually need, and avoid reporting form contents as-is;
- If your region or business scenario requires consent, pause non-essential analytics until consent is given;
- Regularly delete sites and members you no longer need, and protect admin accounts;
- Restrict entry points through the firewall and Caddy, and don't expose database ports to the public internet.
"Cookie-free" reduces privacy risk, but you still need to make a complete judgment based on access logs, IP handling, ad platforms and the region where your business operates.
PostgreSQL stores accounts, sites and configuration, while ClickHouse stores visit events. If you back up only one of them, after a restore you'll get either "can log in but historical reports are empty" or "data exists but site configuration is lost".
Back up PostgreSQL:
cd /opt/plausible
mkdir -p backups
docker compose exec -T postgres pg_dump \
-U plausible -d plausible --format=custom \
> "backups/plausible-postgres-$(date +%F-%H%M).dump"
When backing up the ClickHouse data volume, stop writes first:
docker compose stop plausible clickhouse
docker run --rm \
-v plausible_clickhouse:/source:ro \
-v "$PWD/backups:/backup" \
alpine sh -c 'tar -C /source -czf /backup/plausible-clickhouse.tar.gz .'
docker compose start clickhouse plausible
Keep at least one copy of the backup files on another machine or in object storage, and restrict access permissions. You can refer to VPS backup and restore drills for encryption, checksums and regular restore tests.
After a restore, check the site list, historical reports, real-time events and admin login; don't just check whether the container is running.
Before upgrading, save the current image version, the Compose file, and PostgreSQL and ClickHouse backups. Perform the upgrade during off-peak hours:
cd /opt/plausible
docker compose config >/dev/null
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=200 plausible
Only end the upgrade window after confirming that the admin panel, tracking script, real-time events and historical reports all work normally. If the new version fails to start, restore the old image tag and data backups. Don't run docker compose down -v without a backup, because it deletes the database volumes.
Monitor resources regularly:
docker stats --no-stream plausible plausible-postgres plausible-clickhouse
df -h
docker system df
ClickHouse disk growth is usually faster than PostgreSQL. Set alerts for the Docker data disk, backup directory and logs, and keep at least 20% free space.
docker compose ps
docker compose logs --tail=200 plausible
curl -I http://127.0.0.1:8000
sudo journalctl -u caddy --no-pager -n 100
While the app is still starting, Caddy will temporarily return 502; if it persists, check SECRET_KEY_BASE, BASE_URL, database connections and container memory.
docker compose logs --tail=200 postgres clickhouse
docker compose exec plausible getent hosts postgres clickhouse
The hostnames on the Compose network are the service names postgres and clickhouse, and you can't use localhost. Changing the password in .env won't automatically change the already-initialized database user; you must update the database account in sync or follow the backup restore procedure.
In the browser Network tab, check the status code of /api/event, the request domain and CSP errors. Common causes include a wrong site domain, the script being deferred by a caching plugin, ad blockers blocking the request, and the website using the wrong HTTP/HTTPS address. First use an incognito window to rule out the influence of browser extensions.
docker compose exec clickhouse clickhouse-client --query 'SELECT 1'
docker compose logs --tail=200 clickhouse
df -h /var/lib/docker
Check whether ClickHouse is healthy, whether the disk is full, and whether there are migration errors after an upgrade. If only a certain time period is missing, use the container logs and Caddy access logs to determine whether events weren't reported or the analytics database didn't write them.
- The app listens only on 127.0.0.1:8000, and 5432 and 8123 are not exposed to the public internet
- BASE_URL, the domain and the Caddy HTTPS configuration are consistent
- The admin uses a strong password, and the registration policy stays invite-only
- SMTP invitation and password reset emails have been tested
- The browser can see /api/event, and the real-time panel receives test visits
- The privacy policy, data retention, custom events and consent mechanism have been confirmed
- Both PostgreSQL and ClickHouse have off-site backups
- At least one restore drill has been completed, and historical reports can be restored
- Versions are pinned and backed up before upgrades, and 502 errors, events and disk space are checked after upgrades
Only after completing these steps is Plausible a VPS analytics service that can run long term, rather than a Docker demo only suitable for a temporary trial. For websites that value privacy, page speed and data autonomy, it strikes a good balance between deployment complexity and analytics capability.