[$ xmrhost] _

$ xmrhost-cli docs show --topic=setup-plausible-self-host

[$ ] doc: setup-plausible-self-host

// Self-host Plausible Analytics on an offshore VPS — Docker compose, Caddy TLS, brand-distinct deployment

// published=2026-05-11 · updated=2026-05-11 · diff=intermediate · read=22min · tags=[plausible, analytics, self-host, docker, caddy, privacy] · by=0xLambda


// ABSTRACT

abstract

Full procedure for running Plausible Analytics Community Edition on a small offshore VPS, fronted by Caddy with direct TLS (no Cloudflare). Covers the Docker-compose layout, the env-var matrix, the events-database (ClickHouse) tuning for a single-tenant deployment, the brand-domain configuration, and the post-launch hardening checks. Aimed at small operators running a single analytics instance per brand. Plausible CE is AGPLv3 — self-hosting is permitted; redistribution under modified terms requires AGPL compliance.

Scope and assumptions

Self-hosted Plausible Analytics Community Edition (CE) on a dedicated offshore VPS, fronted by Caddy with direct Let’s Encrypt TLS (no Cloudflare, no CDN-edge layer — those would defeat the privacy-tech posture this runbook is wired into). Single-tenant deployment: one Plausible instance per brand domain. Plausible CE is AGPLv3, the official self-host route, maintained by Plausible Insights ehf (Iceland).

This runbook is opinionated:

  • Dedicated VPS, not co-located. Plausible-ClickHouse-Postgres on the same machine as the production app surface contaminates the threat model. Pick a separate VPS for the analytics stack — vps-1 ($15/mo) is enough for a single brand under 100k pageviews/month.
  • No Cloudflare. The brand voice across /legal/privacy and /threat-models#matrix-homeserver rules out US-incorporated TLS-terminating intermediaries. Caddy serves direct.
  • No third-party analytics on top of Plausible. The point of self-hosting analytics is that no third party sees the visitor data. Layering Google Analytics or Hotjar on top defeats the deployment entirely.
  • Domain on a brand-distinct subdomain. plausible.<brand>.io or stats.<brand>.io — never the apex. Reduces cross-domain cookie / fingerprinting concerns.

Step 1 — provision the VPS

Order a vps-1 plan at /node/vps/vps-1. $15/mo, 1 vCPU / 2 GB RAM / 30 GB NVMe in Iceland or Romania. Pay in Monero per /guide/buy-vps-with-monero — the analytics infrastructure should be on the same payment posture as the production brand.

Standard provisioning ships Debian 12 (bookworm), Ed25519-only sshd (/docs/harden-sshd), KSPP-baseline kernel, auditd, restrictive nftables. Plausible runs in containers on top of this baseline.

Step 2 — DNS

Point a brand-distinct subdomain at the VPS IP:

# at your DNS host (Njalla, Orange, NameSilo, etc.) A plausible.<brand>.io -> <vps-ip> AAAA plausible.<brand>.io -> <vps-ipv6>

TTL 300s during the bring-up so you can fix mistakes fast. Bump to 3600s after the cert provisions.

Step 3 — install Docker + Compose

# minimal Docker install on Debian 12 apt update && apt install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | \ gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \ > /etc/apt/sources.list.d/docker.list apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin systemctl enable --now docker

Step 4 — clone Plausible’s self-host repo

The official self-host repository is maintained at github.com/plausible/community-edition. As of 2026 the canonical clone path:

adduser --system --group --home /opt/plausible plausible cd /opt/plausible git clone --depth 1 https://github.com/plausible/community-edition.git cd community-edition git checkout v3.x.x # check the latest release at the upstream

Step 5 — configure the env file

The repo ships a plausible-conf.env.example. Copy and edit:

cp plausible-conf.env.example plausible-conf.env chmod 0600 plausible-conf.env

Edit the values that matter:

# /opt/plausible/community-edition/plausible-conf.env

# REQUIRED — base URL where the dashboard lives
BASE_URL=https://plausible.<brand>.io

# REQUIRED — random base64 secrets (generate fresh, do NOT reuse)
SECRET_KEY_BASE=<openssl rand -base64 48>
TOTP_VAULT_KEY=<openssl rand -base64 32>

# OPTIONAL — disable registration after creating the first admin account
DISABLE_REGISTRATION=invite_only

# OPTIONAL — outbound email (for invites / password reset)
# If left blank, Plausible silent-skips email delivery
MAILER_EMAIL=noreply@<brand>.io
SMTP_HOST_ADDR=smtp.<your-relay>.io
SMTP_HOST_PORT=587
SMTP_USER_NAME=<smtp-user>
SMTP_USER_PWD=<smtp-pass>

# OPTIONAL — IP geolocation database
# Plausible CE supports MaxMind GeoLite2 via a separate license; for
# privacy-focused deployments, leave blank — country-level resolution
# isn't necessary for most operators
MAXMIND_LICENSE_KEY=

# OPTIONAL — disable subscription / billing features (CE has no billing,
# but the env var must be set to avoid the upgrade UI surfacing)
ENABLE_EMAIL_VERIFICATION=false

Generate the secrets:

openssl rand -base64 48 # SECRET_KEY_BASE openssl rand -base64 32 # TOTP_VAULT_KEY

Step 6 — set up Caddy ingress

Plausible’s official compose file binds the app on port 8000 by default. Front it with Caddy (already running on the brand’s standard offshore image):

cat >> /etc/caddy/Caddyfile <<'EOF' plausible.<brand>.io { 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-Proto {scheme} } 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 } } EOF systemctl reload caddy

Verify the cert provisions (Caddy hits Let’s Encrypt direct, no Cloudflare in the way):

journalctl -u caddy -n 50 | grep -i "obtain\|certificate" # expect: 'obtained certificate' for plausible.<brand>.io curl -sI https://plausible.<brand>.io/api/health # expect: 200 OK (after the app boots in step 7)

Step 7 — boot the stack

cd /opt/plausible/community-edition docker compose pull docker compose up -d docker compose logs -f plausible 2>&1 | head -40

First boot does the Postgres migration + the ClickHouse schema apply. Takes ~30-60 seconds. Watch for Started application in the logs — that’s the readiness signal.

If migrations fail, the most common cause is a stale plausible-conf.env that pre-dates the version pinned in step 4. Re-read the release notes and re-apply.

Step 8 — create the admin account

Browse to https://plausible.<brand>.io/register. Create the first admin account. Once that’s done, edit plausible-conf.env and set:

DISABLE_REGISTRATION=disable

Then docker compose up -d to apply. This locks the registration form so no other accounts can be created against the public URL.

Step 9 — add the site + paste the snippet

In the Plausible dashboard, click “Add a new website”. Enter your brand domain (e.g. xmrhost.io — NOT the Plausible subdomain; the domain you want to track). Plausible generates a snippet:

<script defer
  data-domain="<your-brand>.io"
  src="https://plausible.<brand>.io/js/script.js">
</script>

Paste in your site’s <head> — for an Astro site, this goes in src/layouts/Base.astro next to the existing <OrganizationJsonLd /> block. For a static site, in index.html. For XMRHost-family brands, the convention is to gate the snippet behind a PUBLIC_PLAUSIBLE_DOMAIN env var and inject only when set.

Visit the brand domain in a clean browser tab; refresh; check the Plausible dashboard. The first pageview lands in ~30 seconds.

Step 10 — hardening checks (post-launch)

# 1. cert is valid + auto-renewing curl -sI https://plausible.<brand>.io | grep -i strict-transport # 2. no third-party requests from the snippet curl -s https://plausible.<brand>.io/js/script.js | grep -v plausible.<brand>.io || echo "ok" # 3. ClickHouse not exposed on the public IP ss -tlnp | grep 9000 | grep -v 127.0.0.1 && echo "EXPOSED" || echo "ok" # 4. Postgres not exposed ss -tlnp | grep 5432 | grep -v 127.0.0.1 && echo "EXPOSED" || echo "ok" # 5. Plausible app not directly reachable from public IP ss -tlnp | grep 8000 | grep -v 127.0.0.1 && echo "EXPOSED" || echo "ok" # 6. Docker socket not exposed ls -l /var/run/docker.sock # expect: srw-rw---- root docker

All 6 must pass. If any return EXPOSED, the docker-compose-binding default leaked a service to 0.0.0.0; fix by setting 127.0.0.1: prefix on every ports: entry in the compose file.

Step 11 — backup strategy

Plausible’s state lives in two databases:

# Postgres — user accounts, site configuration, dashboard state docker compose exec plausible_db pg_dump -U postgres plausible_db \ | gzip > /backup/plausible-pg-$(date -u +%Y%m%d).sql.gz # ClickHouse — the events database (the heavy data) docker compose exec plausible_events_db clickhouse-backup create \ events-$(date -u +%Y%m%d) docker compose exec plausible_events_db clickhouse-backup upload \ events-$(date -u +%Y%m%d)

Rsync the backup directory off-host to a separate VPS in a different region (Romania backup of Iceland Plausible, or vice versa — same pattern as the brand’s archive backup, see /guide/offshore-hosting-for-journalists).

Step 12 — upgrade path

Plausible CE releases roughly every 6-8 weeks. The upgrade path:

cd /opt/plausible/community-edition docker compose down git pull origin master git checkout v3.y.y # the new tag — read release notes first docker compose pull docker compose up -d docker compose logs -f plausible 2>&1 | head -30

Read the release notes before each upgrade. Plausible has had two breaking schema-migration events in the CE lifetime; both required a manual clickhouse migration step the standard docker compose up did not handle. The release notes flag these.

Step 13 — privacy properties of the deployment

What this deployment does NOT do, by design:

  • No cookies, no localStorage. Plausible’s script uses no client-side state; visitor identification is by daily-rotating hash of (IP + UA + salt), never reaching individual identity.
  • No IP retention. The hashing salt rotates daily; the IP is not stored in the events DB.
  • No cross-site fingerprinting. Self-hosted means no third party sees any visitor data. The events stay on the VPS.
  • No cookie-consent banner required in most jurisdictions (CNIL, ICO, EDPB guidance all treat properly-configured Plausible as exempt). Verify against your operating jurisdiction’s regulator. Plausible’s docs cover the GDPR / CCPA / PECR alignment.
  • No outbound to plausible.io. The self-host stack does NOT phone home to the SaaS Plausible. Verifiable in step 10 check #2.

See also

Upstream references:

// END OF DOC

$ cd /docs # back to the manual