<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Privacy | Derek Armstrong — Staff Engineer &amp; Solutions Architect</title><link>https://derekarmstrong.dev/tags/privacy/</link><atom:link href="https://derekarmstrong.dev/tags/privacy/index.xml" rel="self" type="application/rss+xml"/><description>Privacy</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Sat, 23 May 2026 00:00:00 +0000</lastBuildDate><image><url>https://derekarmstrong.dev/media/sharing.png</url><title>Privacy</title><link>https://derekarmstrong.dev/tags/privacy/</link></image><item><title>Self-Hosting Plausible Analytics with Podman and Cloudflare Tunnels</title><link>https://derekarmstrong.dev/blog/self-hosting-plausible-analytics/</link><pubDate>Sat, 23 May 2026 00:00:00 +0000</pubDate><guid>https://derekarmstrong.dev/blog/self-hosting-plausible-analytics/</guid><description>&lt;p&gt;I needed website analytics for my blog. Not Google Analytics — I don&amp;rsquo;t want user data leaving my control, I don&amp;rsquo;t want the ads ecosystem, and I don&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;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&amp;rsquo;s worth writing down how I set it up, what broke, and how I make sure I don&amp;rsquo;t lose data.&lt;/p&gt;
&lt;h2 id="the-stack"&gt;The Stack&lt;/h2&gt;
&lt;p&gt;Three containers, two databases, one app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL 16&lt;/strong&gt; — application metadata: sites, users, event configuration, settings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ClickHouse 24.12&lt;/strong&gt; — time-series event data: page views, referrers, exit pages, browser data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plausible App v3.2.1&lt;/strong&gt; — the web UI and tracking endpoint that serves the script tag&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Plausible team provides a docker-compose file you can copy. The modifications for podman were minimal.&lt;/p&gt;
&lt;h2 id="podman-quirks-on-rocky-linux"&gt;Podman Quirks on Rocky Linux&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;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.&lt;/p&gt;
&lt;p&gt;Rocky Linux&amp;rsquo;s podman doesn&amp;rsquo;t handle rootless pods well. I hit two issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pod lifecycle crashes&lt;/strong&gt; — pods would create successfully, but the containers inside would crash immediately with &lt;code&gt;SIGTERM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network mode host conflicts&lt;/strong&gt; — &lt;code&gt;network_mode: host&lt;/code&gt; caused port binding failures because the containers competed for the same ports&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The solution for the pod issue is &lt;code&gt;podman-compose up --in-pod=false&lt;/code&gt;, 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.&lt;/p&gt;
&lt;p&gt;The solution for the network issue is a named bridge network defined in the compose file, with explicit port mappings. No host mode.&lt;/p&gt;
&lt;h2 id="the-setup"&gt;The Setup&lt;/h2&gt;
&lt;p&gt;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&amp;rsquo;s tunnel connects to.&lt;/p&gt;
&lt;p&gt;The environment file handles the sensitive bits: &lt;code&gt;SECRET_KEY_BASE&lt;/code&gt;, &lt;code&gt;BASE_URL&lt;/code&gt;, and &lt;code&gt;DISABLE_REGISTRATION&lt;/code&gt;. The secret key is generated on first deploy with &lt;code&gt;openssl rand -base64 48&lt;/code&gt;. Registration is disabled after the initial admin account is created.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s it. No secrets management, no TLS certificates to maintain, no nginx or traefik layer. Cloudflare&amp;rsquo;s tunnel handles HTTPS termination and routing. I prefer the dashboard for the tunnel config.&lt;/p&gt;
&lt;h2 id="integration-with-hugo"&gt;Integration with Hugo&lt;/h2&gt;
&lt;p&gt;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 &lt;code&gt;analytics.derekarmstrong.dev&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The tracking script is minimal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt; &lt;span class="na"&gt;data-domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;derekarmstrong.dev&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://analytics.derekarmstrong.dev/js/script.js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That goes in the head, and that&amp;rsquo;s it. Plausible handles the rest. No cookies, no GDPR notices, no dark pattern consent banners. It&amp;rsquo;s just page views.&lt;/p&gt;
&lt;h2 id="privacy-first"&gt;Privacy First&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;DISABLE_REGISTRATION=true&lt;/code&gt; 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.&lt;/p&gt;
&lt;h2 id="backing-up"&gt;Backing Up&lt;/h2&gt;
&lt;p&gt;This is the part most guides skip. Boring but important. There&amp;rsquo;s two databases, five podman volumes, one compose file, and one &lt;code&gt;.env&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;backup.sh&lt;/code&gt; script handles this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Stops the containers (so the database writes are durable)&lt;/li&gt;
&lt;li&gt;Exports each podman volume to a tar archive&lt;/li&gt;
&lt;li&gt;Copies &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;compose.yml&lt;/code&gt; to the backup&lt;/li&gt;
&lt;li&gt;Streams the backup off the host to a local machine&lt;/li&gt;
&lt;li&gt;Restarts the containers&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;code&gt;restore.sh&lt;/code&gt; script reverses the process: extract the backup, import the volumes, copy the config, start the containers.&lt;/p&gt;
&lt;p&gt;Both scripts are idempotent. The backup completes in under 2 minutes for my site. If you&amp;rsquo;re running a high-traffic site with large event tables, you might want to adjust the &lt;code&gt;STALE_DAYS&lt;/code&gt; variable to keep more recent backups around.&lt;/p&gt;
&lt;h2 id="running-it"&gt;Running It&lt;/h2&gt;
&lt;p&gt;The workflow is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Fresh deploy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./deploy.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Backup to local machine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./backup.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Restore from backup&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./restore.sh backup-20260523.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Deploy once, back up regularly, restore when needed.&lt;/p&gt;
&lt;h2 id="what-i-learned"&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;The Podman rootless pod bugs on Rocky Linux were a real blocker. The ClickHouse config volume mounts don&amp;rsquo;t work with rootless podman — the workaround is to not mount external configs for ClickHouse at all. Less customization, but it works.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file is the single point of failure for the deployment. If you lose the &lt;code&gt;.env&lt;/code&gt;, you lose the installation. The volume data exists on prodweb, but you can&amp;rsquo;t authenticate to the UI without the config. Every deploy script I&amp;rsquo;ve written since then backs up &lt;code&gt;.env&lt;/code&gt; explicitly. Back it up separately from the volume data.&lt;/p&gt;
&lt;p&gt;Plausible itself is solid. The UI is clean, the data is useful, and the privacy-first approach feels right for a personal blog. I&amp;rsquo;ve been running it for months and it hasn&amp;rsquo;t given me any trouble.&lt;/p&gt;
&lt;h2 id="next"&gt;Next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
— self-hosted AI infrastructure on the same homelab&lt;/li&gt;
&lt;li&gt;
— quick reference for managing containers with Podman&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This is a personal blog post about my homelab and self-hosted infrastructure. It reflects what I&amp;rsquo;ve actually deployed and what has worked in production.&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>