— 5 min read

Google Analytics? No thanks. I wanted an analytics tool that:
After some research, I landed on Umami and self-host it on Fly.io.
Plausible and Fathom are good privacy-first alternatives, but:
As an indie dev with multiple projects, that adds up. I wanted a solution that costs nothing.
Umami is:
Fly.io is perfect for self-hosting: generous free tier, easy deployment, and locations worldwide.
# macOS
brew install flyctl
# Or directly
curl -L https://fly.io/install.sh | shfly launch --image ghcr.io/umami-software/umami:postgresql-latestFly asks for app name and region. I use analytics as the name and fra (Frankfurt) as the region.
Umami needs a PostgreSQL database. My first instinct was Fly's managed Postgres — one command, zero thinking:
fly postgres create --name analytics-db
fly postgres attach analytics-db --app analyticsIt worked, but the bill didn't: $38/month for a database this small. I migrated to a plain, unmanaged Postgres app instead:
# Create a minimal Postgres app (not Fly's managed offering)
fly apps create analytics-db
fly volumes create analytics_db_data --app analytics-db --region fra --size 1
# Deploy postgres:16-alpine with a small config (256MB, shared-cpu-1x)
fly deploy --config fly.postgres.toml --app analytics-db
fly secrets set DATABASE_URL="postgresql://umami:<password>@analytics-db.internal:5432/umami" \
--app analyticsSame database, same reliability, ~$2/month instead of $38. Connect over .internal, not .flycast — the latter needs an extra [[services]] block in the config for no benefit here.
Umami needs an APP_SECRET for session encryption:
fly secrets set APP_SECRET="$(openssl rand -hex 32)"fly deployAfter a few minutes, Umami runs at https://analytics.fly.dev.
I use analytics.herrlichdigital.de:
fly certs create analytics.herrlichdigital.deThen set DNS CNAME to analytics.fly.dev.
Umami automatically creates an admin user:
Change it immediately after first login!
One script tag per project:
<script
defer
src="https://analytics.herrlichdigital.de/script.js"
data-website-id="736b294f-2b1e-4f76-9fba-c898539330f5"
/>In your root.tsx:
export default function Root() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<script
defer
src="https://analytics.herrlichdigital.de/script.js"
data-website-id="736b294f-2b1e-4f76-9fba-c898539330f5"
/>
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}{
process.env.NODE_ENV === "production" && (
<script
defer
src="https://analytics.herrlichdigital.de/script.js"
data-website-id="736b294f-2b1e-4f76-9fba-c898539330f5"
/>
);
}Umami supports custom events for button clicks, form submissions, etc:
// Global function from Umami
declare function umami(eventName: string): void;
// Usage
<button onClick={() => umami("signup-clicked")}>
Sign Up
</button>| Service | Cost/Month | | ------------------------------------ | ---------- | | Fly.io App (shared-cpu-1x, 1GB) | ~$5 | | Fly.io Postgres (self-hosted, 256MB) | ~$2 | | Total | ~$7 |
Not free, but a fraction of what a managed database would cost — my first Postgres setup (Fly's managed offering) alone was $38/month for the database. Self-hosting Postgres too, instead of just the app, is what actually makes this cheap.
For comparison:
Umami's dashboard is clean and shows exactly what you need:
No 47 reports that nobody needs. No ML-powered insights. Just numbers.
Self-hosting Umami was one of the best decisions for my projects:
The only downside: You're responsible for uptime. But Fly.io is stable and Umami has minimal requirements.
I track Sharry, herrlichdigital.de, and all future projects with it. One Umami instance for everything.
Questions? Find me on X @pr0gstar.