What you will set up
Plausible counts the visitors to your websites: which pages they read, where they came from, and which country and kind of device they use. It sets no cookies and stores no IP addresses: in our test, the script left no cookies or browser storage behind, and the statistics database has no column for an address. Your sites need no cookie banner for it. Hosted on your own server in Sweden, the statistics stay on that server.
Plausible Community Edition (CE) is developed by Plausible Insights OÜ, a company in Tartu, Estonia, which also sells Plausible as a hosted service. CE is open source under the GNU Affero General Public License (AGPL) 3.0, and the small tracking script that goes on your pages is under the MIT licence. CE has no telemetry or update check. By default it does fetch a spam list and time zone updates, and it sends referring site names to DuckDuckGo and a hash of your email address to Gravatar, both US companies; step 8 lists what goes where, and how to switch off the last two.
Here it runs as a pod under a user of its own called plausible, behind Caddy from the Podman guide: Plausible itself, PostgreSQL for accounts and settings, and ClickHouse for the visitor statistics.
Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:
- Plausible CE 3.2.1 started with the PostgreSQL and ClickHouse versions that Plausible's own setup uses.
- The owner account was created while nobody else could reach the sign-up page, and after that
/registersent everyone to the login page, over IPv4 and IPv6. - A small website with Plausible's script, visited in a browser, showed up in the dashboard with the visitor's country, and Plausible saw the visitor's real IP address.
- A visitor who sent a made-up IP address in a header was still counted with their real one.
- Both databases were restored from the nightly backup, with every account, site and visit.
- Everything came back by itself after a reboot.
The pod used about 600 MB of memory: Plausible 330 MB, ClickHouse 230 MB and PostgreSQL 30 MB.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running;
- an A record and an AAAA record for
plausible.example.compointing at your server; - a website to measure, here
www.example.com.
Plausible recommends at least 2 GB of memory. ClickHouse needs a processor with SSE 4.2, which every Melonslab server has; grep -m1 -o sse4_2 /proc/cpuinfo prints sse4_2 if yours does.
Replace plausible.example.com with your own name throughout. The examples use 203.0.113.10 for your server and 198.51.100.7 for your own address at home or at the office.
1. Create the user
As root:
useradd -m -s /bin/bash plausible
loginctl enable-linger plausible
machinectl shell plausible@
Everything up to step 5 runs as plausible.
2. Create the secrets
pw=$(openssl rand -hex 24)
printf %s "$pw" | podman secret create plausible-db-password -
printf 'postgres://postgres:%s@127.0.0.1:5432/plausible_db' "$pw" | podman secret create plausible-database-url -
unset pw
openssl rand -base64 48 | tr -d '\n' | podman secret create plausible-secret-key-base -
The first two hold the database password, once for PostgreSQL and once in the address Plausible connects to. The third, SECRET_KEY_BASE, signs logins and encrypts the two-factor codes. It has to move with your data if you ever move Plausible to another server.
3. Get ClickHouse's settings
Plausible's own setup gives ClickHouse four small configuration files: one keeps its logs short, one makes it listen on IPv4 only inside the pod, and two make it use less memory, as ClickHouse recommends for servers with less than 16 GB. Download them from the same version:
mkdir -p ~/clickhouse && cd ~/clickhouse
for f in logs ipv4-only low-resources default-profile-low-resources-overrides; do
wget -q https://raw.githubusercontent.com/plausible/community-edition/v3.2.1/clickhouse/$f.xml
done
Add a fifth, ~/clickhouse/backups.xml, which lets ClickHouse write backups (step 10):
<clickhouse>
<backups>
<allowed_path>/backups/</allowed_path>
</backups>
</clickhouse>
ClickHouse runs as user 101 inside its container, so give it the backup directory:
mkdir -p ~/backup/clickhouse
podman unshare chown 101:101 ~/backup/clickhouse
4. Define the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create plausible.pod:
[Pod]
PodName=plausible
# Only Caddy, on this server, can reach Plausible: the port is not open to the internet.
PublishPort=127.0.0.1:8110:8000
[Install]
WantedBy=default.target
Create plausible-db.container:
[Container]
ContainerName=plausible-db
Image=docker.io/library/postgres:16-alpine
Pod=plausible.pod
Volume=plausible-db:/var/lib/postgresql/data
Secret=plausible-db-password,type=env,target=POSTGRES_PASSWORD
HealthCmd=pg_isready -U postgres
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
Create plausible-events.container:
[Container]
ContainerName=plausible-events
Image=docker.io/clickhouse/clickhouse-server:24.12-alpine
Pod=plausible.pod
Volume=plausible-events:/var/lib/clickhouse
Volume=plausible-events-logs:/var/log/clickhouse-server
Volume=%h/clickhouse/logs.xml:/etc/clickhouse-server/config.d/logs.xml:ro
Volume=%h/clickhouse/ipv4-only.xml:/etc/clickhouse-server/config.d/ipv4-only.xml:ro
Volume=%h/clickhouse/low-resources.xml:/etc/clickhouse-server/config.d/low-resources.xml:ro
Volume=%h/clickhouse/default-profile-low-resources-overrides.xml:/etc/clickhouse-server/users.d/default-profile-low-resources-overrides.xml:ro
Volume=%h/clickhouse/backups.xml:/etc/clickhouse-server/config.d/backups.xml:ro
Volume=%h/backup/clickhouse:/backups
Environment=CLICKHOUSE_SKIP_USER_SETUP=1
Ulimit=nofile=262144:262144
HealthCmd=wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping
Notify=healthy
AutoUpdate=registry
[Service]
Restart=always
And plausible-app.container:
[Unit]
Requires=plausible-db.service plausible-events.service
After=plausible-db.service plausible-events.service
[Container]
ContainerName=plausible-app
Image=ghcr.io/plausible/community-edition:v3.2.1
Pod=plausible.pod
Exec=sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
Volume=plausible-data:/var/lib/plausible
Environment=TMPDIR=/var/lib/plausible/tmp
Environment=BASE_URL=https://plausible.example.com
Environment=CLICKHOUSE_DATABASE_URL=http://127.0.0.1:8123/plausible_events_db
Environment=DISABLE_REGISTRATION=invite_only
Secret=plausible-database-url,type=env,target=DATABASE_URL
Secret=plausible-secret-key-base,type=env,target=SECRET_KEY_BASE
Ulimit=nofile=65535:65535
[Service]
Restart=always
The images and versions are the ones in Plausible's own compose.yml for 3.2.1. Notify=healthy makes each database count as started only once it answers, so Plausible starts after both are ready. At every start, Plausible creates its databases if they are missing and updates them to its version. PostgreSQL and ClickHouse follow the updates of their pinned versions by themselves; Plausible stays on the exact version until you change it (step 11).
Start it:
systemctl --user daemon-reload
systemctl --user start plausible-pod
systemctl --user enable --now podman-auto-update.timer
The first start downloads about 1 GB of images and takes a few minutes. When wget -qO- http://127.0.0.1:8110/login | head -c 100 prints the start of a web page, Plausible is running.
5. Create your account before anyone else
Until the first account exists, Plausible lets whoever opens it first create an account, and that account owns the installation. DISABLE_REGISTRATION does not change that: we tested it set to true, and /register was still open, because Plausible only applies the setting once an account exists. So let Caddy show Plausible only to you until your account is made.
Find the address the server sees for you. In your SSH session to the server:
echo $SSH_CLIENT | cut -d' ' -f1
Go back to root with exit, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile, with that address:
plausible.example.com {
# Until your account exists, only your own address gets through.
@others not remote_ip 198.51.100.7
respond @others "Not yet" 403
reverse_proxy 127.0.0.1:8110
}
If your computer also has IPv6, your browser may use it instead: add your IPv6 address or network after the IPv4 one, separated by a space, such as 198.51.100.7 2001:db8:1234::/48. Restart Caddy with systemctl --user restart caddy.
Open https://plausible.example.com. It goes straight to Register your Plausible CE account. Fill in Full name, Email, Password (at least 12 characters) and Confirm password, and choose Create my account. Everyone else gets Not yet.
An SSH tunnel to port 8110 does not work for this step: the sign-up page needs a WebSocket connection, and Plausible refuses it from any address other than BASE_URL (the browser's console shows Unexpected response code: 403).
6. Open Plausible to everyone
As caddy, replace the block with this one:
plausible.example.com {
# Plausible takes the visitor's address from the first of these headers it finds.
# Only X-Forwarded-For, which Caddy sets, may reach it.
request_header -X-Plausible-IP
request_header -CF-Connecting-IP
request_header -B-Forwarded-For
request_header -Forwarded
reverse_proxy 127.0.0.1:8110
}
Restart Caddy with systemctl --user restart caddy.
Plausible has no trusted-proxy setting. It believes the headers X-Plausible-IP, CF-Connecting-IP, B-Forwarded-For, X-Forwarded-For and Forwarded, in that order, from anyone. Caddy replaces any X-Forwarded-For a visitor sends with the address the visitor really connects from, and the four request_header lines remove the other headers. In our test, an event sent with X-Plausible-IP: 8.8.8.8 through Caddy was counted from Sweden, where it came from, while the same request sent past Caddy, straight to port 8110, was counted from the United States.
With DISABLE_REGISTRATION=invite_only, Plausible's default, only people you invite can create an account. Check it from a computer other than your own, or from the server:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://plausible.example.com/register
302 https://plausible.example.com/login
If you will never invite anyone, set DISABLE_REGISTRATION=true in plausible-app.container instead: then invitation links do not work either.
7. Add your website
In Plausible, under Add website info, enter your site's Domain, such as www.example.com, pick your Reporting timezone, and choose Install Plausible. Plausible shows a snippet like this, with an ID of its own for each site:
<!-- Privacy-friendly analytics by Plausible -->
<script async src="https://plausible.example.com/js/pa-XXXXXXXXXXXXXXXXXXXXXX.js"></script>
<script>
window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};
plausible.init()
</script>
Paste it into the <head> of every page on your site. If the site is served by the same Caddy, a static site needs no more than a block such as:
www.example.com {
root * /srv/www
file_server
}
with the site's directory mounted into Caddy's container, such as Volume=%h/www:/srv/www:ro in caddy.container, followed by systemctl --user daemon-reload and a restart of Caddy.
Open your site in a browser, then the dashboard. The visit appears within a few seconds, under Top pages, Countries and Devices. To check that Plausible sees your real address, open the site's Settings, Shields: IP addresses: the page says Your current IP address is followed by your own address, not the server's.
The script sends nothing from browsers that say they are automated, such as headless Chrome, and Plausible drops visits from user agents it knows as bots. Test with an ordinary browser.
Plausible CE finds the country from DB-IP's free country database, which is built into the image when Plausible publishes a version. It makes no lookups online, and it gives countries only: Regions and Cities stay empty. A newer version of the image brings a newer database. For regions and cities, Plausible can use MaxMind's GeoLite2 instead, which needs a free MaxMind account and about 1 GB more memory; set MAXMIND_LICENSE_KEY (as a secret) and MAXMIND_EDITION=GeoLite2-City, and Plausible downloads and updates the database from MaxMind. We did not test MaxMind.
8. Know what it connects to
We watched the server's outbound connections while Plausible started, counted visits and showed the dashboard. Plausible CE sends no telemetry and has no update check, and its code sends Sentry error reports only if you set SENTRY_DSN. It does contact these services:
- raw.githubusercontent.com (GitHub, a US company owned by Microsoft): at every start and then once a week, it downloads Matomo's public list of referrer spam domains, so that spam visits are not counted. It sends nothing but the request.
- data.iana.org (IANA, run by the US-based ICANN): once a day, its time zone library checks for a new time zone database. It sends nothing but the request. This one comes from Plausible's code; the daily check did not fall within our capture.
- icons.duckduckgo.com (DuckDuckGo, a US company): when the dashboard shows where visitors came from, the server fetches the icon of each referring site, so DuckDuckGo learns the names of the sites that link to you. Your visitors' browsers and addresses are not involved.
- www.gravatar.com (Gravatar, run by the US company Automattic): when you open the dashboard, the server fetches your profile picture by an MD5 hash of your email address.
To switch off the last two, add these lines to plausible.pod, under PublishPort:
# Keep account emails and referrer names away from Gravatar and DuckDuckGo.
AddHost=www.gravatar.com:127.0.0.1
AddHost=icons.duckduckgo.com:127.0.0.1
Then run systemctl --user daemon-reload and systemctl --user restart plausible-pod. Sources then show a plain placeholder icon, and your profile picture a broken image. The spam list and the time zone check have no setting, and we left them on.
9. Email
Plausible sends email for invitations, password resets and weekly or monthly reports. Without an SMTP server, it tries to deliver straight to the recipient's mail server on port 25, which fails from most servers and lands in spam from the rest. On our test server:
- Forgot password? said that the email was sent, and the log,
podman logs plausible-app, showed(Mua.TransportError) timeouthalf a minute later. Nothing arrived. - Invite new guest waited about a minute, saved the invitation, and sent nothing, so the person invited never gets the link.
Logging in, tracking and the dashboard work without email. If you lose your password without email, you need a backup, or to reset it in the database.
To send through a mail server, such as your own, store its password as a secret and add the settings to plausible-app.container:
printf %s 'the-smtp-password' | podman secret create plausible-smtp-password -
Environment=MAILER_EMAIL=plausible@example.com
Environment=SMTP_HOST_ADDR=mail.example.com
Environment=SMTP_HOST_PORT=587
Environment=SMTP_USER_NAME=plausible@example.com
Secret=plausible-smtp-password,type=env,target=SMTP_USER_PWD
For port 465, add Environment=SMTP_HOST_SSL_ENABLED=true. Then systemctl --user daemon-reload and systemctl --user restart plausible-app. These are Plausible's documented settings; we did not test them against a mail server.
10. Back up
Plausible keeps accounts and settings in PostgreSQL and the statistics in ClickHouse. Both can be backed up while Plausible runs. As plausible, create ~/backup.sh:
#!/bin/sh
# Dumps Plausible's two databases and its secrets into ~/backup.
set -e
umask 077
cd ~/backup
podman exec plausible-db pg_dump -U postgres plausible_db > plausible-db.sql
podman unshare rm -rf clickhouse/events
podman exec plausible-events clickhouse-client -q "BACKUP DATABASE plausible_events_db TO File('events')" > /dev/null
for s in plausible-db-password plausible-database-url plausible-secret-key-base; do
printf '%s=%s\n' "$s" "$(podman secret inspect --showsecret --format '{{.SecretData}}' "$s")"
done > secrets.txt
Run it every night with a timer. Create ~/.config/systemd/user/plausible-backup.service:
[Unit]
Description=Back up Plausible's databases
[Service]
Type=oneshot
ExecStart=%h/backup.sh
And ~/.config/systemd/user/plausible-backup.timer:
[Unit]
Description=Back up Plausible every night
[Timer]
OnCalendar=*-*-* 03:00
Persistent=true
[Install]
WantedBy=timers.target
chmod 700 ~/backup.sh
systemctl --user daemon-reload
systemctl --user enable --now plausible-backup.timer
systemctl --user start plausible-backup.service
ls ~/backup
ls shows clickhouse, plausible-db.sql and secrets.txt. secrets.txt holds the passwords and SECRET_KEY_BASE, so keep the backup as safe as the server. To get it off the server every night, use restic on /home/plausible/backup.
To restore, put the secrets back with podman secret create, start the pod so that Plausible creates its databases, stop plausible-app, and replace both databases with the backups:
systemctl --user stop plausible-app
podman exec plausible-db dropdb -U postgres plausible_db
podman exec plausible-db createdb -U postgres plausible_db
podman exec -i plausible-db psql -q -U postgres plausible_db < ~/backup/plausible-db.sql
podman exec plausible-events clickhouse-client -q "DROP DATABASE plausible_events_db SYNC"
podman exec plausible-events clickhouse-client -q "RESTORE DATABASE plausible_events_db FROM File('events')"
Then start it again with systemctl --user start plausible-app. We ran these steps on the test server: the account, the site and every visit came back, and logging in worked.
11. Update
PostgreSQL and ClickHouse update within their pinned versions through podman-auto-update.timer, and podman auto-update --dry-run shows what it would do. Plausible itself stays on the exact version in plausible-app.container, as Plausible recommends, because some versions need extra steps. When a new version is out, read its notes on Plausible's releases page, back up (step 10), then change the version and restart:
cd ~/.config/containers/systemd
sed -i 's|community-edition:v3.2.1|community-edition:v3.2.2|' plausible-app.container
systemctl --user daemon-reload
systemctl --user restart plausible-app
podman image rm ghcr.io/plausible/community-edition:v3.2.1
Plausible migrates its databases by itself when it starts. Also compare the PostgreSQL and ClickHouse versions in the new version's compose.yml with yours: moving PostgreSQL to a new major version means dumping and restoring its data. At the time of writing, 3.2.1 is the latest version, so we could not test an update to a newer one.
Troubleshooting
Someone else can sign up. No account existed yet when the Caddy block without the remote_ip lines went live, so whoever came first became the owner. Stop the pod, delete the volumes with podman volume rm plausible-db plausible-events, and start again at step 4, with step 5 before step 6.
The sign-up page shows its form, but nothing happens when you submit it. You are reaching Plausible at another address than BASE_URL, such as through an SSH tunnel, and the browser's WebSocket connection is refused with 403. Use step 5.
Every visitor has the same country, or the server's own. Caddy is not passing the visitor's address, or something in front of Caddy is. Check Shields: IP addresses in the site's settings, which shows the address Plausible sees for you.
Visits from your test don't appear. The script sends nothing from automated browsers, and Plausible drops events from user agents it knows as bots while still answering 202. Use an ordinary browser.
Plausible restarts again and again, and its log says could not open file "global/pg_filenode.map": Permission denied. The files in a volume were given to another owner, for example by a chown -R of /home/plausible as root. Put them back, as plausible, with podman unshare chown -R 70:70 ~/.local/share/containers/storage/volumes/plausible-db/_data, and the same with 101:101 for plausible-events and plausible-events-logs.
ClickHouse's log says Listen [0.0.0.0]:9009 failed: ... Address already in use. ipv4-only.xml is missing from step 3. Without it, ClickHouse listens on IPv6 and IPv4 at once and warns about it; it still works.
The backup stops with filesystem error: in create_directories: Permission denied ["/backups/events"]. ClickHouse cannot write to ~/backup/clickhouse. Run podman unshare chown 101:101 ~/backup/clickhouse from step 3.