Self-Hosting Umami with Cloudflare, Fly, and Supabase
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.
more
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 │
Where the snippet goes depends on the stack. Two common cases:
A Jekyll site on GitHub Pages, fronted by Cloudflare. Put the snippet in the layout’s (for example _layouts/default.html ). The Worker route on yourdomain.com/np/* intercepts those requests before they reach GitHub Pages. My own blog is set up this way.
A SvelteKit site on Cloudflare Pages. Put the snippet in the of src/app.html. A useful detail: a Worker route takes precedence over a Cloudflare Pages custom domain on the same hostname, so the same Worker still serves /np/ here without any per-repo proxy code. My card-game sites run this way.
Because a Worker route only attaches to zones in the Worker’s own Cloudflare account, a site hosted on a different account needs its own proxy. The tidy way is a Cloudflare Pages Function ( functions/np/[[path]].ts ) with the same logic, which ships with that site and does not depend on your Worker. I have one site on a teammate’s account that uses exactly this.
Keeping it current
Self-hosting means you own the upkeep, but it’s not too much work:
Pin a specific Umami version in the Fly config rather than tracking latest, so a deploy is predictable.
Before upgrading, take a database backup with pg_dump or don’t, if old events are not useful to you. Umami runs migrations on boot, and not every migration is reversible, so the backup can be a safety net.
To upgrade, bump the version, deploy, and check the dashboard still loads and records a visit. If something is wrong, redeploy the previous version and, if needed, restore the backup.
Watch the Umami releases on GitHub for new features.
The Cloudflare Worker rarely changes. Now and then refresh its compatibility date and the wrangler version, then redeploy.
Keep an eye on your Postgres free limit. If the data grows, Umami’s retention settings can prune old events.