Star Whale Analytics Lab — Visitor Statistics with Umami

Written by

in

Self-hosting build series part 5. Taking data sovereignty to the mini-PC.

← Previous: Part 4 — The PC Takes Care of Itself (Backup·Cache·Compression)
→ Next: Part 6 — Domain Purchase: Cloudflare Registrar


TL;DR

  • Google Analytics free = I give visitor data to Google servers + forced cookie consent pop-ups
  • Umami self-hosted = own analysis on mini-PC. No cookies, data owned by me, automatic exemption from GDPR
  • Docker Compose in 5 minutes. Automatic injection of one line tracker into WP = done
  • Display: visitor count, popular posts, traffic sources, devices, countries, dwell time

1. The Hidden Costs of Google Analytics

Item Google Analytics 4 Umami self-hosted
Cost Free (personal use) Free (open source)
Data Ownership Google Own mini-PC
Cookie Consent Popup Required (GDPR/CCPA) Unnecessary (0 cookies)
Page Load Load ~50KB JS ~2KB JS
User Tracking Cross-site (linked to Google ads) Same site only
South Korea PIPA Compliance Complicated Automatic exemption
Setup Complexity 1 hour+ 5 minutes

Google Analytics externalizes the real costs to the user (the visitor). Is it normal for Google to take all that data just because a visitor entered my site? For a personal blog, it is right to operate it ourselves.

Google Analytics vs self-hosted Umami: where the visit record goes Left: Google Analytics 4. The visitor’s browser gets the page from my blog and at the same time sends the visit record to Google servers, which sit outside my area and do cross-site tracking linked to Google ads. A cookie consent popup is required, the data is owned by Google, and the page load is about 50KB of JavaScript. Right: self-hosted Umami. The visit record goes to Umami inside my mini-PC and is stored in Postgres, so the data never leaves. With zero cookies no consent popup is needed, the data is owned on my own mini-PC, and the page load is about 2KB of JavaScript. Google Analytics 4 Visitor’s browser My area Page Visit record My blog WordPress Google servers data goes out Cross-site tracking (linked to Google ads) Cookie consent popup Required Data ownership Google Page load ~50KB JS Umami self-hosted Visitor’s browser My mini-PC: the data never leaves Page Visit record My blog WordPress Umami 0 cookies Postgres Cookie consent popup Not needed (0 cookies) Data ownership Own mini-PC Page load ~2KB JS
Figure 1. With Google Analytics the visit record goes out to Google servers; with self-hosted Umami it stays on my mini-PC (the same items as the table in section 1).

2. Structure

When the visitor browser receives the StarWhale Builder page, the script in the head <script defer src="https://analytics.sticknstone.org/script.js" data-website-id="..."> executes → asynchronous request to Umami → Umami logs to Postgres. The page display is not interrupted.

How one visit gets recorded The visitor’s browser requests a StarWhale Builder page. WordPress returns the page, and its head contains one tracker line that a mu-plugin injects automatically; logged-in users are skipped. The browser runs that script with defer and sends an asynchronous request to Umami at analytics.sticknstone.org, so the page display is not blocked. Umami logs the visit to Postgres. WordPress, Umami and Postgres all run on the mini-PC. mini-PC Visitor’s browser Blog page (WordPress) one tracker line auto-injected in head mu-plugin, skips logged-in users ① Page request ② Page + tracker line Umami analytics.sticknstone.org ③ Async request page not blocked Postgres umami_db ④ Log
Figure 2. How one visit gets recorded. The tracker runs separately with defer, so the page never waits for it.

3. Docker Compose

/home/user/umami/docker-compose.yml:

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://umami:${DB_PASSWORD}@db:5432/umami
      DATABASE_TYPE: postgresql
      APP_SECRET: ${APP_SECRET}
    ports:
      - "3001:3000"

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - umami_db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 6

volumes:
  umami_db:

.env:

APP_SECRET=$(openssl rand -hex 32)
DB_PASSWORD=$(openssl rand -hex 24)

Run:

cd /home/user/umami
docker compose up -d
curl -I http://localhost:3001  # HTTP/1.1 200

Default login: admin / umami. Change the password immediately.


4. Registering StarWhale Builder + Receiving Tracker

Umami dashboard → Websites → Add Website:

Field Value
Name StarWhale Builder
Domain sticknstone.org

Save → Click site → Edit → Tracking Code:

<script defer src="https://analytics.sticknstone.org/script.js" data-website-id="1892c870-dd4d-4ce7-9fd6-2635f8ef6220"></script>

5. Automatic Injection into WP — mu-plugin

The code is automatically placed, not manually by me for every post.

/var/www/html/wp-content/mu-plugins/umami-tracker.php:

<?php
/**
 * Plugin Name: Umami Analytics Tracker
 * Description: Injects Umami tracker into front-end pages. Excludes logged-in users.
 */
if (!defined('ABSPATH')) exit;

add_action('wp_head', function () {
    if (is_admin()) return;
    if (is_user_logged_in()) return;  // Exclude my own visits
    ?>
    <script defer src="https://analytics.sticknstone.org/script.js" data-website-id="1892c870-dd4d-4ce7-9fd6-2635f8ef6220"></script>
    <?php
}, 100);

Checking is_user_logged_in() = the number of times I’ve previewed my own posts will not be counted in the statistics. Surprisingly important.


6. What is Visible on the Dashboard

Screen Data
Overview Daily, weekly, monthly visitors, page views, sessions, dwell time
Pages Ranking of popular posts
Referrers Traffic sources (Google, Twitter, other sites)
Browsers Chrome, Safari, Firefox ratio
OS Windows, Mac, iOS, Android
Devices Desktop, mobile, tablet
Countries Visitors by country
Languages Browser languages
Events Clicks and downloads defined by me

Automatically updates every 5 seconds. Realtime menu = see who is currently viewing which page.


7. Reason to Handle Domain Linking Together

When the tracker is first received, the URL is http://192.168.0.x:3001/script.js (LAN IP). This causes:
– HTTP → Browser Mixed Content block
– LAN IP → Uninterpretable from external visitor PCs

→ Umami must be activated after connecting the domain (part 9). It needs to be exposed externally as analytics.sticknstone.org to function.


FAQ

Q. Is there a free cloud plan for Umami?
Umami Cloud starts at $9 per month. Self-hosted is unlimited and free. If you have a mini-PC, self-hosted is the answer.

Q. Can I migrate from Google Analytics?
No past data import. It’s a fresh start. However, both tools can be operational concurrently (both trackers can be placed).

Q. Do I need to worry about South Korea’s PIPA (Personal Information Protection Act)?
Umami does not store IP addresses (only hashes), and there are no cookies. It qualifies for PIPA exemption.

Q. Is there a mobile app?
No official app. The mobile web is responsive, so it displays well on phones.

Q. Can I export data as CSV?
CSV and JSON export are available on the dashboard. API is also provided.


Next Part Preview

Part 6 — Domain Fraud: Cloudflare Registrar. Reasons for purchasing from Cloudflare with Gabia and WhoIs (price, management integration, future Tunnel connection).


Summary in One Line

The real cost of Google Analytics is the externalization of user data. Umami self-hosted = 5 minutes of Docker on the mini-PC + automatic WP mu-plugin injection = data sovereignty + no cookies + the same information.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *