Introduction
To self-host Plausible Analytics on Ubuntu 24.04, clone the Plausible Community Edition repository, configure the environment file with a domain and generated secret key, bring the Docker Compose stack up, and place a reverse proxy in front of it for HTTPS. Once running, you have a fully private analytics dashboard — no cookies, no third-party data sharing, no pageview limits — served from your own Raff VM. The installer completes in under 35 minutes on a fresh Tier 2 VM.
The reason we see teams migrate to self-hosted Plausible is almost always the same: they hit Plausible Cloud's pageview tier and the math stops working, or they operate in a regulated environment where sending user data to a third party — even a privacy-respecting one — creates compliance friction. Self-hosting solves both. You own the ClickHouse database that stores your analytics, the data never leaves your infrastructure, and there are no per-pageview costs. We run self-hosted Plausible on a Raff Tier 2 VM for several of our own domains — at steady state, the full stack consumes about 600 MB of RAM across the Plausible app, ClickHouse, and PostgreSQL containers, leaving comfortable headroom on a 2 GB VM for sites up to several million monthly pageviews.
The only honest caveat: Plausible Community Edition (the self-hosted version) lags behind Plausible Cloud by a release cycle and does not include email reports or some funnel features available on the paid cloud. For most teams, the analytics core — pageviews, sources, devices, countries, custom events — is identical. If you specifically need email digests or advanced funnels, factor that in before deciding to self-host.
In this tutorial, you will install Docker if not already present, clone and configure the Plausible CE stack, generate required secrets, bring the services up, configure Nginx with SSL for HTTPS access, and verify the full stack is accepting tracking data.
Step 1 — Install Docker and Docker Compose
If Docker is not yet installed on your VM, set it up from the official Docker APT repository. The docker.io package in Ubuntu's default repositories is outdated — use the Docker-maintained source.
bashsudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg
Add the Docker signing key and repository:
bashsudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install Docker Engine and the Compose plugin:
bashsudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Add your user to the docker group:
bashsudo usermod -aG docker $USER
newgrp docker
Verify both are working:
bashdocker --version
docker compose version
Expected output:
Docker version 27.x.x, build xxxxxxx
Docker Compose version v2.x.x
If Docker is already installed from a previous tutorial on this VM, skip to Step 2.
Step 2 — Clone the Plausible CE Repository
Plausible provides an official self-hosting repository called community-edition. Clone it into your web directory:
bashsudo mkdir -p /var/www/plausible
sudo chown $USER:$USER /var/www/plausible
cd /var/www/plausible
git clone https://github.com/plausible/community-edition.git .
List the contents to understand the structure:
bashls -la
Expected output includes:
docker-compose.yml # The stack definition
plausible-conf.env # Your configuration file — edit this
reverse-proxy/ # Optional Caddy reverse proxy config (we will use Nginx instead)
Note
The reverse-proxy/ directory contains a Caddy-based reverse proxy configuration that Plausible ships as an optional convenience setup. This tutorial uses Nginx instead for consistency with the rest of the Raff cluster. The Plausible application stack is identical either way — only the proxy layer differs.
Step 3 — Generate a Secret Key
Plausible requires a SECRET_KEY_BASE — a cryptographically random string used to sign sessions and tokens. Generate one now:
bashopenssl rand -base64 64 | tr -d '\n'
Copy the full output. It will look like:
K4oB7zXqP2mNvRwL9jYdGhEsFcUiT0aO3nWpMbAeVlCkJsHyDgZfQuI1rX8tB2==
You will paste this into the configuration file in the next step. Do not use a shorter string — Plausible validates the length and will refuse to start with an insufficient secret.
Step 4 — Configure the Environment File
Open the Plausible configuration file:
bashnano /var/www/plausible/plausible-conf.env
The file contains a small set of required variables. Work through each one:
Base URL — the public HTTPS address where your Plausible instance will be reached:
bashBASE_URL=https://plausible.your-domain.com
Replace plausible.your-domain.com with your actual subdomain. This value is embedded in invitation emails, shared dashboard links, and the tracking script URL that you embed on your websites. It must be the final production domain — changing it later requires updating every tracking script embed across all your sites.
Secret key:
bashSECRET_KEY_BASE=<paste-your-generated-secret-here>
Database configuration — leave these at their defaults. The Docker Compose file handles the internal networking between Plausible, PostgreSQL, and ClickHouse:
bashDATABASE_URL=postgres://postgres:postgres@plausible_db:5432/plausible_db
CLICKHOUSE_DATABASE_URL=http://plausible_events_db:8123/plausible_events_db
SMTP configuration — required for user registration, password resets, and optional email reports. If you are running a personal instance with a single admin account, you can skip SMTP for now, but any multi-user setup or invitation flow requires it:
bashMAILER_EMAIL=analytics@your-domain.com
SMTP_HOST_ADDR=smtp.your-provider.com
SMTP_HOST_PORT=587
SMTP_USER_NAME=your-smtp-username
SMTP_USER_PWD=your-smtp-password
SMTP_HOST_SSL_ENABLED=false
Disable registration — for a private instance, prevent anyone from registering a new account after your own:
bashDISABLE_REGISTRATION=invite_only
This setting means only users you explicitly invite via the dashboard can create accounts. Set it to true to block all new registrations including invites, or false to allow open registration. invite_only is the right default for a team instance.
Save and exit with Ctrl+O, then Ctrl+X.
Verify no placeholder values remain in the file:
bashgrep -E "changeme|example\.com|replace" /var/www/plausible/plausible-conf.env
This should return no output. If it returns any lines, those values still need replacing.
Step 5 — Start the Plausible Stack
Pull all Docker images before starting:
bashdocker compose pull
This pulls three images: plausible/analytics (the main app), postgres (user and site data), and clickhouse/clickhouse-server (analytics event storage). On a Tier 2 VM with a fresh Docker installation, expect 3–5 minutes for the pulls to complete.
Start the stack:
bashdocker compose up -d
Monitor the startup sequence:
bashdocker compose logs -f --tail=50
Watch for the Plausible app to finish its database migrations and declare itself ready. The initialization order is: plausible_db (PostgreSQL) → plausible_events_db (ClickHouse) → plausible (the main app). ClickHouse takes the longest on first boot — typically 30–60 seconds as it initializes its data directories and schema.
You will see the app is ready when the logs show:
plausible | [info] Running PlausibleWeb.Endpoint with cowboy 2.x.x
plausible | [info] Access PlausibleWeb.Endpoint at https://plausible.your-domain.com
Press Ctrl+C to stop following logs, then verify all containers are healthy:
bashdocker compose ps
Expected output:
NAME STATUS PORTS
plausible healthy 0.0.0.0:8000->8000/tcp
plausible_db healthy 5432/tcp
plausible_events_db healthy 8123/tcp, 9000/tcp
All three containers must show healthy. If plausible shows starting for more than two minutes, check its logs:
bashdocker compose logs plausible --tail=40
The most common first-boot failure is a SECRET_KEY_BASE that is too short or contains characters that were mangled by the editor. If you see a secret-related error, regenerate the key and update the env file.
Check resource usage at idle:
bashdocker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
On a Raff Tier 2 VM with no active traffic, expect approximately:
NAME CPU % MEM USAGE / LIMIT
plausible 0.3% 120MiB / 1.9GiB
plausible_db 0.1% 45MiB / 1.9GiB
plausible_events_db 0.5% 430MiB / 1.9GiB
ClickHouse is the memory floor of this stack — it claims around 400–450 MB at idle. This is expected and stable. Total idle consumption is approximately 600 MB, leaving roughly 1.3 GB headroom on a Tier 2 VM for analytics traffic processing.
Step 6 — Install Nginx and Configure the Reverse Proxy
Plausible runs on port 8000 internally. Nginx will terminate TLS and proxy traffic to it.
Install Nginx:
bashsudo apt install -y nginx
Create a DNS A record pointing plausible.your-domain.com to your Raff VM's public IP before proceeding. Certbot needs the DNS entry to exist and propagate for the ACME challenge to succeed.
Create the Nginx server block:
bashsudo nano /etc/nginx/sites-available/plausible
Paste the following, replacing plausible.your-domain.com with your actual domain:
nginxserver {
listen 80;
server_name plausible.your-domain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Required for Plausible's live visitor dashboard (Server-Sent Events)
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
}
}
The proxy_buffering off and proxy_cache off directives are important for Plausible specifically. The live visitor count on the dashboard uses Server-Sent Events (SSE) — a long-lived HTTP connection that streams updates. Nginx's default response buffering interrupts SSE streams, which causes the live dashboard to appear frozen or disconnected. Disabling buffering on the proxy keeps the SSE connection clean.
Enable the site and test:
bashsudo ln -s /etc/nginx/sites-available/plausible /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Reload Nginx:
bashsudo systemctl reload nginx
Step 7 — Obtain an SSL Certificate with Certbot
Install Certbot and obtain a certificate:
bashsudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d plausible.your-domain.com
Certbot modifies the Nginx configuration and adds HTTPS with automatic HTTP-to-HTTPS redirection. After completion, verify:
bashcurl -I https://plausible.your-domain.com
Expected output:
HTTP/2 200
server: nginx
Open https://plausible.your-domain.com in a browser. You should land on the Plausible registration screen. Create your admin account — this is the only account on a fresh instance. Use a strong password; there is no 2FA on the self-hosted version, so the account password is your primary protection.
After logging in, you will see the Plausible dashboard with an empty state. The next step adds a site and verifies the tracking script delivers data.
Tip
After confirming HTTPS is working through Nginx, close port 8000 in your Raff cloud firewall. The Plausible app port should never be directly accessible from the internet — all traffic should flow through Nginx on 443.
Step 8 — Add a Site and Verify Tracking
In the Plausible dashboard, click + Add Website and enter your site's domain (e.g., yoursite.com). Plausible generates a tracking script snippet:
html<script defer data-domain="yoursite.com" src="https://plausible.your-domain.com/js/script.js"></script>
Add this snippet to the <head> of your website. The script is served from your own Plausible instance — not from Plausible's CDN. This is a key privacy advantage: the script request never touches Plausible's infrastructure.
Verify the tracking script is accessible:
bashcurl -I https://plausible.your-domain.com/js/script.js
Expected output:
HTTP/2 200
content-type: application/javascript; charset=utf-8
Send a test pageview to confirm the ingestion pipeline is working end to end:
bashcurl -X POST https://plausible.your-domain.com/api/event \
-H "User-Agent: Mozilla/5.0 (compatible; test)" \
-H "X-Forwarded-For: 1.2.3.4" \
-H "Content-Type: application/json" \
-d '{"name":"pageview","url":"https://yoursite.com/","domain":"yoursite.com"}'
Expected response:
{"status":"ok"}
Wait 10–15 seconds, then check the Realtime view in your Plausible dashboard. You should see 1 current visitor — the test event you just sent. This confirms the full pipeline is operational: Nginx → Plausible app → ClickHouse event storage → dashboard query.
Verify ClickHouse is persisting events:
bashdocker exec plausible_events_db clickhouse-client \
--query "SELECT count() FROM plausible_events_db.events_v2"
Expected output:
1
The event count matches what we sent. ClickHouse is writing and reading correctly.
Configure Docker to start on boot:
bashsudo systemctl enable docker
Verify the Compose stack restarts automatically after a reboot:
bashsudo reboot
After reconnecting, give the stack about 60 seconds to initialize, then check:
bashcd /var/www/plausible && docker compose ps
All three containers should be back in healthy state without any manual intervention. The restart: unless-stopped policy in the Compose file handles automatic restarts — it is set by default in the Plausible CE repository.
Conclusion
You now have a self-hosted Plausible Analytics instance running on your Raff VM: ClickHouse storing events, PostgreSQL managing users and site configuration, and the Plausible application serving the dashboard and ingestion API over HTTPS through Nginx. Analytics data lives entirely on your infrastructure — no third-party access, no pageview caps, no consent banner requirement.
A few things to handle before considering this production-ready:
- SMTP: If you skipped SMTP configuration in Step 4, go back and add it before inviting team members. Without SMTP, invitation emails and password resets will silently fail. Resend and Postmark both work reliably with Plausible's mailer configuration.
- Backups: Plausible's PostgreSQL data (users, sites, goals) lives in a Docker volume. Schedule daily dumps with
docker exec plausible_db pg_dump -U postgres plausible_db | gzip > /backups/plausible-$(date +%Y-%m-%d).sql.gzand enable Raff VM snapshots from the control panel as a full-system safety net. ClickHouse event data is harder to dump at scale — for most teams, VM snapshots are the practical backup mechanism for the events database. - Upgrades: Check the Plausible CE releases page periodically. Upgrading is:
git pull && docker compose pull && docker compose up -d. Plausible runs database migrations automatically on startup — no manual migration steps are needed for standard upgrades. Always read the release notes before upgrading; breaking changes are rare but documented when they occur.
The economics of this setup are straightforward. Plausible Cloud charges $9/month for 10,000 pageviews, scaling to $19/month for 100,000 and $69/month for 1 million. A Raff Tier 2 VM at $9.99/month handles well over a million monthly pageviews in self-hosted mode. For sites with meaningful traffic, self-hosting pays for itself within the first month. For sites that are growing toward those tiers, starting self-hosted means the cost curve stays flat regardless of growth.
For related self-hosting content in this cluster, the Coolify tutorial and Supabase guide cover deploying the application layer and backend that your tracked sites are likely running on — a natural pairing if you are building the full self-hosted stack on Raff.
