Plausible Community Edition (CE) can be self-hosted on Ubuntu 24.04 with Docker Compose, PostgreSQL, ClickHouse, persistent Docker volumes, and built-in HTTPS. For the current official quickstart, you do not need to install Nginx, Caddy, or Certbot just to get a trusted public TLS endpoint: Plausible CE can listen on ports 80 and 443 and manage Let's Encrypt certificates itself.
This tutorial follows the current Plausible CE v3.2.1 deployment layout verified on September 4, 2026. It uses the repository's compose.yml, stores required settings in .env, adds public port mappings through compose.override.yml, keeps the upstream Compose file untouched for easier upgrades, and uses the dashboard-generated tracking snippet rather than the older hard-coded /js/script.js pattern. Plausible's official quickstart recommends at least 2 GB of RAM and a CPU with SSE 4.2 on x86 or NEON support on ARM because ClickHouse is part of the stack.
The original Plausible deployment used a Raff Linux VM as the Ubuntu 24.04 host. The original workflow was tested on Ubuntu 24.04 LTS on a Raff 1 vCPU / 2 GB RAM VM (Tier 2). This revision re-verifies the upstream Plausible CE workflow and documentation; it does not claim a new end-to-end benchmark or capacity test. When you self-host Plausible, you are responsible for upgrades, backups, availability, monitoring, and recovery.
Prerequisites:
- Ubuntu 24.04 with at least 2 GB RAM recommended by Plausible CE
- A CPU with SSE 4.2 support on x86 or NEON support on ARM
- Docker Engine with the Docker Compose plugin
- SSH access with sudo privileges
- A domain such as
analytics.example.compointing to the VM - Public TCP ports 80 and 443 available for HTTPS
Step 1 — Verify Ubuntu, Docker, CPU support, memory, DNS, and web ports
Confirm the operating system:
cat /etc/os-release
Check Docker and Compose:
docker --version docker compose version
If Docker is not installed, use How to Install Docker on Ubuntu 24.04 before continuing.
Check memory:
free -h
Check CPU flags on an x86 VM:
lscpu | grep -i '^Flags' | grep -o 'sse4_2' || true
On ARM64 systems, inspect the feature line instead:
lscpu | grep -i '^Flags\|^Features'
Plausible's current CE quickstart requires a CPU that supports SSE 4.2 or NEON because ClickHouse depends on those instruction sets.
Check DNS in both address families:
dig +short A analytics.example.com dig +short AAAA analytics.example.com
Check whether another service already owns ports 80 or 443:
sudo ss -lntp | grep -E ':(80|443)\b' || true
A stale AAAA record can cause confusing HTTPS behavior even when the IPv4 A record is correct, so remove or correct IPv6 DNS if this server is not serving the hostname over IPv6.
Verify: Ubuntu should report 24.04, Docker Compose should work, the VM should meet the Plausible CE memory/CPU requirements, DNS should resolve to this server, and ports 80/443 should be available for the deployment.
Step 2 — Install the small host utilities used by this tutorial
Install Git, OpenSSL, DNS tools, and curl:
sudo apt update sudo apt install -y git openssl dnsutils curl ca-certificates
Do not reinstall Docker from a second package source if it is already working. Mixing Ubuntu's Docker packages with Docker's official repository packages can make future upgrades harder to reason about.
Confirm Git and OpenSSL are available:
git --version openssl version
Verify: git --version and openssl version should both return installed versions without errors.
Step 3 — Clone a pinned Plausible Community Edition release
Create a dedicated installation path owned by your administrative user:
sudo install -d -o "$USER" -g "$USER" /opt/plausible-ce
Clone the current CE release used by this revision:
git clone -b v3.2.1 --single-branch \ https://github.com/plausible/community-edition \ /opt/plausible-ce
Enter the project:
cd /opt/plausible-ce
Inspect the files:
ls -la git describe --tags --exact-match
The current repository layout includes compose.yml; older tutorials that expect docker-compose.yml or plausible-conf.env are describing an older CE setup.
Pinning an exact release makes upgrades intentional rather than silently changing your application stack whenever a floating image tag moves.
Verify: /opt/plausible-ce/compose.yml should exist and git describe --tags --exact-match should report v3.2.1 for this revision.
Step 4 — Create a protected Plausible environment file
Move into the project directory:
cd /opt/plausible-ce
Set a restrictive file-creation mask and generate the secret without printing it to the terminal:
umask 077 SECRET_KEY_BASE="$(openssl rand -base64 48 | tr -d '\n')"
Create .env:
cat > .env <<EOF BASE_URL=https://analytics.example.com SECRET_KEY_BASE=${SECRET_KEY_BASE} HTTP_PORT=80 HTTPS_PORT=443 DISABLE_REGISTRATION=invite_only EOF unset SECRET_KEY_BASE chmod 600 .env
Replace analytics.example.com with your real hostname before starting the stack.
BASE_URL is required and is used for generated links and Plausible's origin-related security checks. SECRET_KEY_BASE is also required; the current Plausible CE documentation uses openssl rand -base64 48 to generate it. DISABLE_REGISTRATION=invite_only makes the current default registration policy explicit.
Do not commit .env to Git or paste its secret values into support tickets.
Verify:
stat -c '%a %n' .env grep -E '^(BASE_URL|HTTP_PORT|HTTPS_PORT|DISABLE_REGISTRATION)=' .env
The file mode should be 600, the public URL should be correct, and the command should not print SECRET_KEY_BASE.
Step 5 — Expose Plausible with its built-in automatic HTTPS
Keep the tracked compose.yml untouched and put local deployment changes in compose.override.yml:
cd /opt/plausible-ce cat > compose.override.yml <<'EOF' services: plausible: ports: - "80:80" - "443:443" EOF
Plausible's official quickstart uses this override together with HTTP_PORT=80 and HTTPS_PORT=443. That combination enables Plausible CE's built-in Let's Encrypt certificate handling.
Using an override file also keeps local changes separate from the repository's versioned compose.yml, reducing unnecessary merge conflicts during future upgrades.
Validate the merged Compose configuration:
docker compose config -q
List the services and persistent volume definitions:
docker compose config --services docker compose config --volumes
The current v3.2.1 stack includes PostgreSQL, ClickHouse, the Plausible app, and persistent volumes for database, event, log, and Plausible application state.
Verify: docker compose config -q should exit successfully and the merged configuration should expose only the Plausible service on public ports 80 and 443.
Step 6 — Allow only the intended public traffic
Inspect UFW before making changes:
sudo ufw status verbose
If UFW is already active, confirm your actual SSH path is allowed before adding web ports. For a standard SSH service:
sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp
If UFW is inactive, do not enable it blindly on a remote server. First make sure your SSH rule matches the port and source you actually use. See Set Up UFW Firewall on Ubuntu 24.04 for the full workflow.
Also review any provider-side firewall protecting the VM. Public HTTPS requires inbound TCP 80 and 443 to reach this host.
Docker publishes container ports using its own packet-filtering rules. Docker's documentation warns that published container ports can behave differently from ordinary UFW-managed host services, so do not treat UFW alone as the only network control for Docker workloads.
Verify: Your working SSH path should remain allowed, ports 80/443 should be reachable for Plausible, and no database or ClickHouse port should be intentionally exposed to the public internet.
Step 7 — Start Plausible CE and inspect the service state
Start the complete stack:
cd /opt/plausible-ce docker compose up -d
Check state:
docker compose ps
Inspect recent application logs:
docker compose logs --tail=100 plausible
Inspect database logs if startup does not settle:
docker compose logs --tail=100 plausible_db docker compose logs --tail=100 plausible_events_db
The current Compose file waits for PostgreSQL and ClickHouse health checks before starting the Plausible application. First startup can take longer because databases are initialized and application migrations run.
Do not use hard-coded RAM or startup-time expectations as a health check. Image versions, disk state, host load, and existing data all affect startup behavior.
Verify: docker compose ps should show PostgreSQL and ClickHouse healthy and the Plausible application running without a repeating restart loop.
Step 8 — Verify automatic HTTPS and create the first account
Test HTTPS from the server:
curl -I https://analytics.example.com
Test the HTTP path:
curl -I http://analytics.example.com
For a valid public hostname, HTTP should redirect to HTTPS and the HTTPS endpoint should return an application response with a trusted certificate.
If certificate issuance is still in progress or fails, inspect the application log rather than repeatedly restarting the stack:
docker compose logs --tail=150 plausible
Open in a browser:
https://analytics.example.com
Create the first user account. With DISABLE_REGISTRATION=invite_only, future public self-registration is restricted. If you plan to invite other users or require email verification, configure a supported mail provider using Plausible's current CE configuration options before relying on email workflows.
Verify: The dashboard should load over trusted HTTPS, the first account should sign in successfully, and plain HTTP should redirect to the HTTPS hostname.
Step 9 — Add a website and use the dashboard-generated tracking snippet
In Plausible, add the domain you want to measure. For example, add example.com rather than including https:// in the site domain field.
Then open the site's installation or tracking settings and copy the exact tracking snippet generated by your Plausible instance into the <head> of your website.
Do not copy the old generic pattern from outdated tutorials:
/js/script.js
Plausible introduced a newer site-specific tracking script model in October 2025. The current snippet is generated for the site and can reflect enabled tracking options. Using the dashboard-generated snippet avoids hard-coding a path that may not match the current tracker configuration.
After adding the snippet, deploy the website change and load the site in a browser.
Official current tracking guidance: Add the Plausible tracking script.
Verify: View the website's rendered HTML or browser developer tools and confirm the exact Plausible snippet from the site's installation screen is present in the page <head>.
Step 10 — Verify analytics end to end from a real browser visit
Open the tracked website in a normal browser session and navigate between a few pages.
In browser developer tools, open Network and filter for your Plausible hostname. Confirm that the tracker script loads and that an analytics event request is sent when a pageview occurs.
Then return to the Plausible dashboard and use its installation check or realtime view to confirm the visit is being received.
This is a better production verification than posting a fabricated event with a forged X-Forwarded-For header or querying an internal ClickHouse table name. Internal schemas and tracker payloads can change between CE releases; the public tracker and dashboard are the interfaces your real site depends on.
If the request is blocked, check browser extensions, Content Security Policy, the generated snippet, the site's domain setting, and the Plausible BASE_URL before changing database configuration.
Verify: A real browser pageview should produce a request to your Plausible instance and appear in the site's Plausible dashboard or installation check.
Step 11 — Use an external Caddy reverse proxy only when your topology needs one
Starting with Plausible CE 2.1.2, an external reverse proxy is optional because Plausible can manage HTTPS itself. Keep the built-in HTTPS setup from the previous steps unless you already operate Caddy/Nginx as a shared edge or have another architectural reason to proxy Plausible.
If you choose Caddy, change .env so Plausible listens on its internal HTTP port instead of managing public TLS:
BASE_URL=https://analytics.example.com HTTP_PORT=8000
Remove HTTPS_PORT=443 from .env, then replace the direct-public override with a loopback mapping:
services: plausible: ports: - "127.0.0.1:8000:8000"
A minimal Caddy site is:
analytics.example.com { reverse_proxy 127.0.0.1:8000 }
Then validate and recreate Plausible before configuring/reloading Caddy:
cd /opt/plausible-ce docker compose config -q docker compose up -d curl -I http://127.0.0.1:8000
For the full Caddy edge workflow, use How to Set Up a Caddy Reverse Proxy on Ubuntu 24.04.
Do not run the direct 80/443 Plausible mode and an external reverse proxy on the same public ports at the same time.
Verify: Choose exactly one public-edge model: either Plausible owns 80/443 with built-in TLS, or the app listens on 127.0.0.1:8000 and your external proxy owns the public web ports.
Step 12 — Back up PostgreSQL, ClickHouse, Plausible data, and configuration
Self-hosted Plausible stores different classes of state in multiple persistent volumes. PostgreSQL stores account/site configuration, ClickHouse stores analytics data, and the Plausible service has its own persistent application-data volume. A PostgreSQL dump alone is therefore not a complete Plausible backup.
Create a protected backup directory:
cd /opt/plausible-ce STAMP="$(date +%F-%H%M%S)" BACKUP_DIR="/opt/plausible-ce/backups/$STAMP" mkdir -p "$BACKUP_DIR" chmod 700 /opt/plausible-ce/backups "$BACKUP_DIR"
Create a logical PostgreSQL dump while the stack is running:
docker compose exec -T plausible_db \ pg_dump -U postgres plausible_db \ | gzip > "$BACKUP_DIR/plausible-postgres.sql.gz"
Capture the exact Docker volume names before stopping the services:
DB_VOL="$(docker inspect "$(docker compose ps -q plausible_db)" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}')" EVENT_VOL="$(docker inspect "$(docker compose ps -q plausible_events_db)" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/clickhouse"}}{{.Name}}{{end}}{{end}}')" EVENT_LOG_VOL="$(docker inspect "$(docker compose ps -q plausible_events_db)" --format '{{range .Mounts}}{{if eq .Destination "/var/log/clickhouse-server"}}{{.Name}}{{end}}{{end}}')" APP_VOL="$(docker inspect "$(docker compose ps -q plausible)" --format '{{range .Mounts}}{{if eq .Destination "/var/lib/plausible"}}{{.Name}}{{end}}{{end}}')"
Stop the stack for a consistent volume-level archive:
docker compose stop
Archive each named volume read-only:
docker run --rm -v "$DB_VOL:/source:ro" -v "$BACKUP_DIR:/backup" \ alpine:3 tar -czf /backup/db-data.tar.gz -C /source . docker run --rm -v "$EVENT_VOL:/source:ro" -v "$BACKUP_DIR:/backup" \ alpine:3 tar -czf /backup/event-data.tar.gz -C /source . docker run --rm -v "$EVENT_LOG_VOL:/source:ro" -v "$BACKUP_DIR:/backup" \ alpine:3 tar -czf /backup/event-logs.tar.gz -C /source . docker run --rm -v "$APP_VOL:/source:ro" -v "$BACKUP_DIR:/backup" \ alpine:3 tar -czf /backup/plausible-data.tar.gz -C /source .
Back up the deployment configuration and restart:
cp .env compose.override.yml "$BACKUP_DIR/" chmod 600 "$BACKUP_DIR/.env" docker compose start
Check the archives:
gzip -t "$BACKUP_DIR/plausible-postgres.sql.gz" ls -lh "$BACKUP_DIR"
Keep at least one verified copy away from the VM. Raff Data Protection can provide an additional infrastructure-level recovery layer, and Object Storage can be used as an off-server destination for backup archives when your backup workflow supports it.
Verify: The backup set should contain the PostgreSQL dump, archives for all persistent CE volumes, .env, and compose.override.yml; the stack should return to its previous running state after docker compose start.
Step 13 — Upgrade Plausible CE with an exact version and a rollback plan
Check the installed version first:
cd /opt/plausible-ce git describe --tags --exact-match docker compose images
Before upgrading:
- Create and verify a fresh backup.
- Read the Plausible CE release notes and version-specific upgrade instructions.
- Choose an exact target tag rather than following
latestblindly.
Fetch release tags:
git fetch --tags origin
When you have deliberately chosen a newer target, replace the placeholder below with that exact release:
TARGET=vX.Y.Z git fetch origin tag "$TARGET" git checkout "$TARGET" docker compose pull docker compose up -d
Then verify:
git describe --tags --exact-match docker compose ps docker compose logs --tail=100 plausible curl -I https://analytics.example.com
Plausible recommends version pinning, with patch-version pinning providing the most control. Major releases can require additional migration steps, so an old image tag is not automatically a safe rollback after database migrations. Follow the target release notes rather than assuming every upgrade is reversible by changing an image tag.
Verify: The intended exact release should be checked out, the databases should recover to healthy state, Plausible should run without migration loops, and the public HTTPS dashboard should still respond.
Step 14 — Run the final checks and clean up safely when needed
For the built-in HTTPS topology used by the main tutorial, run:
cd /opt/plausible-ce docker compose config -q git describe --tags --exact-match docker compose ps stat -c '%a %n' .env curl -I http://analytics.example.com curl -I https://analytics.example.com ls -lh backups/ 2>/dev/null || true
Also confirm in the browser that:
- the dashboard loads over trusted HTTPS;
- the administrator can sign in;
- the tracked site's current dashboard-generated snippet is installed;
- a real page visit appears in Plausible;
- no PostgreSQL or ClickHouse port is publicly exposed.
To stop Plausible without deleting persistent data:
docker compose down
A normal docker compose down keeps named volumes. To restart:
docker compose up -d
Do not use this destructive command unless you intentionally want to remove the databases and analytics history and have already verified your backups:
docker compose down -v
Remove /opt/plausible-ce only after you have decided that its configuration and local backup copies are no longer needed.
Verify: After a normal down, the Plausible named volumes should remain. After restarting, the existing account, site configuration, and analytics history should still be present.
Troubleshooting
Automatic HTTPS does not obtain a certificate
Check both DNS families and port ownership:
dig +short A analytics.example.com dig +short AAAA analytics.example.com sudo ss -lntp | grep -E ':(80|443)\b' || true cd /opt/plausible-ce docker compose logs --tail=150 plausible
The hostname must resolve to the server and ports 80/443 must reach the Plausible container. Remove stale IPv6 DNS if the server is not actually reachable over that IPv6 address.
Plausible or a database repeatedly restarts
Check the full state and service-specific logs:
cd /opt/plausible-ce docker compose ps docker compose logs --tail=150 plausible docker compose logs --tail=150 plausible_db docker compose logs --tail=150 plausible_events_db
Also confirm the VM has at least the 2 GB RAM recommended by the current CE quickstart:
free -h docker stats --no-stream
Treat resource output as a measurement of your instance, not as a universal pageview-capacity estimate.
The dashboard loads but tracking data does not arrive
Use the current snippet copied from the site's installation settings. Then check the website's browser Network panel for the tracker script and event request.
Common causes include:
- an old or manually edited tracking snippet;
- a restrictive Content Security Policy;
- browser privacy/ad-blocking extensions;
- the wrong site domain in Plausible;
- a wrong
BASE_URLafter moving the analytics instance.
Do not start by editing ClickHouse tables or fabricating events against an assumed internal schema.
The external reverse proxy returns 502
If you intentionally selected the optional proxy topology, verify the loopback upstream first:
curl -I http://127.0.0.1:8000 sudo ss -lntp | grep ':8000'
The override should map 127.0.0.1:8000:8000, and .env should contain HTTP_PORT=8000 without the direct HTTPS_PORT=443 setup.
WebSocket features fail behind Nginx
Plausible's current community Nginx example includes an explicit /live/websocket WebSocket location with HTTP/1.1 upgrade headers. Do not describe the live connection as ordinary buffered HTTP or assume a generic proxy block handles every Nginx WebSocket case. Caddy's reverse_proxy handles WebSocket upgrades automatically.
An upgrade fails after migrations start
Stop making repeated image/tag changes. Read the release-specific upgrade notes, preserve the failing state and logs, and restore from your pre-upgrade backup when the documented recovery path requires it. Database migrations can make a simple image downgrade unsafe.
Conclusion
You now have Plausible Community Edition running on Ubuntu 24.04 with its current Docker Compose layout, a pinned CE release, protected secrets, built-in automatic HTTPS, current site-specific tracking, persistent PostgreSQL and ClickHouse data, a complete backup plan, and a controlled upgrade path.
Plausible is designed for privacy-focused, cookieless aggregate web analytics, but your site's legal obligations depend on your jurisdiction, configuration, and any other trackers or personal-data processing you use. Self-hosting also moves the operational responsibility to you: keep CE updated, monitor storage and memory, protect the server, and test restores rather than assuming a VM snapshot or PostgreSQL dump alone covers the entire analytics stack.
For related Docker setup, optional Caddy routing, and off-server backup workflows, use the tutorials and product references linked in their relevant steps above.