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.
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:
docker volume ls --filter name=plausible-ce_| What | Service, volume | Back up? |
|---|---|---|
| PostgreSQL 16: users, sites, goals, shared links, API keys, settings | plausible_db, db-data | Yes, with pg_dump |
| ClickHouse: every pageview, event and session, plus imported stats | plausible_events_db, event-data | Yes, with BACKUP |
| ClickHouse server logs | plausible_events_db, event-logs | No |
/var/lib/plausible: Let's Encrypt certificates Plausible issued itself, the MaxMind database cache, CSV exports waiting to be downloaded, temporary files | plausible, plausible-data | Optional. Plausible issues the certificate and downloads the MaxMind file again. |
.env: BASE_URL, SECRET_KEY_BASE, an optional TOTP_VAULT_KEY, mail settings | Repository folder | Yes, encrypted |
compose.yml, compose.override.yml, clickhouse/ | Repository folder | Yes: 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_KEYencrypts them, and unless you set it, Plausible derives it fromSECRET_KEY_BASE.
grep -E '^(BASE_URL|SECRET_KEY_BASE|TOTP_VAULT_KEY)=' /opt/plausible-ce/.envKeep 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:
install -d -m 700 /var/backups/plausibledocker compose exec -T plausible_db pg_dump -U postgres plausible_db | gzip > /var/backups/plausible/plausible_db.sql.gz-Tturns off the terminal thatdocker compose execallocates 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:
<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:
services:
plausible_events_db:
volumes:
- ./clickhouse/backups.xml:/etc/clickhouse-server/config.d/backups.xml:ro
- /var/backups/plausible/clickhouse:/backupsCompose 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:
install -d -m 750 -o 101 -g 101 /var/backups/plausible/clickhousedocker compose up -ddocker 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
docker compose exec -T plausible_events_db clickhouse-client --query "BACKUP DATABASE plausible_events_db TO Disk('backups', 'events-$(date +%F).zip')"BACKUP DATABASEcopies every table inplausible_events_dbwith its definition:events_v2,sessions_v2, theimported_*tables,schema_migrationsand 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:
docker compose stop plausible plausible_events_dbArchive plausible-ce_event-data with a throwaway container, as Docker volume backups shows, then start both again:
docker compose start plausible_events_db plausibleVisits 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/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" -deletechmod 700 /usr/local/bin/plausible-backup.sh20 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 -f1takes the ID; the loop pollssystem.backupsevery 10 seconds, so a long backup never depends on one open connection. - The dump goes to a
.partialfile that is renamed only when pg_dump and gzip both succeed (pipefail); thetrapdeletes it otherwise. - The settings archive holds
.envin plain text.umask 077makes every file the script writes readable by root only. finddeletes 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:
git clone -b v3.2.1 --single-branch https://github.com/plausible/community-edition /opt/plausible-cetar -xzf plausible-config-2026-10-04_0320.tar.gz -C /opt/plausible-ceinstall -d -m 700 /var/backups/plausibleinstall -d -m 750 -o 101 -g 101 /var/backups/plausible/clickhouseCopy 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:
docker compose up -d plausible_db plausible_events_dbCreate the empty database and load the dump, as Plausible's PostgreSQL upgrade guide does:
docker compose exec plausible_db createdb -U postgres plausible_dbgunzip -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:
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:
docker compose up -dPoint 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.ymlpins the ClickHouse image (24.12-alpinein 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, setBASE_URL=http://localhost:8000and delete theHTTP_PORTandHTTPS_PORTlines, so the copy doesn't request a certificate for your real domain. - Replace
compose.override.ymlwith the file below.DISABLE_CRONturns off Plausible's scheduled jobs, the email reports included (it is in Plausible's runtime config, not the wiki);Bamboo.LocalAdapterkeeps any other email instead of sending it; the port listens on 127.0.0.1 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:/backupsRestore as above, then count sites and events on both servers. The live numbers are higher by whatever arrived since the backup:
docker compose exec -T plausible_db psql -U postgres -d plausible_db -c "SELECT count(*) FROM sites;"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:
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
| Error | Fix |
|---|---|
The 'backups.allowed_disk' configuration parameter is not set, cannot use 'Disk' backup engine | backups.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 /backups | The 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 data | Plausible 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 psql | Same 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 fail | SECRET_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:
- Plausible CE README (v3.2.1)
- Plausible CE compose.yml (v3.2.1)
- Plausible CE .gitignore (v3.2.1)
- Plausible CE v2.0.0 files (docker-compose.yml, plausible-conf.env)
- Plausible CE wiki: Configuration
- Plausible CE wiki: Compose Override
- Plausible CE wiki: Upgrade
- Plausible CE wiki: Upgrade PostgreSQL
- Plausible Analytics repository (AGPL-3.0)
- Plausible source v3.2.1: config/runtime.exs
- Plausible source v3.2.1: API key hashing
- Plausible source v3.2.1: release tasks (createdb, migrate)
- Plausible source v3.2.1: Dockerfile (DEFAULT_DATA_DIR)
- Plausible source v3.2.1: ClickHouse schema
- Plausible source v3.2.1: CSV exports to local storage
- Plausible docs: Export your stats
- Plausible docs: CSV import
- ClickHouse docs: Backup and restore overview
- ClickHouse docs: BACKUP / RESTORE to disk
- ClickHouse docs: BACKUP / RESTORE to or from an S3 endpoint
- ClickHouse docs: Alternative backup or restore methods
- ClickHouse docs: system.backups
- ClickHouse docs: FORMAT clause (batch mode default)
- ClickHouse source 24.12: File and Disk backup engines
- ClickHouse source 24.12: RestorerFromBackup
- ClickHouse Docker image 24.12: entrypoint.sh
- ClickHouse Docker image 24.12: Dockerfile.alpine (UID 101)
- Bamboo docs: Bamboo.LocalAdapter
- PostgreSQL documentation: pg_dump
- PostgreSQL documentation: psql
- Docker documentation: docker compose exec
- Docker documentation: Merge Compose files
- Docker documentation: Specify a project name
- Docker documentation: docker volume ls