Post

Implementing a Self-Hosted Umami Analytics Dashboard with Cloudflare Zero Trust

Implementing a Self-Hosted Umami Analytics Dashboard with Cloudflare Zero Trust

I wanted real analytics for my blog - but I didn’t want Google tracking my visitors.

So I deployed Umami analytics in my homelab and protected the dashboard using Cloudflare Zero Trust.

It worked… eventually :) .

Along the way I broke my containers, triggered a 502 error, and accidentally blocked my own analytics traffic.

Here’s how I built the system and what I learned fixing it.

Discovering Umami Analytics

While working on my blog I started thinking about something I didn’t actually have - real analytics.

I could see commits, page builds, and the occasional GitHub Pages traffic spike, but I had no idea who was visiting the site or what posts people were reading.

Most people just drop Google Analytics into their site and call it a day. But that never really sat right with me. I didn’t want to rely on a third-party tracker or hand over visitor data to another company.

That’s when I discovered Umami analytics.

Umami is a lightweight, privacy-friendly, open source analytics platform that you can self-host. It provides clean dashboards and useful visitor insights without cookies or invasive tracking.

Immediately I thought:

This would be perfect to run in my homelab.


My Environment

The stack running in my homelab looks like this:

  • Unraid server
  • Docker containers
  • Umami analytics
  • PostgreSQL database
  • Cloudflare Tunnel
  • Cloudflare Zero Trust Access
  • Jekyll blog hosted with GitHub Pages

Architecture overview:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Visitor

- Website (GitHub Pages)

- Umami script
  - stats.example.com/script.js

  - Cloudflare

- Cloudflare Tunnel

- Unraid Docker

- Umami + PostgreSQL

Umami Docker Stack

Dashboard access:

1
2
3
4
5
Admin

Cloudflare Access authentication

Umami dashboard

Deploying Umami

I deployed Umami using Docker along with a PostgreSQL container.

The important environment variable is the database connection string.

1
DATABASE_URL=postgresql://umami:umami@umami-db:5432/umami

Once running, the dashboard was accessible locally.


Creating a Public Subdomain

Inside Cloudflare DNS I created the subdomain:

1
stats.example.com

Traffic to this subdomain is routed through my Cloudflare Tunnel directly to the Umami container running in my homelab.

This allowed the dashboard to be accessed externally.


Adding the Umami Tracking Script

My blog runs on Jekyll using the Chirpy theme, which does not natively support Umami analytics.

Because of this I manually injected the tracking script into the site layout.

1
2
<script defer src="https://stats.example.com/script.js"
        data-website-id="YOUR-UMAMI-WEBSITE-ID"></script>

After rebuilding the site and pushing the changes to GitHub Pages, the analytics script began loading correctly.


Protecting the Dashboard with Cloudflare Access

Inside Cloudflare Zero Trust, I created a new Access application.

Navigation path:

1
Zero Trust > Access > Applications > Add Application

Application type:

1
Self Hosted

Application domain:

1
stats.example.com

This tells Cloudflare that traffic to this dashboard should require authentication.


Creating an Access Policy

I created a policy allowing only my email address.

Policy configuration:

1
2
3
4
Policy name: Allow Admin
Action: Allow
Session duration: 24 hours
Rule selector: Emails

Allowed user:


The First Issue I Ran Into

After enabling Access, visiting the dashboard returned a 502 Bad Gateway error.

Cloudflare displayed the diagnostic page showing:

1
2
3
Browser: Working
Cloudflare: Working
Host: Error

This indicated that the issue was occurring on the origin server rather than at Cloudflare.


What Actually Caused the 502

The Umami container had crashed.

Checking the container logs showed me the problem:

1
Can't reach database server at 'umami-db'

This happened because:

  • the database container stopped
  • containers are not on the same network
  • the database hostname does not match the container name

Restarting the database container and then restarting the Umami container resolved the error.


The Second Problem

Even after fixing the container issue, analytics still showed zero visitors.

Using the browser developer tools network tab I discovered that:

1
2
script.js loaded successfully
tracking requests were failing

The issue turned out to be related to Cloudflare Access configuration.


The Configuration Mistake

My Access application was protecting the entire subdomain.

1
stats.example.com

That meant Cloudflare was blocking requests to:

1
2
/script.js
/api/send

However, these endpoints must remain public so that visitors can send analytics events.


The Correct Configuration

The fix was restricting Access protection to only the login path.

Public hostname configuration:

1
2
3
Subdomain: stats
Domain: example.com
Path: /login*

This allows analytics traffic while protecting the dashboard login.

Public endpoints that remain accessible:

1
2
stats.example.com/script.js
stats.example.com/api/send

Protected endpoint:

1
stats.example.com/login

Verifying That Analytics Works

Using the browser developer tools network tab I confirmed the requests were successful.

1
2
script.js → 200
/send → 200

Testing network script protocol in brower dev tools

Testing network send protocol in brower dev tools

This confirmed that the analytics event was successfully recorded.

Shortly after, my Umami dashboard began showing visitors.

Real time Umami dashboard


Final Result

Visitors can load the analytics script normally, while the dashboard remains protected.

Analytics flow:

1
Visitor > Website > Umami script > Analytics stored

Admin access flow:

1
Admin > Cloudflare Access > Email authentication > Umami dashboard

Lessons Learned

A few things I learned while setting this up:

  • Protecting an entire subdomain with Cloudflare Access can break API endpoints
  • Analytics scripts must remain publicly reachable
  • Container networking issues can surface as 502 errors in Cloudflare
  • Browser developer tools are extremely useful for debugging analytics

The biggest lesson was that security controls can easily break application functionality if they are applied too broadly.

This is part of the security lab I run in my homelab where I experiment with SIEM, honeypots, detection engineering, and infrastructure security.


If it’s not broken, fix it til it is.

This post is licensed under CC BY 4.0 by the author.