Self-Hosting Plausible Analytics with Podman and Cloudflare Tunnels

May 23, 2026·
Derek Armstrong - Staff Software Engineer and Solutions Architect
Derek Armstrong
· 5 min read

I needed website analytics for my blog. Not Google Analytics — I don’t want user data leaving my control, I don’t want the ads ecosystem, and I don’t need per-user behavioral tracking for a personal site. I needed something that tells me which posts show up in search and which ones I should stop writing. Plausible fills that gap.

The open-source Community Edition is free, runs on commodity hardware, and has been supported by the Plausible team for years. The stack runs PostgreSQL, ClickHouse, and the application. Not hard, but enough infrastructure that it’s worth writing down how I set it up, what broke, and how I make sure I don’t lose data.

The Stack

Three containers, two databases, one app:

  • PostgreSQL 16 — application metadata: sites, users, event configuration, settings
  • ClickHouse 24.12 — time-series event data: page views, referrers, exit pages, browser data
  • Plausible App v3.2.1 — the web UI and tracking endpoint that serves the script tag

The Plausible team provides a docker-compose file you can copy. The modifications for podman were minimal.

Podman Quirks on Rocky Linux

It’s supposed to be a drop-in replacement for docker-compose. Same YAML, same commands, just swap the container engine. The issue is rootless pods.

Rocky Linux’s podman doesn’t handle rootless pods well. I hit two issues:

  1. Pod lifecycle crashes — pods would create successfully, but the containers inside would crash immediately with SIGTERM
  2. Network mode host conflictsnetwork_mode: host caused port binding failures because the containers competed for the same ports

The solution for the pod issue is podman-compose up --in-pod=false, which tells podman to run the containers without grouping them into a pod. Each container runs independently, shares the network via a standard user-defined bridge. Same networking, no pod bugs.

The solution for the network issue is a named bridge network defined in the compose file, with explicit port mappings. No host mode.

The Setup

The deployment lives in a subdirectory on prodweb. The compose file defines three services, four named volumes for each database, and one bridge network. The app container publishes port 8000, which Cloudflare’s tunnel connects to.

The environment file handles the sensitive bits: SECRET_KEY_BASE, BASE_URL, and DISABLE_REGISTRATION. The secret key is generated on first deploy with openssl rand -base64 48. Registration is disabled after the initial admin account is created.

That’s it. No secrets management, no TLS certificates to maintain, no nginx or traefik layer. Cloudflare’s tunnel handles HTTPS termination and routing. I prefer the dashboard for the tunnel config.

Integration with Hugo

The Hugo site uses a theme that supports Plausible out of the box — the Blox theme has a built-in Plausible integration that lets you set the domain and it loads from your self-hosted instance. I changed the script source to point to my Plausible instance and updated the CSP content security policy to allow the tracking script to load from analytics.derekarmstrong.dev.

The tracking script is minimal:

<script async defer data-domain="derekarmstrong.dev" src="https://analytics.derekarmstrong.dev/js/script.js"></script>

That goes in the head, and that’s it. Plausible handles the rest. No cookies, no GDPR notices, no dark pattern consent banners. It’s just page views.

Privacy First

The DISABLE_REGISTRATION=true flag is non-negotiable. Access is restricted to anyone with an account and a valid session token. Plausible scrubs IP addresses by default, storing only a cryptographic hash. The only identifiable data is the domain name and referrer. No user-level tracking, no behavioral analytics. Just aggregate counts.

Backing Up

This is the part most guides skip. Boring but important. There’s two databases, five podman volumes, one compose file, and one .env file. All of it matters. As long as the PostgreSQL and ClickHouse data stays consistent, restoring is straightforward: export the volumes, copy the files, restore on a new server, start the containers, done.

The backup.sh script handles this:

  1. Stops the containers (so the database writes are durable)
  2. Exports each podman volume to a tar archive
  3. Copies .env and compose.yml to the backup
  4. Streams the backup off the host to a local machine
  5. Restarts the containers

The restore.sh script reverses the process: extract the backup, import the volumes, copy the config, start the containers.

Both scripts are idempotent. The backup completes in under 2 minutes for my site. If you’re running a high-traffic site with large event tables, you might want to adjust the STALE_DAYS variable to keep more recent backups around.

Running It

The workflow is:

# Fresh deploy
./deploy.sh

# Backup to local machine
./backup.sh

# Restore from backup
./restore.sh backup-20260523.tar.gz

That’s it. Deploy once, back up regularly, restore when needed.

What I Learned

The Podman rootless pod bugs on Rocky Linux were a real blocker. The ClickHouse config volume mounts don’t work with rootless podman — the workaround is to not mount external configs for ClickHouse at all. Less customization, but it works.

The .env file is the single point of failure for the deployment. If you lose the .env, you lose the installation. The volume data exists on prodweb, but you can’t authenticate to the UI without the config. Every deploy script I’ve written since then backs up .env explicitly. Back it up separately from the volume data.

Plausible itself is solid. The UI is clean, the data is useful, and the privacy-first approach feels right for a personal blog. I’ve been running it for months and it hasn’t given me any trouble.

Next


This is a personal blog post about my homelab and self-hosted infrastructure. It reflects what I’ve actually deployed and what has worked in production.

Derek Armstrong - Staff Software Engineer and Solutions Architect
Authors
Staff Software Engineer | Solutions Architect
Staff Software Engineer, AI Systems Engineer, and Solutions Architect with 10+ years of experience designing and shipping production systems at enterprise scale. I lead teams building payment platforms processing billions in annual volume, architect cloud-native infrastructure, and integrate AI/ML capabilities into mission-critical systems. Passionate about turning complex technical challenges into reliable, scalable solutions — and about mentoring the engineers who will carry that work forward.