How to deploy & self-host Plausible on DigitalOcean

How to deploy & self-host Plausible on DigitalOcean

We may earn an affiliate commission through purchases made from our guides and tutorials.

Here’s a straight-shooting, practical guide to deploying and self-hosting Plausible Analytics on DigitalOcean. I’ll walk you from a clean Droplet to a production-ready instance, and I’ll flag the “why” along the way so the choices make sense later when you have to maintain it.

If you’re skimming: yes, you’ll need a DigitalOcean account and a domain pointed at your server. We’ll use Docker and Docker Compose, because Plausible’s official self-host setup is built around containers.

What you’re installing (and why)

Plausible is a lightweight, privacy-first web analytics app written in Elixir. In the self-hosted build, it uses PostgreSQL for application data and ClickHouse for events—fast and efficient. The official Plausible Community Edition (CE) repo ships a Docker Compose setup (with Caddy for HTTPS) that’s the cleanest way to run it yourself. Minimum you want is a CPU with SSE 4.2/NEON and ~2GB RAM so ClickHouse doesn’t starve.

If you’d rather press the easy button, DigitalOcean also offers a Marketplace image (“1-Click App”) for Plausible—but this guide shows the manual path so you actually understand what’s running.

What you need up front

  • A Droplet (Ubuntu 22.04/24.04 LTS is fine). A basic plan with 2GB RAM is a sensible starting point because of ClickHouse.
  • A domain or subdomain (e.g., analytics.example.com) with an A record pointing to your Droplet’s public IP. We’ll use this for TLS and the app’s base URL.
  • Docker + Docker Compose on the server. (We’ll install in a second.)
  • Optional: a mail relay (SMTP) for sign-in emails; you can add this later.

Create your Droplet and SSH in

Spin up an Ubuntu LTS Droplet in your preferred region, then SSH to it as a sudo-capable user.

ssh root@YOUR_DROPLET_IP
adduser deployer
usermod -aG sudo deployer
su - deployer

If you like things tidy, enable a basic firewall (allow SSH, HTTP, HTTPS):

sudo ufw allow OpenSSH
sudo ufw allow http
sudo ufw allow https
sudo ufw enable

Install Docker and Docker Compose

DigitalOcean has a one-click Docker image, but let’s install directly so you know what’s on the box.

sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo 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
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu $(. /etc/os-release; echo $VERSION_CODENAME) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker

Test:

docker version
docker compose version

Clone the official Plausible CE stack

The Plausible team maintains a canonical community-edition repo with Compose files and a sane default Caddy reverse proxy. We’ll pin to the current tag (example below shows v3.0.1—use the latest tag you see in the repo).

git clone -b v3.0.1 --single-branch https://github.com/plausible/community-edition plausible-ce
cd plausible-ce
ls -1

You should see compose.yml, README.md, and a clickhouse/ directory.

Set your environment

Plausible needs a BASE_URL and a strong SECRET_KEY_BASE at minimum. The official guide uses a simple .env right next to compose.yml.

touch .env
echo "BASE_URL=https://analytics.example.com" >> .env
echo "SECRET_KEY_BASE=$(openssl rand -base64 48)" >> .env

If you want Plausible and Caddy to directly terminate TLS on ports 80/443 (no extra Nginx layer), add these:

echo "HTTP_PORT=80"  >> .env
echo "HTTPS_PORT=443" >> .env

That instructs the bundled Caddy to request and manage Let’s Encrypt certs automatically—as long as your DNS is already pointing at the Droplet.

Prefer Nginx or already have Traefik/Caddy elsewhere? You can omit the port mapping here and put Plausible behind your existing reverse proxy using the project’s “Reverse Proxy” wiki notes.

(Option A) Let Caddy handle TLS for you

This is the fastest path to a live site with HTTPS:

cat > compose.override.yml << 'YAML'
services:
  plausible:
    ports:
      - 80:80
      - 443:443
YAML

docker compose up -d

After containers start, hit https://analytics.example.com and create the first user. That’s your admin. The compose setup wires up ClickHouse and Postgres for you under the hood.

(Option B) Put Plausible behind Nginx on the host

If you prefer the “classic” approach—Docker only on localhost, and Nginx on the host doing TLS—bind the app to 127.0.0.1:8000 and use Certbot for certificates.

First, make Plausible listen only on localhost:

# In the plausible-ce directory:
sed -n '1,200p' compose.yml | sed 's/8000:8000/127.0.0.1:8000:8000/' > /tmp/compose.yml
mv /tmp/compose.yml compose.yml
docker compose up -d

Install and configure Nginx:

sudo apt update
sudo apt install -y nginx
sudo tee /etc/nginx/sites-available/plausible.conf >/dev/null <<'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name analytics.example.com;

    access_log /var/log/nginx/plausible.access.log;
    error_log  /var/log/nginx/plausible.error.log;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
    }
}
NGINX
sudo ln -s /etc/nginx/sites-available/plausible.conf /etc/nginx/sites-enabled/plausible.conf
sudo nginx -t && sudo systemctl reload nginx

Get a Let’s Encrypt cert and auto-HTTPS redirect:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d analytics.example.com

Log in, create your site, add the script

Visit your domain, sign in as the admin you just created, and add your first site in the UI. Then paste the tracking script it gives you into your site’s <head>.

If you just want the bare script reference for later, it looks like this:

<script defer data-domain="your-site.example" src="https://analytics.example.com/js/script.js"></script>

Nginx: serve Plausible from your main site

You’ll proxy two paths from your main site (example.com) to your Plausible host (analytics.example.com): the script and the event endpoint. This dodges some ad blockers and keeps things tidy under one origin.

Put this inside your existing server { … } block for example.com (not the Plausible vhost). Adjust hostnames if yours differ.

# --- Plausible proxy (from main site -> plausible host) ---
# Script file
location = /js/script.js {
    proxy_pass https://analytics.example.com/js/script.js;
    proxy_http_version 1.1;
    proxy_set_header Host analytics.example.com;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Optional: small cache so the script isn't fetched on every hit
    proxy_buffering on;
    proxy_cache_valid 200 1h;
    expires 1h;
}

# Events endpoint
location = /api/event {
    proxy_pass https://analytics.example.com/api/event;
    proxy_http_version 1.1;
    proxy_set_header Host analytics.example.com;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # Keep responses small/fast
    proxy_buffering off;
}

# If you also use the new "hash" script, proxy it too:
# location = /js/script.hash.js { proxy_pass https://analytics.example.com/js/script.hash.js; ... }

After you add that, reload Nginx and update your site’s HTML to use the local paths:

sudo nginx -t && sudo systemctl reload nginx

Then swap your embed to:

<script defer data-domain="your-site.example" src="/js/script.js"></script>

Nothing else on Plausible changes; it still lives at analytics.example.com, but your visitors only talk to your main site.

SMTP (email) so sign-in/verification works

By default, Plausible can try to send email directly from the server—often unreliable. Configure SMTP in your env so verification and invites don’t land in the void. In the CE wiki’s Configuration page you’ll find the available variables (e.g., MAILER_ADAPTER, SMTP_HOST_ADDR, etc.). Add them into .env and restart.

# Example (adjust to your provider)
cat >> .env << 'ENV'
MAILER_ADAPTER=Swoosh.SMTP
SMTP_HOST_ADDR=smtp.mailgun.org
SMTP_HOST_PORT=587
SMTP_USER_NAME=postmaster@mg.example.com
SMTP_USER_PWD=XXXXXXXX
SMTP_RETRIES=2
ENV

docker compose up -d

Upgrades (don’t skip this)

When Plausible releases a new CE tag, you’ll want to move up cleanly. The project’s Upgrade wiki explains the process and any Postgres major-version cautions. General flow:

cd ~/plausible-ce
git fetch --tags
git checkout vX.Y.Z   # the new release tag
docker compose pull
docker compose up -d
docker image prune -f

If a Postgres major upgrade is included, follow the repo’s Upgrade PostgreSQL notes before starting containers. This is one of those “measure twice, cut once” situations.

Backups you won’t regret

You have two stateful stores: Postgres and ClickHouse. Keep them safe.

A simple, service-aware approach (run as root or a user in docker group):

# Postgres dump
docker exec -t plausible-db pg_dump -U postgres -F c -b -v -f /var/lib/postgresql/data/backup_$(date +%F).dump plausible

# ClickHouse backup (creates a backup on disk)
docker exec -t plausible-events-db clickhouse-client --query="BACKUP DATABASE plausible TO Disk('backups', 'plausible_backup_$(date +%F)')"

Then copy those artifacts off-box (rsync, rclone, or DO Spaces). For belt-and-suspenders safety, take DigitalOcean Droplet snapshots regularly. (Backups aren’t glamorous, but they’re what let you sleep.)

Backups: Postgres + ClickHouse with a systemd timer

This is a pragmatic backup that:

  • dumps Postgres,
  • triggers a ClickHouse backup to its internal “backups” disk,
  • packages both, rotates locally,
  • and (optionally) syncs to an off-box remote (commented out—uncomment when ready).

Create the script:

sudo tee /usr/local/bin/backup-plausible.sh >/dev/null <<'BASH'

#!/usr/bin/env bash
set -euo pipefail

# --- settings ---
BACKUP_ROOT="/var/backups/plausible"
TIMESTAMP="$(date +%F_%H%M%S)"
WORKDIR="${BACKUP_ROOT}/${TIMESTAMP}"
RETENTION_DAYS=7

# Docker service names as in compose.yml
PG_CONTAINER="plausible-db"
CH_CONTAINER="plausible-events-db"
PG_DB="plausible"
PG_USER="postgres"

mkdir -p "${WORKDIR}"

echo "[*] Postgres dump..."
docker exec -t "${PG_CONTAINER}" pg_dump -U "${PG_USER}" -F c -b -v -f "/tmp/${PG_DB}_${TIMESTAMP}.dump" "${PG_DB}"
docker cp "${PG_CONTAINER}:/tmp/${PG_DB}_${TIMESTAMP}.dump" "${WORKDIR}/${PG_DB}.dump"
docker exec -t "${PG_CONTAINER}" rm -f "/tmp/${PG_DB}_${TIMESTAMP}.dump"

echo "[*] ClickHouse backup..."
# Creates a named backup inside ClickHouse's 'backups' disk (as configured by the CE stack)
CH_BK_NAME="plausible_backup_${TIMESTAMP}"
docker exec -t "${CH_CONTAINER}" clickhouse-client --query="BACKUP DATABASE plausible TO Disk('backups','${CH_BK_NAME}')"

# Pull ClickHouse backup directory out to the host
docker exec -t "${CH_CONTAINER}" bash -lc "tar -C /var/lib/clickhouse/backups -czf /tmp/${CH_BK_NAME}.tar.gz ${CH_BK_NAME}"
docker cp "${CH_CONTAINER}:/tmp/${CH_BK_NAME}.tar.gz" "${WORKDIR}/clickhouse_${TIMESTAMP}.tar.gz"
docker exec -t "${CH_CONTAINER}" rm -f "/tmp/${CH_BK_NAME}.tar.gz"

echo "[*] Package both into a single archive..."
tar -C "${WORKDIR}" -czf "${BACKUP_ROOT}/plausible_${TIMESTAMP}.tar.gz" .
rm -rf "${WORKDIR}"

echo "[*] Rotate old backups (> ${RETENTION_DAYS} days)..."
find "${BACKUP_ROOT}" -type f -name "plausible_*.tar.gz" -mtime +${RETENTION_DAYS} -delete

# --- Optional offsite sync (example with rclone) ---
# rclone copy "${BACKUP_ROOT}/plausible_${TIMESTAMP}.tar.gz" remote:plausible-backups

echo "[*] Done: ${BACKUP_ROOT}/plausible_${TIMESTAMP}.tar.gz"
BASH

sudo chmod +x /usr/local/bin/backup-plausible.sh
sudo mkdir -p /var/backups/plausible

Add a systemd unit to run the script:

sudo tee /etc/systemd/system/backup-plausible.service >/dev/null <<'UNIT'
[Unit]
Description=Plausible backup (Postgres + ClickHouse)
Wants=network-online.target docker.service
After=network-online.target docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-plausible.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
UNIT

And a timer to run it nightly at 03:27 (odd times avoid cron stampedes):

sudo tee /etc/systemd/system/backup-plausible.timer >/dev/null <<'TIMER'
[Unit]
Description=Run Plausible backup nightly

[Timer]
OnCalendar=*-*-* 03:27
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
TIMER

sudo systemctl daemon-reload
sudo systemctl enable --now backup-plausible.timer

Quick smoke test:

sudo systemctl start backup-plausible.service
journalctl -u backup-plausible.service -n 100 --no-pager
ls -lh /var/backups/plausible/

If you plan to ship backups off the box (you should), wire up rclone config once, then uncomment that line in the script and re-run. DigitalOcean Spaces, S3, Backblaze—your call. The point is to have one local copy and one remote copy at all times.

Monitoring & housekeeping

  • Health checks/logs: docker compose ps, docker compose logs -f plausible tell you most of what you need.
  • Disk: ClickHouse is efficient but can grow. Monitor with docker system df and du -sh under your Docker volumes directory.
  • TLS renewals: If you used the built-in Caddy path, certs auto-renew. If you used Nginx+Certbot, it installs a systemd timer/cron—verify with sudo systemctl status certbot.timer.

Sanity checks & common fixes

  • Can’t get a certificate? Your BASE_URL must match your DNS record, and ports 80/443 must be open to the Droplet. If using Caddy (Option A), make sure those ports are mapped and not already taken by another service.
  • Emails not arriving? Use a real SMTP relay and set the mailer vars; don’t rely on raw server-sent mail in 2025 unless you enjoy SPF/DKIM pain.
  • High memory usage? Upgrade the Droplet to 2GB+ RAM. ClickHouse likes headroom.

Quick paths if you’re in a hurry

  • Small personal sites: Option A (Caddy inside Compose) is the fastest: fewer moving parts, auto-TLS.
  • Existing Nginx estates: Option B keeps your reverse proxy central and predictable, which ops teams appreciate.
  • Zero-to-analytics: DigitalOcean Marketplace “Plausible Analytics” spins up an opinionated setup for you. It’s fine. You can still tweak later.

Final word

Keep it simple, keep it documented, and don’t skip backups. Plausible CE’s own repo is your source of truth for config flags, reverse-proxy notes, and upgrades; DigitalOcean’s tutorial is a solid companion for the Nginx+Certbot route. Between those two, you’ll have a setup that works today and won’t surprise you six months from now.

Was this helpful?

Thanks for your feedback!
Alex is the resident editor and oversees all of the guides published. His past work and experience include Colorlib, Stack Diary, Hostvix, and working with a number of editorial publications. He has been wrangling code and publishing his findings about it since the early 2000s.

Leave a comment

Your email address will not be published. Required fields are marked *