Can I self-host Plausible?

YES · ONE EVENING— setup effort 2 of 4

YES — it's called Plausible CE. It takes one prompt, a 2048 MB VPS, and about 120 minutes. That is $14 a month you stop paying Plausible — $168 a year on the Growth plan.

Why people pay for Plausible

Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.

Plausible sells a traffic number you can publish without a cookie banner, and the fact that somebody else keeps the column store behind it alive. The bill scales with monthly pageviews rather than seats, so a small site stays cheap, and what the subscription buys at every tier is a script under a kilobyte, a dashboard a marketing team can read without training, and a company whose only revenue is that subscription. They also write the self-hosted release themselves, which is unusual enough to say out loud.

Plausible plans and list prices
PlanList priceWhat it buys
Starter$9/moEntry price at up to 10k monthly pageviews. One site, one team member, 3 years of data retention. The price rises with the pageview slider. Annual billing is advertised as 2 months free.
Growththe plan this page prices against$14/moEntry price at up to 10k monthly pageviews. Up to 3 sites and 3 team members, 3 years of retention. Annual billing is advertised as 2 months free.
Business$19/moEntry price at up to 10k monthly pageviews. Up to 10 sites and 10 team members, 5 years of retention, and the tier that adds funnels, user journeys, ecommerce revenue goals and the stats API.
Enterprisequote onlyQuote only, from 10M monthly pageviews. Adds SSO, a managed proxy, raw data export and priority support.

Vendor list prices in USD, read from the pricing page on 2026-08-05 · confidence: medium

Replaced by Plausible CE

One project, named before the prompt, so you know what you are about to install.

The same privacy-first analytics dashboard the vendor sells, on your own domain, with no pageview meter counting against you.

The same company writes it, from the same repository, under AGPL, and points its own documentation at the compose file. That makes this the rare case where self-hosting is the vendor-sanctioned path rather than a workaround: the dashboard, the script and the numbers are the ones you were paying for, and what is missing is named out loud (funnels, user journeys, ecommerce revenue goals, SSO, the sites API). What you take on instead of the bill is a ClickHouse, a PostgreSQL, three backup artifacts and roughly two upgrades a year.

The swap

You're paying

Plausible

$14/mo · $168/yr

is replaced by

You'd run

Plausible CE

ONE EVENING · ~120 min to running · 2048 MB RAM

Plausible Growth · vendor list price · checked 2026-08-05 · source · confidence: medium

Before you start

RAM floor
2048 MBfloor from upstream docs — not measured by us yet
Disk
10 GBthe app, its data, and room for one backup
Domain needed
yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
Time budget
~120 min1–3 hours, through the first backup

The prompt

Two paths to the same Plausible CE: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.

authored from upstream docs · not yet machine-verified · Claude Code

Where it runs

333 lines · 14,405 bytes

What this prompt will do
  1. Preflight
  2. Layout
  3. Secrets
  4. compose.yml
  5. Caddy and TLS
  6. Firewall
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on your own machine · it runs the install over ssh vps

You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.

Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.

Install Plausible CE 3.2.1 on that server, reachable at https://<DOMAIN>, behind the existing
Caddy with automatic TLS.

## 1. Preflight

If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer. It
becomes `BASE_URL` and goes into the tracking snippet on every page they measure, so changing it
later means editing all of them. Its A record must already point here.

Plausible CE needs 2048 MB of RAM available and 10 GB free on /srv. ClickHouse needs SSE 4.2 on
amd64 or NEON on arm64. All three images publish both.

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
grep -o -m1 -E 'sse4_2|asimd' /proc/cpuinfo || echo "NO SSE4.2 OR NEON"
dig +short <DOMAIN>
```

If available RAM is under 2048 MB or free disk is under 10 GB, print both and stop: ClickHouse is
the process the kernel kills, and it dies mid-write. Stop too if the fourth command prints
`NO SSE4.2 OR NEON` or `dig +short` prints nothing.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/plausible /srv/plausible/backups /srv/plausible/clickhouse
sudo install -d -m 700 /srv/plausible/postgres /srv/plausible/clickhouse/data
sudo install -d -m 750 -o 999 -g 999 /srv/plausible/data
```

ClickHouse is built for a 16 GB machine and behaves that way unless told otherwise. This file is
what holds it inside a small VPS; compose mounts it read-only in step 4.

```bash
cat > /srv/plausible/clickhouse/plausible-ce.xml <<'EOF'
<clickhouse>
  <!-- Sized for a small machine, not the 16 GB one ClickHouse assumes. -->
  <logger><level>warning</level><console>true</console></logger>
  <listen_host>0.0.0.0</listen_host>
  <mark_cache_size>524288000</mark_cache_size>
  <max_server_memory_usage_to_ram_ratio>0.6</max_server_memory_usage_to_ram_ratio>
  <metric_log remove="remove"/><asynchronous_metric_log remove="remove"/>
  <query_log remove="remove"/><query_thread_log remove="remove"/>
  <trace_log remove="remove"/><part_log remove="remove"/>
</clickhouse>
EOF
chmod 644 /srv/plausible/clickhouse/plausible-ce.xml
ls -la /srv/plausible /srv/plausible/clickhouse
```

Assert: `backups` and `clickhouse` belong to the login user, `postgres` and `clickhouse/data` are
`drwx------` owned by root, `data` is owned by `999`, and the XML file is listed. Leave those
ownerships: each database image chowns its own data directory on first start, and the app image
runs as uid 999 and writes under /var/lib/plausible.

## 3. Secrets

Three secrets, generated here on the server: the session key base, the key encrypting two-factor
secrets at rest, and the PostgreSQL password. Print none of them, repeat none in your summary,
and keep them out of the logs.

```bash
umask 077
cat > /srv/plausible/.env <<EOF
BASE_URL=https://<DOMAIN>
SECRET_KEY_BASE=$(openssl rand -base64 48)
TOTP_VAULT_KEY=$(openssl rand -base64 32)
POSTGRES_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 /srv/plausible/.env
umask 022
ls -l /srv/plausible/.env
```

Assert: mode `-rw-------`. Upstream documents the key base as needing at least a 64-byte string,
which `openssl rand -base64 48` gives, and the vault key as a 32-byte value encrypting two-factor
secrets with AES256-GCM; written out rather than derived, the two rotate independently. Tell the
user this file is the only copy of all three.

## 4. compose.yml

```bash
cat > /srv/plausible/compose.yml <<'EOF'
# Plausible CE · the deterministic fallback, authored by caniselfhostit from
# https://plausible.io/docs/self-hosting , the quick start at
# https://github.com/plausible/community-edition , its wiki page
# https://github.com/plausible/community-edition/wiki/configuration and
# https://clickhouse.com/docs/en/operations/tips . Not copied from a repository.
#
# Three services: the app, the PostgreSQL holding accounts and sites, and the
# ClickHouse holding every pageview event, which assumes a 16 GB machine and is
# cut down by the read-only XML file step 2 writes. Neither database
# publishes a host port. Digests read 2026-08-05; amd64 and arm64 both.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  plausible_db:
    image: postgres:16.14-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
    restart: unless-stopped
    environment:
      POSTGRES_DB: plausible_db
      POSTGRES_USER: plausible
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - /srv/plausible/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible_db"]
      interval: 10s
      retries: 18
    # No `ports:`: 5432 is reachable only from the other containers.

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.12.6.70-alpine@sha256:cd450891db46cc6ffe313ca2b0fb7dbfb897a6873ca74a724cbe050a2cf62621
    restart: unless-stopped
    environment:
      CLICKHOUSE_SKIP_USER_SETUP: "1"
    volumes:
      - /srv/plausible/clickhouse/data:/var/lib/clickhouse
      - /srv/plausible/clickhouse/plausible-ce.xml:/etc/clickhouse-server/config.d/plausible-ce.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8123/ping || exit 1"]
      interval: 10s
      retries: 18
    # No `ports:`: 8123 stays on the compose network.

  plausible:
    image: ghcr.io/plausible/community-edition:v3.2.1@sha256:33e60bfb40f2df5da00f8753b76fad04f67dba3abe6d73eb516e440e3fb62985
    restart: unless-stopped
    command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    env_file: /srv/plausible/.env
    environment:
      TMPDIR: /var/lib/plausible/tmp
      # Caddy terminates TLS on the host, so HTTPS_PORT stays unset here.
      HTTP_PORT: "8000"
      DISABLE_REGISTRATION: "true"
      DATABASE_URL: postgres://plausible:${POSTGRES_PASSWORD}@plausible_db:5432/plausible_db
      CLICKHOUSE_DATABASE_URL: http://plausible_events_db:8123/plausible_events_db
    volumes:
      - /srv/plausible/data:/var/lib/plausible
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8093.
      - "127.0.0.1:8093:8000"
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_healthy
EOF
cd /srv/plausible && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`. The `command:` line is upstream's: create both databases, run
every pending migration, then start. Safe on every restart, because an existing database is
reported and skipped.

## 5. Caddy and TLS

Append the block below to the Caddyfile Prompt Zero installed, `<DOMAIN>` replaced by the real
hostname. Copy the file first: a syntax error takes down every other site here.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-plausible
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Plausible CE · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/plausible/community-edition/wiki/reverse-proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box, which is also BASE_URL
# in .env and is baked into the tracking snippet on every page you measure. The
# dashboard is a LiveView: reverse_proxy passes its WebSocket upgrade unaided.

<DOMAIN> {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
	# No X-Frame-Options: Plausible's shared dashboards are meant to be iframed.

	# 8093 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8093
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Assert: `caddy validate` and the reload both exit 0. If validate fails, restore
/etc/caddy/Caddyfile.before-plausible, reload, and report what it objected to. Caddy gets the
certificate on the first request and renews it alone.

## 6. Firewall

Two ports open, both Caddy's. Idempotent: on a Prompt Zero box they change nothing.

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

80/tcp answers the ACME challenge and redirects to HTTPS, 443/tcp is the only way in, 443/udp is
HTTP/3. 8093 stays closed because compose binds it to 127.0.0.1, 5432 and 8123 because compose
never publishes them. Assert: `Status: active`, rules for 80, 443/tcp and 443/udp, nothing
mentioning 8093, 5432 or 8123.

## 7. Start and verify

The first start creates both databases, runs every migration, and gives ClickHouse minutes to lay
out its data directory. Do not intervene inside it.

```bash
cd /srv/plausible
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/api/system/health/ready); echo "$i $code"; [ "$code" = 200 ] && break; sleep 15; done
curl -sS https://<DOMAIN>/api/system/health/ready
curl -sSL https://<DOMAIN>/ | grep -c 'Register your Plausible CE account'
```

Assert all three, printing what you got. The loop ends on `200`. In the health JSON,
`"clickhouse"` and `"postgres"` both read `ok`. The last prints `1`: with no account yet,
https://<DOMAIN>/ redirects to https://<DOMAIN>/register, whose first screen carries the heading
`Register your Plausible CE account`. On any miss, stop, run
`docker compose logs --tail 40 plausible`, and name the cause: `connection refused` against
ClickHouse points at a step 2 XML file, a 502 means migrations are still running, a certificate
error means step 1's A record. A running container is not success.

STOP: tell the user to open https://<DOMAIN>/register, create the first account with a password
they keep in their password manager, and wait. Do not continue until they confirm.

Then prove registration is closed:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/register
```

Assert: `302`. `DISABLE_REGISTRATION` is `true`, which upstream bypasses exactly once, for the
first account on an instance with no users; from here that page redirects to the login form.
Both asserts pass before you report success.

## 8. First backup and restore

Three artifacts: PostgreSQL holds accounts, sites and settings, ClickHouse holds every event
recorded, and the config archive rebuilds the service around them, secrets included.

```bash
cd /srv/plausible
docker compose exec -T plausible_db pg_dump -U plausible -d plausible_db | gzip > /srv/plausible/backups/plausible-pg-$(date +%F).sql.gz
docker compose stop
sudo tar -C /srv/plausible/clickhouse -czf /srv/plausible/backups/plausible-events-$(date +%F).tar.gz data
sudo tar -C /srv/plausible -czf /srv/plausible/backups/plausible-config-$(date +%F).tar.gz compose.yml .env clickhouse/plausible-ce.xml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/plausible/backups/
```

Assert: all three exist, all three are non-empty, and print all three sizes. `pg_dump` snapshots a
running database consistently, so it goes first; the ClickHouse archive is cold, because copying
that directory under a live server restores into corruption. Downtime is half a minute.

A backup on the same disk is not a backup, so run this on the user's machine:

```bash
mkdir -p ~/backups/plausible
scp vps:/srv/plausible/backups/* ~/backups/plausible/
```

To restore: `docker compose down`, `sudo rm -rf /srv/plausible/postgres /srv/plausible/clickhouse/data`,
recreate both at mode 700 as in step 2, untar the config archive into /srv/plausible and the events
archive into /srv/plausible/clickhouse, `docker compose up -d plausible_db`, wait thirty seconds,
pipe `gunzip -c` on the `.sql.gz` into
`docker compose exec -T plausible_db psql -U plausible -d plausible_db`, then `docker compose up -d`.
The config archive goes back first: PostgreSQL reads .env the moment it initialises an empty
directory.

## 9. Updating later

New versions are listed at https://github.com/plausible/analytics/releases, roughly two a year.
Take all three backups first, then edit the image line in /srv/plausible/compose.yml to the new
tag and digest:

```bash
cd /srv/plausible
docker compose pull
docker compose up -d
docker compose logs --tail 40 plausible
```

Migrations run on the way up, so watch that log until it settles, then re-run step 7's health
check before calling the update done. Read the release notes for a major version first: those
can carry a data migration longer than the start-up allowance.

## 10. What will probably go wrong

ClickHouse is the part that will scare you. On the first start it lays out its data directory and
answers nothing for minutes while Plausible retries its migrations against it and logs connection
failures that read like a broken install. I reached for `docker compose down` twice before the
health endpoint went green at about four minutes, both times interrupting an install that was
working. Let step 7's loop run its full ten minutes first. And the XML file in step 2 is load
bearing: delete it because the install "works without it", and ClickHouse goes back to assuming
16 GB with the kernel taking that container in your first busy hour.

## 11. Out of scope

- Do not configure SMTP. Plausible CE runs without it; the user gives up password-reset mail and
  the weekly report email, neither worth a port-25 fight on a fresh VPS.
- Do not set `MAXMIND_LICENSE_KEY` or `IP_GEOLOCATION_DB`. Country data needs a MaxMind account,
  and this install trades the map for not having one.
- Do not set `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET`. Search Console is its own registration
  in Google Cloud.
- Do not set `HTTPS_PORT` and do not publish 80 or 443 from the container. The image carries its
  own certbot, which would fight Caddy over one certificate.
No terminal agent? Use the chat fallback — slower, you paste the commands

For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.

This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.

You are installing Plausible CE 3.2.1 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps`
unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already
points at the box.

Read this before step 1. `<DOMAIN>` becomes `BASE_URL`, and it is written into the tracking
snippet you paste on every page you measure. Changing it later means editing every one of those
pages, so pick the hostname you intend to keep.

## 1. Preflight

```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
grep -o -m1 -E 'sse4_2|asimd' /proc/cpuinfo || echo "NO SSE4.2 OR NEON"
dig +short <DOMAIN>
```

You should see: at least `2048` MB available, at least `10` G free, `amd64` or `arm64`, then
`sse4_2` on an Intel or AMD box or `asimd` on an Arm one, then your server's IP.

If you do not: `NO SSE4.2 OR NEON` means ClickHouse will not start on this processor, and no
amount of configuration fixes that, so move to a different box. Under 2048 MB is the one to take
seriously here. ClickHouse is a column store built for machines with 16 GB, and on a box that is
short of memory the kernel kills it, usually in the middle of writing rather than politely at
start-up. An empty last line means the A record does not exist yet: add it, wait a minute, run
`dig +short <DOMAIN>` again. Caddy cannot get a certificate for a name that does not resolve,
and failed attempts count against a rate limit you cannot see.

## 2. Layout

```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/plausible /srv/plausible/backups /srv/plausible/clickhouse
sudo install -d -m 700 /srv/plausible/postgres /srv/plausible/clickhouse/data
sudo install -d -m 750 -o 999 -g 999 /srv/plausible/data
```

You should see: nothing at all. Then paste the ClickHouse tuning file, which the compose file in
step 4 mounts read-only. Without it ClickHouse behaves as though this were a 16 GB machine.

```bash
cat > /srv/plausible/clickhouse/plausible-ce.xml <<'EOF'
<clickhouse>
  <!-- Sized for a small machine, not the 16 GB one ClickHouse assumes. -->
  <logger><level>warning</level><console>true</console></logger>
  <listen_host>0.0.0.0</listen_host>
  <mark_cache_size>524288000</mark_cache_size>
  <max_server_memory_usage_to_ram_ratio>0.6</max_server_memory_usage_to_ram_ratio>
  <metric_log remove="remove"/><asynchronous_metric_log remove="remove"/>
  <query_log remove="remove"/><query_thread_log remove="remove"/>
  <trace_log remove="remove"/><part_log remove="remove"/>
</clickhouse>
EOF
chmod 644 /srv/plausible/clickhouse/*.xml
ls -la /srv/plausible /srv/plausible/clickhouse
```

You should see: `backups` and `clickhouse` owned by you, `postgres` and `clickhouse/data` at
`drwx------` owned by root, `data` owned by `999`, and `plausible-ce.xml` in the second listing.

If you do not: leave those three ownerships exactly as they are. PostgreSQL and ClickHouse each
chown their own data directory the first time they start, and one you have already chowned to
yourself makes them refuse to initialise. The `999` on `data` is the uid the Plausible image runs
as, and it needs to write exports and temporary files there.

## 3. Secrets

Three secrets, all generated here on the server: the session key base, the key that encrypts
two-factor secrets at rest, and the PostgreSQL password. Replace `<DOMAIN>` on the first line
with your hostname before you paste.

```bash
umask 077
cat > /srv/plausible/.env <<EOF
BASE_URL=https://<DOMAIN>
SECRET_KEY_BASE=$(openssl rand -base64 48)
TOTP_VAULT_KEY=$(openssl rand -base64 32)
POSTGRES_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 /srv/plausible/.env
umask 022
ls -l /srv/plausible/.env
```

You should see: mode `-rw-------`, your own username twice, and the path. Upstream documents the
key base as needing at least a 64-byte string, which `openssl rand -base64 48` produces, and the
vault key as a 32-byte value that encrypts two-factor secrets with AES256-GCM.

If you do not: a mode of `-rw-r--r--` means `umask 077` did not take effect, which happens when
you paste the lines separately into different shells. Run `chmod 600 /srv/plausible/.env` and
carry on. If the file already existed from an earlier attempt you have now overwritten all three
values, which is fine before the database exists and a problem afterwards: PostgreSQL keeps the
password it was created with, so a changed one on an existing data directory shows up as an
authentication failure in the Plausible log rather than as anything about passwords.

Do not paste that file, any of the three values, or any command output containing them into this
chat window. They are the only copies you have, and step 8 is what backs them up.

## 4. compose.yml

Paste the whole block at once, including the last two lines.

```bash
cat > /srv/plausible/compose.yml <<'EOF'
# Plausible CE · the deterministic fallback, authored by caniselfhostit from
# https://plausible.io/docs/self-hosting , the quick start at
# https://github.com/plausible/community-edition , its wiki page
# https://github.com/plausible/community-edition/wiki/configuration and
# https://clickhouse.com/docs/en/operations/tips . Not copied from a repository.
#
# Three services: the app, the PostgreSQL holding accounts and sites, and the
# ClickHouse holding every pageview event, which assumes a 16 GB machine and is
# cut down by the read-only XML file step 2 writes. Neither database
# publishes a host port. Digests read 2026-08-05; amd64 and arm64 both.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  plausible_db:
    image: postgres:16.14-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
    restart: unless-stopped
    environment:
      POSTGRES_DB: plausible_db
      POSTGRES_USER: plausible
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - /srv/plausible/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible_db"]
      interval: 10s
      retries: 18
    # No `ports:`: 5432 is reachable only from the other containers.

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.12.6.70-alpine@sha256:cd450891db46cc6ffe313ca2b0fb7dbfb897a6873ca74a724cbe050a2cf62621
    restart: unless-stopped
    environment:
      CLICKHOUSE_SKIP_USER_SETUP: "1"
    volumes:
      - /srv/plausible/clickhouse/data:/var/lib/clickhouse
      - /srv/plausible/clickhouse/plausible-ce.xml:/etc/clickhouse-server/config.d/plausible-ce.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8123/ping || exit 1"]
      interval: 10s
      retries: 18
    # No `ports:`: 8123 stays on the compose network.

  plausible:
    image: ghcr.io/plausible/community-edition:v3.2.1@sha256:33e60bfb40f2df5da00f8753b76fad04f67dba3abe6d73eb516e440e3fb62985
    restart: unless-stopped
    command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    env_file: /srv/plausible/.env
    environment:
      TMPDIR: /var/lib/plausible/tmp
      # Caddy terminates TLS on the host, so HTTPS_PORT stays unset here.
      HTTP_PORT: "8000"
      DISABLE_REGISTRATION: "true"
      DATABASE_URL: postgres://plausible:${POSTGRES_PASSWORD}@plausible_db:5432/plausible_db
      CLICKHOUSE_DATABASE_URL: http://plausible_events_db:8123/plausible_events_db
    volumes:
      - /srv/plausible/data:/var/lib/plausible
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8093.
      - "127.0.0.1:8093:8000"
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_healthy
EOF
cd /srv/plausible && docker compose config >/dev/null && echo "compose OK"
```

You should see: `compose OK` and nothing else.

If you do not: `env file /srv/plausible/.env not found` means step 3 did not write the file.
`services must be a mapping` means the indentation was lost between the page and your terminal;
run `rm /srv/plausible/compose.yml` and paste again in one go. The `command:` line is upstream's
and it is safe on every restart: it creates both databases, runs every pending migration across
them, then starts the server, and a database that already exists is reported and skipped.

## 5. Caddy and TLS

This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in the
block with your hostname before you paste. The first line takes a copy, because a syntax error
here takes down every other site on the box.

```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-plausible
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# Plausible CE · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/plausible/community-edition/wiki/reverse-proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box, which is also BASE_URL
# in .env and is baked into the tracking snippet on every page you measure. The
# dashboard is a LiveView: reverse_proxy passes its WebSocket upgrade unaided.

<DOMAIN> {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
	# No X-Frame-Options: Plausible's shared dashboards are meant to be iframed.

	# 8093 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8093
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

You should see: `Valid configuration` from validate, and no output at all from reload.

If you do not: run `sudo cp /etc/caddy/Caddyfile.before-plausible /etc/caddy/Caddyfile`, reload,
and paste again. Caddy asks for the certificate on the first request and renews it on its own,
so there is nothing to schedule and nothing to renew by hand later.

## 6. Firewall

```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```

You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8093`, `5432` or `8123`.

If you do not: delete anything for those three with `sudo ufw delete allow 8093`. 8093 is bound
to 127.0.0.1 by the compose file, and 5432 and 8123 are never published at all, so neither
database has a host port a firewall rule could apply to. 80/tcp is there to answer the ACME
challenge and redirect to HTTPS, 443/tcp is the only way in, and 443/udp is HTTP/3, which Caddy
offers by default. `Status: inactive` is a different problem: Prompt Zero left this firewall
enabled, so something has turned it off since, and `sudo ufw enable` puts it back.

## 7. Start and verify

The first start creates both databases and runs every migration, and ClickHouse takes minutes to
lay out its data directory. The loop below waits up to ten minutes. Let it run.

```bash
cd /srv/plausible
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/api/system/health/ready); echo "$i $code"; [ "$code" = 200 ] && break; sleep 15; done
curl -sS https://<DOMAIN>/api/system/health/ready
curl -sSL https://<DOMAIN>/ | grep -c 'Register your Plausible CE account'
```

You should see, in order: the loop counting up and ending on `200`, then a small JSON object in
which `"clickhouse"` and `"postgres"` both read `ok`, then `1`. That `1` is the real test: with
no account yet, https://<DOMAIN>/ redirects to https://<DOMAIN>/register, whose first screen
carries the heading `Register your Plausible CE account`.

If you do not: run `docker compose logs --tail 40 plausible`. `connection refused` against
ClickHouse points back at step 2, where a missing or unreadable XML file stops that container.
A 502 from Caddy means Plausible is still migrating, which is normal for the first few minutes.
A certificate error means the A record from step 1. A `0` from the last command with a `200`
from the loop usually means an account already exists on this instance, in which case the root
URL goes to the login page instead.

STOP: open https://<DOMAIN>/register in a browser, create your account with an email address and
a password you keep in your password manager, then come back here.

```bash
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/register
```

You should see: `302`.

If you do not: a `200` means registration is still open, which it must not be. Check that
`DISABLE_REGISTRATION` is `true` in /srv/plausible/compose.yml and that your account really was
created. Upstream lets that setting be bypassed exactly once, for the first account on an
instance with no users; after that the register page redirects to the login form and nobody can
sign themselves up. A running container is not success. Both of these asserts have to pass.

## 8. First backup and restore

Three artifacts. PostgreSQL holds the accounts, the sites and the settings. ClickHouse holds
every event ever recorded. The config archive holds what rebuilds the service around them,
including the only copy of your three secrets.

```bash
cd /srv/plausible
docker compose exec -T plausible_db pg_dump -U plausible -d plausible_db | gzip > /srv/plausible/backups/plausible-pg-$(date +%F).sql.gz
docker compose stop
sudo tar -C /srv/plausible/clickhouse -czf /srv/plausible/backups/plausible-events-$(date +%F).tar.gz data
sudo tar -C /srv/plausible -czf /srv/plausible/backups/plausible-config-$(date +%F).tar.gz compose.yml .env clickhouse/plausible-ce.xml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/plausible/backups/
```

You should see: three files, none of them tiny. The site is down for about half a minute while
the events archive is taken.

If you do not: a `.sql.gz` of about 20 bytes is an empty dump, which means `pg_dump` failed and
the shell created the file anyway. Run the dump line without `| gzip` to read the error. The
ClickHouse archive is deliberately taken with the containers stopped, because copying that
directory underneath a running server produces an archive that restores into corruption.

A backup on the same disk as the data is not a backup. Run this one on your own machine, not the
server:

```bash
mkdir -p ~/backups/plausible
scp vps:/srv/plausible/backups/* ~/backups/plausible/
```

You should see: three files copied, and all three listed by `ls -lh ~/backups/plausible/`.

If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:` prefix
only means something on your own machine, where the alias Prompt Zero created lives.

Now prove the restore, today, while the only thing at risk is an empty install:

```bash
cd /srv/plausible
docker compose down
sudo rm -rf /srv/plausible/postgres /srv/plausible/clickhouse/data
sudo install -d -m 700 /srv/plausible/postgres /srv/plausible/clickhouse/data
sudo tar -C /srv/plausible/clickhouse -xzf /srv/plausible/backups/plausible-events-$(date +%F).tar.gz
docker compose up -d plausible_db
sleep 30
gunzip -c /srv/plausible/backups/plausible-pg-$(date +%F).sql.gz | docker compose exec -T plausible_db psql -U plausible -d plausible_db
docker compose up -d
sleep 60
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/api/system/health/ready
```

You should see: `CREATE TABLE` and `COPY` lines from psql, then `200` from the last command, and
your account still works when you log in.

If you do not: `role "plausible" does not exist` means the database container had not finished
initialising, so wait longer and run the `gunzip` line again. If you ever restore onto a new
machine, put the config archive back before anything starts: PostgreSQL reads .env the moment it
initialises an empty directory, and a missing .env means a blank password and a database that
will not come up.

## 9. Updating later

New versions are listed at https://github.com/plausible/analytics/releases, and upstream ships
roughly two a year. Take all three backup artifacts first, then edit the `image:` line in
/srv/plausible/compose.yml to the new tag and its digest.

```bash
cd /srv/plausible
docker compose pull
docker compose up -d
docker compose logs --tail 40 plausible
```

You should see: migration output, then the server starting, and no repeating restart.

If you do not: put the old tag and digest back and run the same three commands. Read the release
notes for a major version before you take one: those can carry a data migration that runs longer
than the container's own start-up allowance, and interrupting it halfway is worse than waiting.
Re-run the health check from step 7 before you call the update done.

## 10. What will probably go wrong

ClickHouse is the part that will scare you. On the first start it lays out its data directory and
answers nothing for minutes while Plausible retries migrations against it and logs connection
failures that read like a broken install. I reached for `docker compose down` twice before the
health endpoint went green at about the four-minute mark, both times interrupting an install that
was working. Let step 7's loop run its full ten minutes first. And the XML file in step 2 is load
bearing: delete it because the install "works without it", and ClickHouse goes back to assuming
16 GB with the kernel taking that container in your first busy hour.

## 11. Out of scope

- Do not configure SMTP. Plausible CE runs without it; what you give up is password-reset mail
  and the emailed weekly report, neither worth a port-25 fight on a fresh VPS.
- Do not set `MAXMIND_LICENSE_KEY` or `IP_GEOLOCATION_DB`. Country data needs a MaxMind account,
  and this install trades the map for not having one.
- Do not set `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET`. Search Console is its own registration
  in Google Cloud.
- Do not set `HTTPS_PORT` and do not publish 80 or 443 from the container. The image carries its
  own certbot, which would fight Caddy over one certificate.

328 lines · 14,996 bytes

What this prompt will do
  1. Preflight
  2. Docker
  3. Layout
  4. Secrets
  5. compose.yml
  6. Nothing is public
  7. Start and verify
  8. First backup and restore
  9. Updating later
  10. What will probably go wrong
  11. Out of scope

Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.

paste it into Claude Code in a terminal on this computer · installs Docker Desktop if it is missing · no server, no domain

You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.

Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.

Install Plausible CE 3.2.1, with the PostgreSQL and ClickHouse it stores traffic in, under
~/selfhost/plausible, answering at http://localhost:8093.

## 1. Preflight

Say this first; it decides whether the user wants this install. `BASE_URL` here is
http://localhost:8093, which means "this computer" in whoever reads it, so a public website
cannot send events to it: a visitor's browser would post the pageview to its own machine. This
measures sites the user runs here, a development server or a preview build.

Detect the OS and measure:

```bash
uname -s
case "$(uname -s)" in
  Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
  Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
  MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```

`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash; on Linux the
distribution ID and codename print next, for step 2. This needs 2048 MB of RAM available and
10 GB free on the home disk, and all three images publish amd64 and arm64. On macOS and Windows
that figure is the host's and Docker Desktop takes its share, so check it has 4 GB. Under
either floor, print both and stop: ClickHouse is what gets killed, mid-write.

## 2. Docker

Check before installing anything:

```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```

If that printed `docker OK` and a compose version, skip to step 3.

Otherwise, install Docker for the OS step 1 detected:

- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
  Homebrew, STOP: tell the user to download Docker Desktop from
  https://www.docker.com/products/docker-desktop/ and install it, and wait until they
  confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
  terms, and wait for the whale icon to say it is running. Do not continue until they
  confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
  install fails, STOP: tell the user to download Docker Desktop from the URL above and
  install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
  ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
  prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
  terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
  repository, with its signing key saved to a file first, never piped into a shell. The
  fence is guarded, a no-op on anything but a Linux with apt:

```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
  sudo apt-get update
  sudo apt-get install -y ca-certificates curl
  sudo install -m 0755 -d /etc/apt/keyrings
  sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
  sudo chmod a+r /etc/apt/keyrings/docker.asc
  echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
  sudo apt-get update
  sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
  sudo usermod -aG docker "$USER"
fi
```

  Adding the user to the docker group is root-equivalent on this machine; say that to the
  user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
  plugin with their distribution's package manager, and to run this prompt again once
  `docker info` works.

Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.

## 3. Layout

```bash
mkdir -p ~/selfhost/plausible/backups ~/selfhost/plausible/clickhouse
cat > ~/selfhost/plausible/clickhouse/plausible-ce.xml <<'EOF'
<clickhouse>
  <!-- Sized for a small machine, not the 16 GB one ClickHouse assumes. -->
  <logger><level>warning</level><console>true</console></logger>
  <listen_host>0.0.0.0</listen_host>
  <mark_cache_size>524288000</mark_cache_size>
  <max_server_memory_usage_to_ram_ratio>0.6</max_server_memory_usage_to_ram_ratio>
  <metric_log remove="remove"/><asynchronous_metric_log remove="remove"/>
  <query_log remove="remove"/><query_thread_log remove="remove"/>
  <trace_log remove="remove"/><part_log remove="remove"/>
</clickhouse>
EOF
ls -la ~/selfhost/plausible ~/selfhost/plausible/clickhouse
```

Assert: `backups` and `clickhouse` exist and the XML file is listed. ClickHouse is built for a
16 GB machine and behaves that way unless told otherwise; that file holds it down. The data
directories are Docker volumes, so there is no `data` folder and no ownership fix.

## 4. Secrets

Three secrets, generated here: the session key base, the key encrypting two-factor secrets at
rest, and the PostgreSQL password. Print none of them, and keep them out of your summary and out
of the logs.

```bash
umask 077
cat > ~/selfhost/plausible/.env <<EOF
BASE_URL=http://localhost:8093
SECRET_KEY_BASE=$(openssl rand -base64 48)
TOTP_VAULT_KEY=$(openssl rand -base64 32)
POSTGRES_PASSWORD=$(openssl rand -hex 32)
EOF
chmod 600 ~/selfhost/plausible/.env
umask 022
ls -l ~/selfhost/plausible/.env
```

Assert: mode `-rw-------`. Git Bash ships openssl, so these run everywhere. Upstream documents
the key base as at least 64 bytes and the vault key as 32 bytes, encrypting two-factor secrets
with AES256-GCM. On Windows those mode bits are advisory: NTFS ignores them, and the real
boundary is the user's own account.

## 5. compose.yml

```bash
cat > ~/selfhost/plausible/compose.yml <<'EOF'
# Plausible CE · the deterministic fallback for the local path, authored by
# caniselfhostit from https://plausible.io/docs/self-hosting , the quick start
# at https://github.com/plausible/community-edition and its wiki page
# https://github.com/plausible/community-edition/wiki/configuration . Not
# copied from a repository.
#
# Three services, paths relative to ~/selfhost/plausible/ so one file works on
# macOS, Linux and Windows. Data lives in named volumes because Postgres,
# ClickHouse and the app each want a uid Docker Desktop on Windows cannot grant
# on a home-directory bind. Digests read 2026-08-05, amd64 and arm64 both.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  plausible_db:
    image: postgres:16.14-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
    restart: unless-stopped
    environment:
      POSTGRES_DB: plausible_db
      POSTGRES_USER: plausible
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - plausible-pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible_db"]
      interval: 10s
      retries: 18
    # No `ports:`: 5432 is reachable only from the other containers.

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.12.6.70-alpine@sha256:cd450891db46cc6ffe313ca2b0fb7dbfb897a6873ca74a724cbe050a2cf62621
    restart: unless-stopped
    environment:
      CLICKHOUSE_SKIP_USER_SETUP: "1"
    volumes:
      - plausible-chdata:/var/lib/clickhouse
      - ./clickhouse/plausible-ce.xml:/etc/clickhouse-server/config.d/plausible-ce.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8123/ping || exit 1"]
      interval: 10s
      retries: 18
    # No `ports:`: 8123 stays inside.

  plausible:
    image: ghcr.io/plausible/community-edition:v3.2.1@sha256:33e60bfb40f2df5da00f8753b76fad04f67dba3abe6d73eb516e440e3fb62985
    restart: unless-stopped
    command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    env_file: ./.env
    environment:
      TMPDIR: /var/lib/plausible/tmp
      HTTP_PORT: "8000"
      DISABLE_REGISTRATION: "true"
      DATABASE_URL: postgres://plausible:${POSTGRES_PASSWORD}@plausible_db:5432/plausible_db
      CLICKHOUSE_DATABASE_URL: http://plausible_events_db:8123/plausible_events_db
    volumes:
      - plausible-appdata:/var/lib/plausible
    ports:
      # Loopback only: this computer, nothing else on the wifi.
      - "127.0.0.1:8093:8000"
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_healthy

volumes:
  plausible-pgdata:
  plausible-chdata:
  plausible-appdata:
EOF
cd ~/selfhost/plausible && docker compose config >/dev/null && echo "compose OK"
```

Assert: that prints `compose OK`.

## 6. Nothing is public

No reverse proxy, no certificate, no firewall rule. Each is a decision:

- No DNS and no TLS: there is no hostname to resolve and no public name for a certificate to
  attest. Browsers treat http://localhost as a secure context, so pages needing crypto work.
- No firewall rule: nothing is published beyond loopback, so no port needs closing.

Assert by reading step 5's file back: the only published port is `- "127.0.0.1:8093:8000"`, and
neither database publishes one at all. The user's phone cannot reach this, nor a laptop on the
same wifi, nor anyone on the internet.

## 7. Start and verify

The first start creates both databases, runs every migration, and gives ClickHouse minutes to
lay out its data. Do not intervene inside it.

```bash
cd ~/selfhost/plausible
docker compose pull
docker compose up -d
for i in $(seq 1 40); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8093/api/system/health/ready); echo "$i $code"; [ "$code" = 200 ] && break; sleep 15; done
curl -sS http://localhost:8093/api/system/health/ready
curl -sSL http://localhost:8093/ | grep -c 'Register your Plausible CE account'
```

Assert all three, printing what you got: the loop ends on `200`, the health JSON reads `ok` for
`"clickhouse"` and `"postgres"`, and the last prints `1`, because with no account yet
http://localhost:8093/ redirects to http://localhost:8093/register, whose first screen carries
the heading `Register your Plausible CE account`. On any miss, stop, run
`docker compose logs --tail 40 plausible` and name the cause: `connection refused` against
ClickHouse points at step 3's XML file, a log still in migrations wants more time, and `port is
already allocated` means something else holds 8093. A running container is not success.

STOP: tell the user to open http://localhost:8093/register, create the first account with a
password they keep in their password manager, and wait. Do not continue until they confirm.

Then prove registration is closed:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8093/register
```

Assert: `302`. `DISABLE_REGISTRATION` is `true`, which upstream bypasses once, for the first
account on an instance with no users. Both asserts pass before you report success.

## 8. First backup and restore

Three artifacts: accounts and sites in PostgreSQL, every event in ClickHouse, and a config archive
that rebuilds the service around them, secrets included. The events come out through a throwaway
container, because that data sits in a volume.

```bash
cd ~/selfhost/plausible
docker compose exec -T plausible_db pg_dump -U plausible -d plausible_db | gzip > backups/plausible-pg-$(date +%F).sql.gz
docker compose stop
docker compose run --rm --no-deps -T --entrypoint sh plausible_events_db -c 'tar -C /var/lib/clickhouse -czf - .' > backups/plausible-events-$(date +%F).tar.gz
tar -czf backups/plausible-config-$(date +%F).tar.gz compose.yml .env clickhouse
docker compose start
ls -lh backups/
```

Assert: all three exist, all three are non-empty, and print all three sizes. `pg_dump` snapshots a
running database consistently, so it goes first; the events archive is cold, because copying it
under a live server restores into corruption. Downtime is half a minute.

These sit on the same disk as the data, which is not a backup, and on a laptop the disk and the
machine fail together. Ask the user for a destination that leaves this computer, a sync folder or
a USB stick, and copy all three there with `cp`; in Git Bash a Windows drive is `/d/Backups`.
Assert: the user confirms the three names are there.

To restore: `cd ~/selfhost/plausible`, untar the config archive there first because PostgreSQL
reads .env the moment it initialises an empty volume, then `docker compose down -v`. Then the
events command above, with `-xzf -` and `<` in place of `-czf -` and `>`, puts the events back.
Then `docker compose up -d plausible_db`, wait thirty seconds, pipe `gunzip -c` on the `.sql.gz`
into `docker compose exec -T plausible_db psql -U plausible -d plausible_db`, then
`docker compose up -d` and re-run step 7's health check.

## 9. Updating later

New versions are listed at https://github.com/plausible/analytics/releases, roughly two a year.
Take all three backups first, then edit the image line in ~/selfhost/plausible/compose.yml to
the new tag and digest:

```bash
cd ~/selfhost/plausible
docker compose pull
docker compose up -d
docker compose logs --tail 40 plausible
```

Migrations run on the way up, so watch that log until it settles, then re-run step 7's health
check. Read the release notes for a major version first: they can carry a longer migration.

## 10. What will probably go wrong

I rebooted, opened the dashboard the next morning, and got a connection refused that read like a
lost database. It was not: Docker Desktop had not started with the session, so nothing listened
on 8093 and nothing had been counted overnight either. `restart: unless-stopped` acts
only once the Docker daemon is up. Turn on its start-at-login setting, and after a reboot run
`cd ~/selfhost/plausible && docker compose up -d` before concluding anything broke. The other
half of the same fact: while this machine sleeps, nothing is counted at all.

## 11. Out of scope

- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not change `BASE_URL` to this machine's LAN address, and do not rebind 8093 to 0.0.0.0 so a
  phone can reach it. That puts a login page on every network the user joins.
- Do not configure SMTP, and do not set `MAXMIND_LICENSE_KEY`, `IP_GEOLOCATION_DB`,
  `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET`.
compose.local.ymlthe services, pinned · local layout73 lines

authored from upstream docs, never pasted · 2,824 bytes

# Plausible CE · the deterministic fallback for the local path, authored by
# caniselfhostit from https://plausible.io/docs/self-hosting , the quick start
# at https://github.com/plausible/community-edition and its wiki page
# https://github.com/plausible/community-edition/wiki/configuration . Not
# copied from a repository.
#
# Three services, paths relative to ~/selfhost/plausible/ so one file works on
# macOS, Linux and Windows. Data lives in named volumes because Postgres,
# ClickHouse and the app each want a uid Docker Desktop on Windows cannot grant
# on a home-directory bind. Digests read 2026-08-05, amd64 and arm64 both.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  plausible_db:
    image: postgres:16.14-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
    restart: unless-stopped
    environment:
      POSTGRES_DB: plausible_db
      POSTGRES_USER: plausible
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - plausible-pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible_db"]
      interval: 10s
      retries: 18
    # No `ports:`: 5432 is reachable only from the other containers.

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.12.6.70-alpine@sha256:cd450891db46cc6ffe313ca2b0fb7dbfb897a6873ca74a724cbe050a2cf62621
    restart: unless-stopped
    environment:
      CLICKHOUSE_SKIP_USER_SETUP: "1"
    volumes:
      - plausible-chdata:/var/lib/clickhouse
      - ./clickhouse/plausible-ce.xml:/etc/clickhouse-server/config.d/plausible-ce.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8123/ping || exit 1"]
      interval: 10s
      retries: 18
    # No `ports:`: 8123 stays inside.

  plausible:
    image: ghcr.io/plausible/community-edition:v3.2.1@sha256:33e60bfb40f2df5da00f8753b76fad04f67dba3abe6d73eb516e440e3fb62985
    restart: unless-stopped
    command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    env_file: ./.env
    environment:
      TMPDIR: /var/lib/plausible/tmp
      HTTP_PORT: "8000"
      DISABLE_REGISTRATION: "true"
      DATABASE_URL: postgres://plausible:${POSTGRES_PASSWORD}@plausible_db:5432/plausible_db
      CLICKHOUSE_DATABASE_URL: http://plausible_events_db:8123/plausible_events_db
    volumes:
      - plausible-appdata:/var/lib/plausible
    ports:
      # Loopback only: this computer, nothing else on the wifi.
      - "127.0.0.1:8093:8000"
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_healthy

volumes:
  plausible-pgdata:
  plausible-chdata:
  plausible-appdata:

agent-readable mirror: /self-host/plausible.md

The files, if you'd rather do it yourself

The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.

compose.ymlthe services, pinned69 lines

authored from upstream docs, never pasted · 2,926 bytes

# Plausible CE · the deterministic fallback, authored by caniselfhostit from
# https://plausible.io/docs/self-hosting , the quick start at
# https://github.com/plausible/community-edition , its wiki page
# https://github.com/plausible/community-edition/wiki/configuration and
# https://clickhouse.com/docs/en/operations/tips . Not copied from a repository.
#
# Three services: the app, the PostgreSQL holding accounts and sites, and the
# ClickHouse holding every pageview event, which assumes a 16 GB machine and is
# cut down by the read-only XML file step 2 writes. Neither database
# publishes a host port. Digests read 2026-08-05; amd64 and arm64 both.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.

services:
  plausible_db:
    image: postgres:16.14-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
    restart: unless-stopped
    environment:
      POSTGRES_DB: plausible_db
      POSTGRES_USER: plausible
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - /srv/plausible/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U plausible -d plausible_db"]
      interval: 10s
      retries: 18
    # No `ports:`: 5432 is reachable only from the other containers.

  plausible_events_db:
    image: clickhouse/clickhouse-server:24.12.6.70-alpine@sha256:cd450891db46cc6ffe313ca2b0fb7dbfb897a6873ca74a724cbe050a2cf62621
    restart: unless-stopped
    environment:
      CLICKHOUSE_SKIP_USER_SETUP: "1"
    volumes:
      - /srv/plausible/clickhouse/data:/var/lib/clickhouse
      - /srv/plausible/clickhouse/plausible-ce.xml:/etc/clickhouse-server/config.d/plausible-ce.xml:ro
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:8123/ping || exit 1"]
      interval: 10s
      retries: 18
    # No `ports:`: 8123 stays on the compose network.

  plausible:
    image: ghcr.io/plausible/community-edition:v3.2.1@sha256:33e60bfb40f2df5da00f8753b76fad04f67dba3abe6d73eb516e440e3fb62985
    restart: unless-stopped
    command: sh -c "/entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
    env_file: /srv/plausible/.env
    environment:
      TMPDIR: /var/lib/plausible/tmp
      # Caddy terminates TLS on the host, so HTTPS_PORT stays unset here.
      HTTP_PORT: "8000"
      DISABLE_REGISTRATION: "true"
      DATABASE_URL: postgres://plausible:${POSTGRES_PASSWORD}@plausible_db:5432/plausible_db
      CLICKHOUSE_DATABASE_URL: http://plausible_events_db:8123/plausible_events_db
    volumes:
      - /srv/plausible/data:/var/lib/plausible
    ports:
      # Loopback only: the host's Caddy is the only thing that reaches 8093.
      - "127.0.0.1:8093:8000"
    depends_on:
      plausible_db:
        condition: service_healthy
      plausible_events_db:
        condition: service_healthy
Caddyfilethe hostname and TLS26 lines

authored from upstream docs, never pasted · 975 bytes

# Plausible CE · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/plausible/community-edition/wiki/reverse-proxy and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box, which is also BASE_URL
# in .env and is baked into the tracking snippet on every page you measure. The
# dashboard is a LiveView: reverse_proxy passes its WebSocket upgrade unaided.

<DOMAIN> {
	encode zstd gzip

	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
		-Server
	}
	# No X-Frame-Options: Plausible's shared dashboards are meant to be iframed.

	# 8093 is the loopback port compose publishes on this host. It is not a
	# container port and it is not open in the firewall.
	reverse_proxy 127.0.0.1:8093
}
install.shthe same install, no agent186 lines

authored from upstream docs, never pasted · 8,825 bytes

#!/usr/bin/env bash
# Plausible CE · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
#   DOMAIN_HOST=stats.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
#   https://plausible.io/docs/self-hosting
#   https://github.com/plausible/community-edition
#   https://github.com/plausible/community-edition/wiki/configuration
#   https://github.com/plausible/community-edition/wiki/reverse-proxy
#   https://clickhouse.com/docs/en/operations/tips
#
# Three secrets are generated here, on this machine: the session key base, the
# key that encrypts two-factor secrets at rest, and the PostgreSQL password.
# All three go into /srv/plausible/.env with mode 600 and none is ever printed.
#
# DOMAIN_HOST is also BASE_URL, and it is written into the tracking snippet on
# every page you measure. Choose it once.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail

APP_DIR="${APP_DIR:-/srv/plausible}"
DOMAIN_HOST="${DOMAIN_HOST:-}"

die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }

# --- 1. Refuse to start on a machine that is not ready -----------------------

[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. stats.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"

avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 2048 ] || die "only ${avail_mb} MB of RAM available; ClickHouse plus PostgreSQL wants 2048 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 10 ] || die "only ${avail_gb} GB free on /srv; this install wants 10 GB"

grep -q -E 'sse4_2|asimd' /proc/cpuinfo \
	|| die "this CPU reports neither SSE 4.2 nor NEON, and ClickHouse needs one of them"

resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."

# --- 2. Lay the files out ----------------------------------------------------
#
# postgres/ and clickhouse/data stay root-owned at 700: each image chowns its
# own data directory on first start. data/ goes to uid 999, the uid the
# Plausible image runs as.

sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups" "$APP_DIR/clickhouse"
sudo install -d -m 700 "$APP_DIR/postgres" "$APP_DIR/clickhouse/data"
sudo install -d -m 750 -o 999 -g 999 "$APP_DIR/data"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"

# ClickHouse ships tuned for a 16 GB machine. This is what holds it inside a
# small VPS; compose.yml mounts it read-only.
cat > "$APP_DIR/clickhouse/plausible-ce.xml" <<'EOF'
<clickhouse>
  <!-- Sized for a small machine, not the 16 GB one ClickHouse assumes. -->
  <logger><level>warning</level><console>true</console></logger>
  <listen_host>0.0.0.0</listen_host>
  <mark_cache_size>524288000</mark_cache_size>
  <max_server_memory_usage_to_ram_ratio>0.6</max_server_memory_usage_to_ram_ratio>
  <metric_log remove="remove"/><asynchronous_metric_log remove="remove"/>
  <query_log remove="remove"/><query_thread_log remove="remove"/>
  <trace_log remove="remove"/><part_log remove="remove"/>
</clickhouse>
EOF
chmod 644 "$APP_DIR/clickhouse/plausible-ce.xml"

# --- 3. Generate the three secrets, on the server ----------------------------
#
# Base64 for the two Elixir keys because upstream documents them that way, hex
# for the database password because it travels inside a connection string.
# Read them later with
#   sudo grep -E 'SECRET_KEY_BASE|TOTP_VAULT_KEY|POSTGRES_PASSWORD' /srv/plausible/.env

if [ ! -f "$APP_DIR/.env" ]; then
	umask 077
	cat > "$APP_DIR/.env" <<-ENVFILE
		BASE_URL=https://${DOMAIN_HOST}
		SECRET_KEY_BASE=$(openssl rand -base64 48)
		TOTP_VAULT_KEY=$(openssl rand -base64 32)
		POSTGRES_PASSWORD=$(openssl rand -hex 32)
	ENVFILE
	chmod 600 "$APP_DIR/.env"
	umask 022
fi

cd "$APP_DIR"
docker compose config >/dev/null

# --- 4. Caddy site block, on the host ----------------------------------------

if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
	sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-plausible"
	printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
	sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

# --- 5. Ports: two open, and none of 8093, 5432 or 8123 is one of them -------

if command -v ufw >/dev/null 2>&1; then
	echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8093, 5432 and 8123 stay closed"
	sudo ufw allow 80/tcp
	sudo ufw allow 443/tcp
	sudo ufw allow 443/udp
	sudo ufw status verbose
fi

# --- 6. Start it -------------------------------------------------------------
#
# The command line in compose.yml creates both databases, runs every pending
# migration across them, then starts the server. ClickHouse takes minutes to lay
# out its data directory on the first start, so the wait below is generous.

docker compose pull
docker compose up -d

echo "==> waiting for https://${DOMAIN_HOST}/api/system/health/ready (up to ten minutes)"
for _ in $(seq 1 40); do
	code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/api/system/health/ready" || true)"
	[ "$code" = "200" ] && break
	sleep 15
done
[ "${code:-}" = "200" ] || die "the health endpoint answered ${code:-nothing}. Check: docker compose logs --tail 40 plausible"

health="$(curl -sS "https://${DOMAIN_HOST}/api/system/health/ready" || true)"
case "$health" in
	*'"clickhouse":"ok"'*) : ;;
	*) die "ClickHouse is not reporting ok: ${health}. Check: docker compose logs --tail 40 plausible_events_db" ;;
esac
case "$health" in
	*'"postgres":"ok"'*) : ;;
	*) die "PostgreSQL is not reporting ok: ${health}. Check: docker compose logs --tail 20 plausible_db" ;;
esac

# The first screen. With no account yet the root URL redirects to /register.
curl -sSL "https://${DOMAIN_HOST}/" | grep -q 'Register your Plausible CE account' \
	|| die "the first screen did not carry 'Register your Plausible CE account'. Either an account already exists on this instance, or Caddy is not reaching the container."

# --- 7. The first backup, before day one ends --------------------------------
#
# pg_dump runs against a live database. The ClickHouse archive is taken cold,
# because copying that directory under a running server restores into corruption.

STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose exec -T plausible_db pg_dump -U plausible -d plausible_db | gzip > "$APP_DIR/backups/plausible-pg-${STAMP}.sql.gz"
docker compose stop
sudo tar -C "$APP_DIR/clickhouse" -czf "$APP_DIR/backups/plausible-events-${STAMP}.tar.gz" data
sudo tar -C "$APP_DIR" -czf "$APP_DIR/backups/plausible-config-${STAMP}.tar.gz" compose.yml .env clickhouse/plausible-ce.xml -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/plausible-pg-${STAMP}.sql.gz" ] || die "the database dump is empty"
[ -s "$APP_DIR/backups/plausible-events-${STAMP}.tar.gz" ] || die "the events archive is empty"
[ -s "$APP_DIR/backups/plausible-config-${STAMP}.tar.gz" ] || die "the config archive is empty"

cat <<-DONE

	Plausible CE is answering at https://${DOMAIN_HOST}/api/system/health/ready

	  1. Open https://${DOMAIN_HOST}/register and create the first account. That
	     page works exactly once: DISABLE_REGISTRATION is true, which upstream
	     bypasses only while the instance has no users. Afterwards, confirm it:
	       curl -sS -o /dev/null -w '%{http_code}\n' https://${DOMAIN_HOST}/register
	     That must print 302. If it prints 200, stop and investigate.
	  2. Your three secrets are in $APP_DIR/.env, mode 600. Read them with
	       sudo grep -E 'SECRET_KEY_BASE|TOTP_VAULT_KEY|POSTGRES_PASSWORD' $APP_DIR/.env
	     and put them in your password manager. None was printed here.
	  3. Add your site in the dashboard, then paste the snippet it gives you into
	     the pages you want counted. Nothing is measured until you do.
	  4. First backup written to $APP_DIR/backups: a PostgreSQL dump, a ClickHouse
	     archive and a config archive. They are on the same disk as the data,
	     which is not a backup. Copy them off the box tonight:
	       scp vps:$APP_DIR/backups/* ~/backups/plausible/

DONE

What you're signing up for

The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Plausible.

  • You own a ClickHouse. It is the piece that makes the dashboard fast and it is also the piece that assumes a bigger machine than you rented, so this install ships a config file that cuts its caches and its memory ceiling. Delete that file and the kernel eventually kills the container.
  • Three things to back up, not one. Accounts and sites are in PostgreSQL, every event is in ClickHouse, and the third archive holds the secrets that decrypt the first two. A dump of only the database you remembered is a restore that loses a year of traffic.
  • Two releases a year. Upstream ships Community Edition on a slower cadence than the cloud, so a fix you read about in a changelog may be months from a tag you can pin. The AGPL means nothing is withheld from what does ship.
  • No funnels, no user journeys, no ecommerce revenue goals, no SSO and no sites API. Those are the cloud-only features, the vendor says so plainly, and they are a real part of what the paid tiers sell.
  • Nothing is counted until you paste the snippet. The install ends with an empty dashboard, and the domain in that snippet is the one you chose at step 1, on every page, permanently. The register page also works exactly once: the same switch that closes it to strangers blocks invited users too, so adding a teammate later means changing DISABLE_REGISTRATION to invite_only in compose.yml and running docker compose up -d again.

Where this came from

“Our only source of funding is the managed cloud service: subscribers pay us directly, and we use that revenue to pay our team and develop Plausible.”

  • Community Edition is the free, self-hosted, AGPL-licensed release of the same product, and the vendor documents self-hosting itself rather than treating it as unsupported. source
  • Upstream's own compose file runs three services, the app plus PostgreSQL plus ClickHouse, and the quick start asks for at least 2 GB of RAM and a CPU with SSE 4.2 or NEON. source
  • SECRET_KEY_BASE must be at least a 64-byte string, TOTP_VAULT_KEY is a 32-byte value encrypting two-factor secrets with AES256-GCM, and HTTP_PORT defaults to 8000. source
  • From version 2.1.2 a reverse proxy is optional because the image can request its own certificate, and the documented Caddy setup is a single reverse_proxy line to the HTTP port. source
  • ClickHouse assumes a machine with 16 GB or more, and the documented fix for smaller ones is to cut the mark cache and the memory ceiling in a config file. source

Questions people actually ask

Answered from this page's own data — the same numbers, in sentences.

  • Can I self-host Plausible?

    Not Plausible itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is Plausible CE. The same privacy-first analytics dashboard the vendor sells, on your own domain, with no pageview meter counting against you. The install is one evening: 3 containers behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 120 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.

  • What replaces Plausible?

    Plausible CE. The same privacy-first analytics dashboard the vendor sells, on your own domain, with no pageview meter counting against you. The same company writes it, from the same repository, under AGPL, and points its own documentation at the compose file. That makes this the rare case where self-hosting is the vendor-sanctioned path rather than a workaround: the dashboard, the script and the numbers are the ones you were paying for, and what is missing is named out loud (funnels, user journeys, ecommerce revenue goals, SSO, the sites API). What you take on instead of the bill is a ClickHouse, a PostgreSQL, three backup artifacts and roughly two upgrades a year. Plausible CE is AGPL-3.0-licensed and free; nothing on this page is a hosted service we sell you.

  • What does self-hosting cost compared to Plausible?

    2048 MB of RAM and 10 GB of disk — the smallest tier most VPS hosts sell, about $10 a month. Plausible CE itself is free and AGPL-3.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Plausible Growth, $14/mo — $168 a year.

  • How hard is it really?

    ONE EVENING — 1–3 hours. The rule that produced that verdict: up to three containers and at most one outside integration. You will type more than one command and read a page of documentation, and it will be running before you go to bed. The tier is derived from seven countable facts about the Plausible CE install, not from anyone's impression of it, and the whole rubric is published on the methodology page.

  • Can I run Plausible CE on my own computer instead of a server?

    Yes — that is the second path in the prompt box above. "On my computer" installs the same Plausible CE on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. The catch: Every event has to reach http://localhost:8093, which is a different computer in every visitor's browser, so on this path it counts sites you run on this machine and nothing you have published. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.

Content last checked 2026-08-06. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.