Self-Hosting Umami with Cloudflare, Fly, and Supabase

Sankra1 pts0 comments

Self-hosting Umami with Cloudflare, Fly, and Supabase

Self-hosting Umami with Cloudflare, Fly, and Supabase

Jul 11, 2026<br>| 8 min read<br>Cloudflare,<br>Supabase,<br>Fly,<br>Docker,<br>Open Source

This is a guide for running your own privacy-friendly web analytics: cookieless, first-party so ad blockers do not eat it, and cheap to run. It works for static sites and dynamic ones alike. I’ve implemented it across my own sites ( hjerpbakk.com, a Jekyll blog on GitHub Pages and a handful of SvelteKit sites on Cloudflare Pages), and I use those as worked examples throughout.

What you want from analytics

A reasonable set of goals:

Privacy-friendly. Cookieless, no personal data, so no consent banner under EU and UK rules.

Cheap. A few dollars a month, not a subscription that scales with traffic.

Lightweight. A tracking script that does not slow the pages down.

Multiple users. You and anyone else who should see the numbers.

The data stays yours. Hosted by you, not rented from a third party.

If you run more than a few sites, the cheap analytics cloud plans that cap you at three sites push you towards self-hosting anyway, and once you self-host, owning the data comes for free.

Why Umami

I’ve used Plausible before, but stopped for 2 reasons:

It became “expensive” while running many websites

I did not use the analytics to drive changes to my sites

Revisited this now since i plan on actually improving(!!!) some of my sites and want to be datadriven when doing so. Also, I’m still genuinely curious about which card games are the most popular in the nordic countries.

I chose Umami since it’s open source, cookieless, and small. The tracking script is about a kilobyte, and the dashboard answers the everyday questions: which pages, how many visitors, where they came from. It has real user accounts, so other people can get a login without any workaround too.

The ad-blocker problem

Most analytics scripts load from a third-party host on a predictable path. Block lists like EasyPrivacy know those hosts and paths, so a share of visitors never get counted. The fix is to serve the tracking first-party: from your own domain, on a path you choose, so the browser only ever talks to your site. Cloudflare is good at this, which is why it earns a place in the stack even though it does not host the app.

Why not run all of it on Cloudflare

It is tempting to keep everything on one platform. But since Umami runs on PostgreSQL or MySQL only, there is no official SQLite or Cloudflare D1 support.

The database has to live somewhere that speaks Postgres, and since I use Supabase already, that was the natural choice for me. Other serverless Postgres like Neon would work just as well i think, but examples below assume Supabase.

Cloudflare’s compute does not fit the app either. Workers are V8 isolates, not a Node server, so Umami does not run there unless you fork it and patch its database layer, which then breaks on every Umami update. Cloudflare Containers can run the official image, but they need the paid Workers plan and are built to sleep when idle, so the first pageview after a quiet spell can be lost. For one small always-on box, a tiny Fly.io machine at a few dollars a month is the better fit.

So Cloudflare does the job it is best at (the first-party edge), and the app runs where it is happy (a normal container on Fly).

The architecture

Three pieces, each with one job:

Supabase holds the Postgres database.

Fly.io runs the official Umami container, connected to Supabase. It serves the dashboard, the tracking script, and the ingest endpoint.

A Cloudflare Worker on your domain proxies the script and the ingest under a neutral path (this guide uses /np/), so tracking is first-party and the well-known Umami paths are hidden.

The flow when someone loads a page:

Visitor loads a page on yourdomain.com<br>Cloudflare Worker (route: yourdomain.com/np/*)<br>│ /np/x.js → Umami /script.js (cached at the edge)<br>│ /np/api/send → Umami /api/send<br>Umami on Fly.io ──writes──▶ Supabase (Postgres)

Everything on the site except the /np/ path is untouched. The browser sees a first-party script on your own domain, which is what the block lists do not flag.

A note on Subresource Integrity: do not put an integrity hash on this script tag. The script is first-party, not a third-party CDN file, and its contents change whenever you upgrade Umami, so a pinned hash would break the tracker on every release. SRI is the right tool for third-party CDN scripts, not for a self-hosted one.

Setting it up

The order that works:

In your Postgres host, copy the connection string. With Supabase, use the Session pooler string (port 5432) which supports the migrations Umami runs on startup.

Deploy the official Umami image (ghcr.io/umami-software/umami:postgresql-latest) to Fly with two secrets: the database connection string and a random APP_SECRET. We want one small machine that is always on so ingest never cold-starts. Umami creates its own tables on first boot.

Log in...

umami cloudflare supabase party first script

Related Articles