
Self-hosting Umami in two hours, dropping GA4 across the fleet
I replaced GA4 with a self-hosted Umami instance across six production sites. Setup took two hours, data stays on my Postgres, and I no longer need cookie banners.
I run six production sites alone. For years, each one had a GA4 property. That meant six separate dashboards, cookie banners on every EU visitor, and the constant feeling that I was paying Google in data for a product I barely used.
Last month I killed the last GA4 tag. Every site now reports to a single Umami instance at insights.draftedby.com, hosted on my own Coolify-managed server. Setup took about two hours including DNS, Postgres provisioning, and the first dashboard. Here is exactly how I did it and what I lost in the process.
Why Umami, not Plausible or Fathom
I evaluated three options. Plausible's hosted plans start around EUR 9 per month for 10k pageviews; their self-hosted version is open-source but requires both PostgreSQL and ClickHouse, which adds operational weight if you do not already run ClickHouse. Fathom is hosted-only, starts around USD 15 per month, and bills by pageview tier. For six sites with mixed traffic, both options work but neither felt right for a homelab where I want one database technology, not two.
Umami is MIT licensed, runs on Postgres and Node, and does not bill by pageview. I already run Postgres for other apps on the same Coolify instance. Adding one more database cost me zero ops surface.
The cookieless approach was the real decision driver. Umami does not set cookies. It uses a hash of the visitor's IP and user-agent to approximate unique visitors. That makes it GDPR-safe out of the box. I removed cookie banners from every site the same day. The only banner left is a simple "this site uses cookies for authentication" on the login pages.
The two-hour setup
My homelab runs on Coolify, which I migrated to from Vercel a few months ago. If you are curious about that switch, I wrote about it in the Vercel-to-Coolify migration post. The same server handles all six sites, their Postgres databases, and now Umami.
Here is the sequence I followed.
Create a fresh Postgres database in Coolify. I named it umami, used the default Postgres 16 image, and noted the internal connection string Coolify generated. Umami needs DATABASE_URL and DATABASE_TYPE=postgresql.
Deploy Umami via Coolify's Docker template. I used umami/umami:latest as the image. Coolify's UI lets you add environment variables directly. I set three:
APP_SECRET= a 64-character random string fromopenssl rand -hex 32DATABASE_URL= the Postgres connection string Coolify gave meDATABASE_TYPE=postgresql
Wire DNS. I pointed insights.draftedby.com to my Coolify server's IP via a Cloudflare Tunnel. Cloudflare handles the SSL termination. Coolify auto-generates a Let's Encrypt certificate for the internal reverse proxy, but I prefer the tunnel for simplicity.
Login with the default credentials (admin / umami). The first thing I did was change the password and enable 2FA.
Create a website per domain inside Umami. Each site gets a UUID. I named them after the domains: preparemescours.fr, draftmylesson.com, and so on.
That was it. The whole thing took about two hours because I was learning the Umami UI. A second instance would take thirty minutes.
The Next.js wiring detail
My sites are Next.js apps. Umami provides a tracking script that you include on every page. The naive approach is to hardcode the script tag in _document.tsx or layout.tsx. That works but pollutes dev and preview environments with real analytics data.
I built a tiny component called UmamiScript that only renders when specific environment variables are set.
// components/UmamiScript.tsx
import Script from "next/script";
export default function UmamiScript() {
const id = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
const host = process.env.NEXT_PUBLIC_UMAMI_HOST;
if (!id || !host) return null;
return (
<Script
defer
src={`${host}/script.js`}
data-website-id={id}
strategy="afterInteractive"
/>
);
}
I added the variables to my production Coolify deployment's environment. They are not set in dev or preview builds. Next.js inlines NEXT_PUBLIC_* variables at build time, so the component returns null on non-production builds.
The component lives in the root layout:
// app/layout.tsx
import UmamiScript from "@/components/UmamiScript";
export default function RootLayout({ children }) {
return (
<html>
<body>
<UmamiScript />
{children}
</body>
</html>
);
}
One line per site. No cookie consent library. No GDPR popup.
What I miss from GA4
I will be honest. Umami is a pageview and event counter with a clean UI. It does not do audience demographics. GA4 could tell me age range and gender of my visitors. Umami cannot. I do not target ads, so I do not miss this much, but if you run paid campaigns you will want something else.
Conversion attribution is thin. Umami supports UTM parameters out of the box. The dashboard shows which campaigns drove visits. But it cannot model assisted conversions or multi-touch attribution. For my content sites this is fine. I care about "did the blog post bring someone to the contact form" not "which ad in a sequence converted them."
Google Search Console integration is nonexistent. I still log into GSC separately to see which queries drive traffic. Umami shows referrers but not search queries. This is a Google data lock-in I have not broken yet.
What I do not miss
Cookie banners. I removed six of them. The sites load faster, the UI is cleaner, and I stopped worrying about consent management platforms that cost money and break layouts.
Sampled data. GA4's free tier samples reports when you exceed certain thresholds. Umami gives you exact counts because it queries your own Postgres. No sampling, no approximations.
The report maze. GA4 buries the simple "how many people visited yesterday" behind a navigation tree. Umami shows it on the dashboard as a single number. I click one link and I know.
Honest scope
This setup works because my sites are content and lead generation, not transactional commerce. If I ran an ecommerce store with paid ad campaigns, Umami alone would not be enough. The conversion reports are too basic, and there is no revenue tracking beyond custom events.
For product analytics I pair Umami with PostHog Cloud EU on the Drafted By suite. PostHog handles session recordings, feature flags, and funnel analysis. Umami handles the simple "how many people visited and where did they come from" question. Two tools, distinct jobs, no overlap.
The takeaway
Two hours of setup saved me six cookie banners and a monthly analytics bill. My data lives on my Postgres instance, accessible via SQL if I ever want to run custom queries. The tradeoff is losing audience demographics and deep conversion attribution. For a solo founder running content and lead-gen sites, that tradeoff is easy to accept.
If you are on a similar setup, try it. Provision a Postgres database, deploy the container, point a subdomain at it. You can have it running before lunch.