VPS Snaps

How to back up self-hosted Plausible Analytics

A self-hosted Plausible Community Edition backup has three parts: a pg_dump of PostgreSQL, which holds users, sites and settings; a ClickHouse BACKUP of plausible_events_db, which holds every pageview and event; and the .env file with SECRET_KEY_BASE. Both database backups run while Plausible keeps counting visits, once you give ClickHouse a backup disk in a small config file. To restore, load both databases on the same CE version before Plausible starts for the first time.

11 min readUpdated Checked against official documentation

What Plausible CE stores and where

Plausible Community Edition (CE) is the self-hosted, AGPL-licensed release of Plausible Analytics, run with Docker Compose from the plausible/community-edition repository. This guide assumes a clone of v3.2.1, the current release, at /opt/plausible-ce. Compose prefixes volume names with the folder name:

Terminal
docker volume ls --filter name=plausible-ce_
WhatService, volumeBack up?
PostgreSQL 16: users, sites, goals, shared links, API keys, settingsplausible_db, db-dataYes, with pg_dump
ClickHouse: every pageview, event and session, plus imported statsplausible_events_db, event-dataYes, with BACKUP
ClickHouse server logsplausible_events_db, event-logsNo
/var/lib/plausible: Let's Encrypt certificates Plausible issued itself, the MaxMind database cache, CSV exports waiting to be downloaded, temporary filesplausible, plausible-dataOptional. Plausible issues the certificate and downloads the MaxMind file again.
.env: BASE_URL, SECRET_KEY_BASE, an optional TOTP_VAULT_KEY, mail settingsRepository folderYes, encrypted
compose.yml, compose.override.yml, clickhouse/Repository folderYes: image versions, ports and ClickHouse config

Git ignores .env and compose.override.yml, so no other copy of them exists. Installs older than v2.1.2 use docker-compose.yml and plausible-conf.env instead.

The per-site CSV export under Site Settings > Imports & Exports holds aggregated stats for one site, without imported data. It moves a site; it doesn't back up the instance.

The secrets in .env

SECRET_KEY_BASE is more than a session key. Restore the databases under a different value and:

  • every dashboard session ends, so everyone logs in again;
  • every API key stops working, because Plausible stores only a SHA-256 hash of each key computed with SECRET_KEY_BASE;
  • Plausible can no longer decrypt the stored two-factor secrets. TOTP_VAULT_KEY encrypts them, and unless you set it, Plausible derives it from SECRET_KEY_BASE.
Terminal
grep -E '^(BASE_URL|SECRET_KEY_BASE|TOTP_VAULT_KEY)=' /opt/plausible-ce/.env

Keep these values in your password manager too. If you use Docker secrets (files in /run/secrets or CONFIG_DIR), back up those files.

Dump PostgreSQL

The database is plausible_db, owned by the postgres user, as in the default DATABASE_URL. Create a private backup folder, then dump from the repository folder:

Terminal
install -d -m 700 /var/backups/plausible
Terminal
docker compose exec -T plausible_db pg_dump -U postgres plausible_db | gzip > /var/backups/plausible/plausible_db.sql.gz
  • -T turns off the terminal that docker compose exec allocates by default, which would corrupt the output. Backing up a database in Docker Compose explains why.
  • pg_dump runs inside the container, so its version always matches the server: PostgreSQL 16 in the current compose.yml.
  • The output is plain SQL, the format Plausible's own PostgreSQL upgrade guide loads back with psql.

If you changed DATABASE_URL, use its names. The pg_dump guide covers the other options.

Give ClickHouse a backup disk

ClickHouse refuses BACKUP ... TO Disk(...) until its config allows a backup disk, with The 'backups.allowed_disk' configuration parameter is not set, cannot use 'Disk' backup engine. Create this file, taken from the example in ClickHouse's docs. It defines a disk named backups at /backups/ inside the container and allows backups there:

/opt/plausible-ce/clickhouse/backups.xml
<clickhouse>
    <storage_configuration>
        <disks>
            <backups>
                <type>local</type>
                <path>/backups/</path>
            </backups>
        </disks>
    </storage_configuration>
    <backups>
        <allowed_disk>backups</allowed_disk>
        <allowed_path>/backups/</allowed_path>
    </backups>
</clickhouse>

Mount it, and a host folder at /backups, through compose.override.yml, the file Plausible's wiki recommends for local changes. Keep any plausible: section already there:

/opt/plausible-ce/compose.override.yml
services:
  plausible_events_db:
    volumes:
      - ./clickhouse/backups.xml:/etc/clickhouse-server/config.d/backups.xml:ro
      - /var/backups/plausible/clickhouse:/backups

Compose merges these with the mounts in compose.yml by target path, so the stock ones stay. ClickHouse runs as UID 101 in its image, so give the folder that owner, recreate the container and check the disk:

Terminal
install -d -m 750 -o 101 -g 101 /var/backups/plausible/clickhouse
Terminal
docker compose up -d
Terminal
docker compose exec plausible_events_db clickhouse-client --query "SELECT name, path FROM system.disks"

The output should list backups with the path /backups/ beside default.

Back up the events with BACKUP

Terminal
docker compose exec -T plausible_events_db clickhouse-client --query "BACKUP DATABASE plausible_events_db TO Disk('backups', 'events-$(date +%F).zip')"
  • BACKUP DATABASE copies every table in plausible_events_db with its definition: events_v2, sessions_v2, the imported_* tables, schema_migrations and the rest.
  • The double quotes let the host shell fill in the date; the single quotes inside are SQL strings. Every backup needs a new name, or ClickHouse stops with BACKUP_ALREADY_EXISTS.
  • It prints the operation's ID and BACKUP_CREATED, and the archive appears on the host in /var/backups/plausible/clickhouse.
  • Plausible keeps recording visits meanwhile. Visits that arrive during the backup land in the next one.

With ASYNC at the end, the command returns the ID at once, and system.backups shows the status move from CREATING_BACKUP to BACKUP_CREATED or BACKUP_FAILED, with the reason in error. That table empties when ClickHouse restarts. Two options help larger instances:

  • Incremental backups. SETTINGS base_backup = Disk('backups', 'events-full.zip') stores only what changed since that full backup. A restore needs the base as well, so keep it as long as any backup built on it.
  • Straight to a bucket. TO S3('https://<bucket>.s3.<region>.amazonaws.com/plausible/events-2026-10-04', '<access-key-id>', '<secret-access-key>') writes to S3 or S3-compatible storage with no config file and no local copy. The keys are part of the query; ClickHouse's docs suggest named collections to keep them out of query logs.

Back up ClickHouse first and PostgreSQL second. Then every event in the archive belongs to a site the dump knows. The other way round, a site added between the two is missing from the dump, and after a restore the next site you create gets its ID, and its visits.

Or copy the volume with ClickHouse stopped

ClickHouse's docs list file-level copies only as alternatives to BACKUP. For a plain archive of event-data, stop Plausible and ClickHouse first:

Terminal
docker compose stop plausible plausible_events_db

Archive plausible-ce_event-data with a throwaway container, as Docker volume backups shows, then start both again:

Terminal
docker compose start plausible_events_db plausible

Visits that arrive while Plausible is stopped are lost. A copy taken while ClickHouse runs reads files it may be merging or deleting at that moment; ClickHouse's docs don't describe that as a backup method.

Automate it

This root script runs the ClickHouse backup with ASYNC and waits for it, then dumps PostgreSQL, then packs the settings, all into /var/backups/plausible:

/usr/local/bin/plausible-backup.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

cd /opt/plausible-ce
OUT=/var/backups/plausible
STAMP=$(date +%Y-%m-%d_%H%M)
KEEP_DAYS=7

ch() { docker compose exec -T plausible_events_db clickhouse-client --query "$1"; }

# 1. ClickHouse first
id=$(ch "BACKUP DATABASE plausible_events_db TO Disk('backups', 'events-$STAMP.zip') ASYNC" | cut -f1)
status=CREATING_BACKUP
while [ "$status" = "CREATING_BACKUP" ]; do
  sleep 10
  status=$(ch "SELECT status FROM system.backups WHERE id = '$id'")
done
if [ "$status" != "BACKUP_CREATED" ]; then
  echo "ClickHouse backup $id ended with: $status" >&2
  ch "SELECT error FROM system.backups WHERE id = '$id'" >&2
  exit 1
fi

# 2. PostgreSQL second
trap 'rm -f "$OUT"/*.partial' EXIT
docker compose exec -T plausible_db pg_dump -U postgres plausible_db \
  | gzip > "$OUT/plausible_db-$STAMP.sql.gz.partial"
mv "$OUT/plausible_db-$STAMP.sql.gz.partial" "$OUT/plausible_db-$STAMP.sql.gz"

# 3. Settings, including the secrets in .env
tar -czf "$OUT/plausible-config-$STAMP.tar.gz" .env compose.yml compose.override.yml clickhouse

find "$OUT" -maxdepth 2 -type f -mtime +"$KEEP_DAYS" -delete
Terminal
chmod 700 /usr/local/bin/plausible-backup.sh
/etc/cron.d/plausible-backup
20 3 * * * root /usr/local/bin/plausible-backup.sh >> /var/log/plausible-backup.log 2>&1
  • Without a terminal, clickhouse-client prints tab-separated rows, so cut -f1 takes the ID; the loop polls system.backups every 10 seconds, so a long backup never depends on one open connection.
  • The dump goes to a .partial file that is renamed only when pg_dump and gzip both succeed (pipefail); the trap deletes it otherwise.
  • The settings archive holds .env in plain text. umask 077 makes every file the script writes readable by root only.
  • find deletes files older than 7 days, ClickHouse archives in the subfolder included.

The folder still sits on Plausible's disk. Encrypt it and copy it off the server: Encrypting backups shows gpg, and rclone uploads to most storage.

Restore onto a new server

Restore onto the CE version that made the backup: the image: line of the compose.yml in your settings archive names it. On the new server, as root, with Docker installed:

Terminal
git clone -b v3.2.1 --single-branch https://github.com/plausible/community-edition /opt/plausible-ce
Terminal
tar -xzf plausible-config-2026-10-04_0320.tar.gz -C /opt/plausible-ce
Terminal
install -d -m 700 /var/backups/plausible
Terminal
install -d -m 750 -o 101 -g 101 /var/backups/plausible/clickhouse

Copy events-2026-10-04_0320.zip into /var/backups/plausible/clickhouse and the PostgreSQL dump into /var/backups/plausible. Then, in /opt/plausible-ce, start only the two databases, and wait until docker compose ps shows both as healthy:

Terminal
docker compose up -d plausible_db plausible_events_db

Create the empty database and load the dump, as Plausible's PostgreSQL upgrade guide does:

Terminal
docker compose exec plausible_db createdb -U postgres plausible_db
Terminal
gunzip -c /var/backups/plausible/plausible_db-2026-10-04_0320.sql.gz | docker compose exec -T plausible_db psql -U postgres -d plausible_db --single-transaction -v ON_ERROR_STOP=1

--single-transaction with ON_ERROR_STOP=1 makes the load all or nothing. Then restore the events. RESTORE DATABASE creates the database and its tables from the archive and prints RESTORED:

Terminal
docker compose exec -T plausible_events_db clickhouse-client --query "RESTORE DATABASE plausible_events_db FROM Disk('backups', 'events-2026-10-04_0320.zip')"

Now start everything. The start command in compose.yml runs db createdb, which finds both databases already there, then db migrate, which has nothing to apply on the same version:

Terminal
docker compose up -d

Point your domain at the new server; with HTTP_PORT=80 and HTTPS_PORT=443 in .env, Plausible requests its own Let's Encrypt certificate once DNS resolves. Moving to a new provider covers the cut-over.

Load both databases before Plausible starts for the first time. If it starts on empty databases, it creates its tables, and the restores then fail with relation ... already exists and Cannot restore the table ... because it already contains some data.

Upgrades are not restores

To upgrade, Plausible's wiki has you fetch the new release's files and run docker compose up -d; the new image migrates both databases as it starts, and major versions list extra steps in their release notes. There is no documented way back, so:

  • Run the backup script right before every upgrade and keep that set for a while.
  • To roll back, restore that set onto the old version, as above.
  • Restore onto the version that made the backup, then upgrade, following the same release notes.
  • Plausible's guide to PostgreSQL major versions dumps and reloads, the same commands as a restore; see also PostgreSQL major upgrades.
  • compose.yml pins the ClickHouse image (24.12-alpine in v3.2.1). Restore with the one from your settings archive, so the ClickHouse that wrote the backup reads it.

Test the restore on a throwaway server

A restored copy is a working Plausible that sends email: weekly and monthly reports, traffic alerts. Test on a throwaway server. After unpacking the settings archive there, and before starting anything, make two changes:

  • In .env, set BASE_URL=http://localhost:8000 and delete the HTTP_PORT and HTTPS_PORT lines, so the copy doesn't request a certificate for your real domain.
  • Replace compose.override.yml with the file below. DISABLE_CRON turns off Plausible's scheduled jobs, the email reports included (it is in Plausible's runtime config, not the wiki); Bamboo.LocalAdapter keeps any other email instead of sending it; the port listens on 127.0.0.1 only.
compose.override.yml (test server only)
services:
  plausible:
    ports:
      - 127.0.0.1:8000:8000
    environment:
      DISABLE_CRON: "true"
      MAILER_ADAPTER: Bamboo.LocalAdapter
  plausible_events_db:
    volumes:
      - ./clickhouse/backups.xml:/etc/clickhouse-server/config.d/backups.xml:ro
      - /var/backups/plausible/clickhouse:/backups

Restore as above, then count sites and events on both servers. The live numbers are higher by whatever arrived since the backup:

Terminal
docker compose exec -T plausible_db psql -U postgres -d plausible_db -c "SELECT count(*) FROM sites;"
Terminal
docker compose exec -T plausible_events_db clickhouse-client --query "SELECT count() FROM plausible_events_db.events_v2"

Then log in through an SSH tunnel at http://localhost:8000 and open last month's dashboard for a few sites:

Terminal
ssh -N -L 8000:127.0.0.1:8000 root@<test-server-ip>

Time the whole restore; that is your real recovery time. Then delete the test server. Testing restores turns this into a routine.

Common errors

ErrorFix
The 'backups.allowed_disk' configuration parameter is not set, cannot use 'Disk' backup enginebackups.xml isn't mounted. Check compose.override.yml and run docker compose up -d from the repository folder.
Backup Disk('backups', '...') already exists. (BACKUP_ALREADY_EXISTS)Use a new name for every backup, such as one with the date and time.
BACKUP fails with a permission error on /backupsThe host folder isn't writable by UID 101. Run chown 101:101 /var/backups/plausible/clickhouse.
Cannot restore the table ... because it already contains some dataPlausible started before the restore and created its tables. On the new server only, docker compose down -v deletes its volumes; start the restore again.
relation ... already exists from psqlSame cause, same fix.
SECRET_KEY_BASE configuration option is required.env is missing, or Compose ran outside the repository folder. Restore .env from the settings archive.
Everyone is logged out, API keys are rejected, two-factor codes failSECRET_KEY_BASE or TOTP_VAULT_KEY differs from the one the backup was made with. Put the original values back.

Frequently asked questions

Where does self-hosted Plausible store its data?
In two databases on Docker volumes: PostgreSQL in db-data holds users, sites and settings, and ClickHouse in event-data holds the pageviews and events. The secrets are in the .env file next to compose.yml.
Can I back up Plausible without stopping it?
Yes. pg_dump and ClickHouse's BACKUP command both run while Plausible keeps recording visits. Only a file copy of the event-data volume needs ClickHouse stopped.
What happens if I lose SECRET_KEY_BASE?
Your stats survive, but everyone is logged out, every API key stops working, and the stored two-factor secrets can't be decrypted unless you set TOTP_VAULT_KEY separately and kept it.
How do I move Plausible to a new server?
Install the same CE version there, restore .env and the compose files, start only the two databases, load the PostgreSQL dump and RESTORE the ClickHouse backup, then start Plausible and point DNS at it.

How this was checked

Commands, limits and prices were checked against these official pages, on October 4, 2026: