TheLineman runs on a Linux box in my house. The web server is Apache2 running on Pop!_OS. Traffic goes through a Cloudflare Tunnel so my home IP stays hidden. DNS is handled by Pi-hole on my LAN. I built the site from scratch in plain HTML.

The idea of dropping a Google Analytics script on top of all that never sat right. I'm running Pi-hole specifically to block tracking at the DNS level across my entire network — and then routing my own visitors' data to Google. That's architecturally inconsistent with everything else I've built.

So I set up Umami — open source, self-hosted, privacy-first analytics. No data leaves my server. No cookies. GDPR compliant by default. And it runs in Docker alongside everything else on the same machine already serving the site.

★ GOOGLE ANALYTICS VS UMAMI

[ GOOGLE ANALYTICS ] Data sent to Google servers
Requires cookie consent banner
Blocked by most ad blockers
Slow script, affects page load
Free but you're the product
Complex UI, mostly overkill
[ UMAMI ] Data stays on your server
No cookies — no banner needed
Not blocked by most blockers
Lightweight ~2kb script
Free and open source
Clean UI, exactly what you need

The other practical advantage: because Umami doesn't use cookies and isn't on Google's known analytics domains, it gets through ad blockers that would strip Google Analytics entirely. Your visitor count is actually closer to reality.

★ WHAT YOU NEED

[ PREREQUISITES ] A Linux server (I'm on Pop!_OS 24.04)
Docker and Docker Compose installed
A domain with Cloudflare managing DNS
An existing Cloudflare Tunnel (optional but recommended)
About 30 minutes

★ STEP 1 — INSTALL DOCKER

If you don't have Docker yet:

sudo apt update sudo apt install docker.io docker-compose -y sudo systemctl enable docker sudo systemctl start docker # Add your user to the docker group so you don't need sudo every time sudo usermod -aG docker $USER newgrp docker

★ STEP 2 — SET UP UMAMI WITH DOCKER COMPOSE

mkdir -p ~/docker/umami && cd ~/docker/umami nano docker-compose.yml

Paste this into the file:

version: '3' services: umami: image: ghcr.io/umami-software/umami:postgresql-latest ports: - "3000:3000" environment: DATABASE_URL: postgresql://umami:umami@db:5432/umami DATABASE_TYPE: postgresql APP_SECRET: REPLACE_WITH_RANDOM_STRING depends_on: - db restart: always db: image: postgres:15-alpine environment: POSTGRES_DB: umami POSTGRES_USER: umami POSTGRES_PASSWORD: umami volumes: - umami-db-data:/var/lib/postgresql/data restart: always volumes: umami-db-data:

Generate a proper secret before starting:

# Generate a secure random secret openssl rand -hex 32 # Copy the output and replace REPLACE_WITH_RANDOM_STRING in the compose file
# Start the containers docker compose up -d # Watch the logs to confirm it starts cleanly docker compose logs -f

Once you see the database migrations complete and Umami listening on port 3000, you're ready. Test it locally:

curl http://localhost:3000 # Should return HTML — the Umami login page

★ STEP 3 — EXPOSE IT VIA CLOUDFLARE TUNNEL

Rather than opening a port on your router, route it through your existing Cloudflare Tunnel. This keeps it behind Cloudflare's edge with HTTPS handled automatically.

# Add the subdomain route to your tunnel cloudflared tunnel route dns YOUR-TUNNEL-NAME analytics.yourdomain.com # Update your tunnel config nano ~/.cloudflared/config.yml

Add the Umami ingress rule alongside your existing site rule:

ingress: - hostname: yourdomain.com service: http://localhost:80 - hostname: analytics.yourdomain.com service: http://localhost:3000 - service: http_status:404
sudo systemctl restart cloudflared

Wait a minute, then open https://analytics.yourdomain.com — you should see the Umami login page over HTTPS. Default credentials are admin / umami — change them immediately.

★ STEP 4 — LOCK IT DOWN WITH CLOUDFLARE ACCESS

Your Umami dashboard is now publicly accessible at analytics.yourdomain.com. Anyone who finds it can try to log in. The Umami login is fine but adding Cloudflare Access in front of it is a clean extra layer — only your email can even reach the login page.

[ CLOUDFLARE ACCESS SETUP ] 1. dash.cloudflare.com → Zero Trust → Access → Applications
2. Add Application → Self-hosted
3. Application domain: analytics.yourdomain.com
4. Create policy → Allow → Emails → [email protected]
5. Save

Now anyone hitting analytics.yourdomain.com gets a Cloudflare email challenge first. Only your email gets through.

★ STEP 5 — ADD THE TRACKING SCRIPT

In Umami: Settings → Websites → Add Website → enter your domain → Save → Get Tracking Code.

You'll get a script tag like this:

<script defer src="https://analytics.yourdomain.com/script.js" data-website-id="YOUR-WEBSITE-ID"></script>

Add it to every HTML file just before </head>. Fastest way to do all files at once:

cd /var/www/html for file in *.html; do sed -i 's|</head>|<script defer src="https://analytics.yourdomain.com/script.js" data-website-id="YOUR-ID"></script>\n</head>|' "$file" done # Verify it worked grep -l "umami" /var/www/html/*.html

★ WHAT YOU CAN NOW TRACK

Open Umami and load your site in a browser — you'll see yourself appear as a live visitor within seconds. What Umami gives you that Cloudflare's basic analytics doesn't:

[ UMAMI VS CLOUDFLARE ANALYTICS ] Umami adds:
✓ Time on page per article
✓ Bounce rate
✓ Individual page session depth
✓ Custom events (affiliate link clicks, button clicks)
✓ UTM parameter tracking
✓ Real-time visitor view

Cloudflare still useful for:
✓ Raw bandwidth and request counts
✓ Bot traffic filtering view
✓ Cache hit rates and performance

The two together give you a complete picture — Cloudflare for infrastructure, Umami for behaviour. And all of it stays on your hardware._

[ ALSO ON THELINEMAN ] The full story of getting this site live — Apache, DNS, Cloudflare Tunnel, and every mistake along the way.
SELF-HOSTING TROUBLESHOOTING ▶