# Umami Self-Hosted Setup (Analytics) Stapsgewijze handleiding om Umami (self-hosted website-analytics) op te zetten op de webserver-Pi (Pi Main, `192.168.1.103`) met Podman, en bereikbaar te maken via `analytics.slpsoftware.nl`. Dit vult het "Analytics/uptime dashboard" open item uit `operations/production-readiness-checklist.md` in voor het analytics-gedeelte. Umami draait onder een **eigen dedicated Linux-user** (`umami`) in plaats van onder het bestaande hoofdaccount, zodat elke self-hosted service (nu en in de toekomst) netjes geïsoleerd blijft — zie "Waarom een dedicated user" hieronder. Dit patroon kan hergebruikt worden voor volgende self-hosted diensten op dezelfde Pi('s). ## Vereisten - Podman is al aanwezig op de Pi (bevestigd door de gebruiker) — systeembreed geïnstalleerd, dus beschikbaar voor elke user, ook een nieuw aangemaakte. - `podman-compose` geïnstalleerd voor de `umami`-user: `pip3 install --user podman-compose` (of via de package manager van je distro, indien beschikbaar) — zie stap 0 hieronder. - DNS: een A-record voor `analytics.slpsoftware.nl` dat naar het publieke IP van de reverse-proxy-Pi wijst (dezelfde Pi die ook `test.slpsoftware.nl` afhandelt). ## Waarom een dedicated user Podman zelf hoeft niet per user geïnstalleerd te worden (het is een systeembreed pakket), maar rootless Podman-containers erven wél de rechten van de user die ze start. Door Umami onder een eigen `umami`-user te draaien in plaats van je eigen hoofdaccount: - kan een kwetsbaarheid in de Umami-container (zelfs bij een container-escape binnen de rootless-namespace) geen bestanden van je persoonlijke account lezen/schrijven; - houd je containerstorage, systemd user-services en logs van verschillende self-hosted diensten netjes gescheiden per user, ook als je Pi qua rekenkracht beperkt is; - kun je dit patroon straks 1-op-1 hergebruiken voor een volgende self-hosted dienst (bv. een eigen `uptime`-user, `git`-user, etc.), zonder dat diensten elkaars bestanden kunnen benaderen. ## 0. Dedicated `umami`-user aanmaken (op de webserver-Pi) 1. Maak de user aan (zonder wachtwoord-login is prima, we loggen in via `sudo -iu umami` of `su - umami`): ```bash sudo useradd --create-home --shell /bin/bash umami sudo passwd -l umami # login met wachtwoord blokkeren, sudo -iu blijft werken ``` 2. Controleer dat er een subuid/subgid-range is toegewezen (nodig voor rootless Podman). Op de meeste moderne distributies (incl. Raspberry Pi OS) gebeurt dit automatisch bij `useradd`: ```bash grep umami /etc/subuid /etc/subgid ``` Zie je geen output, voeg dan handmatig een range toe (pas de startwaarde aan als die al in gebruik is door een andere user): ```bash sudo usermod --add-subuids 200000-265535 --add-subgids 200000-265535 umami ``` 3. Zorg dat de `umami`-sessie blijft "linger-en", zodat rootless Podman-services ook actief blijven zonder dat de user is ingelogd (nodig voor stap 2 hieronder): ```bash sudo loginctl enable-linger umami ``` 4. Log in als de nieuwe user om de rest van de setup uit te voeren: ```bash sudo -iu umami ``` ## 1. Umami + database opzetten (als de `umami`-user, op de webserver-Pi) 0. Installeer `podman-compose` voor deze user, indien nog niet systeembreed aanwezig: ```bash pip3 install --user podman-compose ``` 1. Maak een map aan, bv. `~/umami/` (dit is nu `/home/umami/umami/`), en kopieer daarin: - `operations/deployment/umami/podman-compose.yml.example` → `~/umami/podman-compose.yml` - `operations/deployment/umami/.env.example` → `~/umami/.env` 2. Vul in `~/umami/.env` een echte `POSTGRES_PASSWORD` en `APP_SECRET` in (bv. via `openssl rand -base64 32` voor beide — gebruik twee verschillende waarden). 3. Start de containers: ```bash cd ~/umami podman-compose up -d ``` 4. **Firewall (ufw) openzetten voor poort 3001.** Pi Main gebruikt `ufw` met als default policy `deny incoming` — net als poort `3000` (Gitea) al een expliciete regel heeft voor het interne IP van de reverse-proxy-Pi, heeft `3001` (Umami) dezelfde regel nodig, anders krijg je bij het testen `curl: (56) Recv failure: Connection reset by peer` (ook wanneer je *op Pi Main zelf* naar het publieke IP `192.168.1.103` curl't — dat gaat namelijk nog steeds via de firewall-INPUT-chain, in tegenstelling tot `localhost`/`127.0.0.1`): ```bash sudo ufw allow from 192.168.1.102 to any port 3001 proto tcp sudo ufw reload ``` Controleer met `sudo ufw status verbose` dat er nu een regel voor `3001/tcp` staat, analoog aan de bestaande regel voor `3000/tcp`. 5. Controleer dat Umami draait en bereikbaar is op het interne netwerk. Let op: de **externe** poort is `3001`, niet het gebruikelijke `3000` — die poort is op deze Pi al in gebruik door Gitea (`bind: address already in use` bij het opstarten van de container als je toch `3000` gebruikt): ```bash curl http://192.168.1.103:3001/api/heartbeat ``` Dit zou een JSON-antwoord met `"ok"` moeten teruggeven. Krijg je toch een `Connection reset by peer`, controleer dan eerst of `curl http://localhost:3001/api/heartbeat` (dus via loopback, buiten de firewall om) wél werkt — zo ja, dan zit het probleem zeker in de ufw-regel hierboven en niet in Umami/Podman zelf. > **Let op — na een wijziging aan `podman-compose.yml`/`.env` (bv. een andere poort):** > `podman-compose up -d` update alléén containers waarvan de configuratie is gewijzigd, > maar een poortmapping (`ports:`) wordt door Podman **niet** live herladen op een > bestaande, al aangemaakte container. Heb je `podman-compose up -d` al eerder gedraaid > met een oude versie van `podman-compose.yml` (bv. met poort `3000` in plaats van > `3001`), werk dan eerst je lokale `~/umami/podman-compose.yml` bij met de nieuwste > versie uit dit repository, en draai daarna: > ```bash > cd ~/umami > podman-compose down > podman-compose up -d > ``` > `podman-compose down` verwijdert de containers (niet de database-data, die staat in > een persistent volume) en `up -d` maakt ze opnieuw aan met de bijgewerkte poort/config. > Dit is enkel nodig als de container al bestond met de oude configuratie; bij een > eerste, nieuwe `up -d` (of als de vorige poging überhaupt nooit is gestart doordat > Podman de poort niet kon claimen) is dit niet nodig. ## 2. Automatisch starten na reboot (systemd user service) Podman-compose start niet vanzelf op na een herstart van de Pi, tenzij je dit expliciet instelt. Omdat rootless Podman hier al gebruikt wordt, is een systemd **user**-service — gekoppeld aan de `umami`-user — de eenvoudigste aanpak. De `loginctl enable-linger umami` uit stap 0.3 is hiervoor al gezet, dus deze service blijft ook draaien zonder dat de `umami`-user zelf is ingelogd. 1. Log in (of blijf ingelogd) als de `umami`-user: `sudo -iu umami`. > **Let op — "Failed to connect to bus: No medium found" bij `systemctl --user`:** > Dit betekent dat er (nog) geen D-Bus/systemd user-sessie draait voor `umami`, meestal > omdat `sudo -iu umami` geen volledige PAM-login-sessie registreert zoals een echte > interactieve login dat wel doet — daardoor bestaat `/run/user/` (en de bus daarin) > nog niet, zelfs met linger enabled. Los dit als volgt op: > ```bash > # Controleer eerst of linger daadwerkelijk actief staat: > loginctl show-user umami | grep Linger > # Verwacht: Linger=yes. Staat er Linger=no, herhaal dan: > sudo loginctl enable-linger umami > # Start de user-manager expliciet (uid van umami opzoeken met: id -u umami): > sudo systemctl start user@$(id -u umami).service > # Log opnieuw in als umami en zet XDG_RUNTIME_DIR expliciet, voor deze sessie: > sudo -iu umami > export XDG_RUNTIME_DIR=/run/user/$(id -u) > ``` > Probeer daarna `systemctl --user daemon-reload` opnieuw. Blijft het misgaan, gebruik dan > in plaats van `sudo -iu umami` het commando `sudo machinectl shell umami@ /bin/bash` om > in te loggen — dat registreert wél altijd een volledige sessie inclusief D-Bus, in > tegenstelling tot `sudo -iu`/`su -`. 2. Maak `~/.config/systemd/user/umami.service` aan: ```ini [Unit] Description=Umami analytics (podman-compose) After=network-online.target [Service] WorkingDirectory=%h/umami ExecStart=/usr/bin/podman-compose up ExecStop=/usr/bin/podman-compose down Restart=on-failure [Install] WantedBy=default.target ``` 3. Activeer en start de service (nog steeds als de `umami`-user): ```bash systemctl --user daemon-reload systemctl --user enable --now umami.service ``` ## 3. Reverse proxy + SSL (op de reverse-proxy-Pi) 1. Kopieer `operations/deployment/nginx/analytics-nginx.conf.example` (de versie van vóór certbot: alleen poort 80, geen SSL) naar bv. `/etc/nginx/sites-available/slpsoftware-analytics.conf` op de reverse-proxy-Pi, en maak een symlink in `sites-enabled`. 2. Zorg dat het DNS-record voor `analytics.slpsoftware.nl` al actief is, en draai dan: ```bash sudo certbot --nginx -d analytics.slpsoftware.nl ``` Certbot herschrijft het bestand automatisch met de HTTPS-configuratie en de HTTP→HTTPS-redirect — zie `operations/deployment/nginx/analytics-nginx.conf.post-certbot.example` voor hoe het er dan uitziet (referentie, niet zelf kopiëren). 3. Herlaad nginx: `sudo nginx -t && sudo systemctl reload nginx`. 4. Test: open `https://analytics.slpsoftware.nl` in de browser — je zou het Umami-inlogscherm moeten zien (standaard inloggegevens: `admin` / `umami`, **direct wijzigen na eerste login**). ## 4. Website registreren in Umami en het website-ID ophalen Maak een **aparte Umami-website-entry per omgeving** aan — test en productie zijn verschillende domeinen en horen dus niet in dezelfde statistieken te belanden: 1. Log in op `https://analytics.slpsoftware.nl` en wijzig direct het standaardwachtwoord. 2. Ga naar **Settings → Websites → Add website** en maak een entry voor de testomgeving: - Name: `SLP Software (test)` (of naar keuze) - Domain: `test.slpsoftware.nl` 3. Na het opslaan toont Umami een **Website ID** (een UUID) voor deze entry — nodig voor `VITE_UMAMI_WEBSITE_ID_TEST` in de volgende stap. 4. Herhaal stap 2-3 voor productie zodra die omgeving wordt opgezet: een tweede website- entry met Domain `slpsoftware.nl`, met een eigen Website ID voor `VITE_UMAMI_WEBSITE_ID_PRODUCTION`. ## 5. Tracking script koppelen aan de website (build-configuratie) De React-app (`src/components/UmamiAnalytics.tsx`) injecteert het Umami tracking-script automatisch, mits de volgende build-time variabelen zijn ingesteld — geen van deze zijn **secrets** (client-side zichtbaar), dus als Gitea Actions **repository variables** (niet secrets), net als `VITE_SENTRY_DSN`. Omgevingsspecifieke variabelen (die per omgeving een andere waarde hebben) krijgen consistent een `_TEST`/`_PRODUCTION`-postfix; gedeelde variabelen (die bewust voor beide omgevingen identiek zijn) niet: | Variabele | Waarde | |---|---| | `VITE_UMAMI_SCRIPT_URL` | `https://analytics.slpsoftware.nl/script.js` (gedeeld tussen test en productie — zelfde Umami-instance/endpoint voor beide, geen postfix) | | `VITE_UMAMI_WEBSITE_ID_TEST` | het Website ID van de **test**-website-entry uit stap 4 | | `VITE_UMAMI_WEBSITE_ID_PRODUCTION` | het Website ID van de **productie**-website-entry uit stap 4 (pas nodig vóór de eerste `deploy_production`-run) | Stel deze in via **Gitea → Repository Settings → Actions → Variables**. Zodra ze bestaan, pakt de eerstvolgende build ze automatisch op; zonder deze variabelen slaat de app het inladen van het script gewoon over (geen crash, geen tracking). Let op: dit zijn de namen van de Gitea-variabelen — de app zelf verwacht altijd de envvar-naam `VITE_UMAMI_WEBSITE_ID` (zie `UmamiAnalytics.tsx`), dus in `continuous_integration.yaml` wordt bv. `VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_TEST }}` gebruikt in de `build`-job en `VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_PRODUCTION }}` in `build-production` — verwissel deze niet, anders komt productieverkeer in de teststatistieken terecht (of andersom). **Lokaal (`pnpm dev`)**: het tracking-script wordt hier bewust nooit geladen (zie `UmamiAnalytics.tsx`), zodat lokaal testen de bezoekersstatistieken niet vervuilt. Wil je dit toch lokaal testen, zet dan tijdelijk beide waarden in `.env.local` (zie `.env.example`) én verwijder tijdelijk de `import.meta.env.DEV`-check. ## Openstaande handmatige stappen - [ ] Dedicated `umami`-user aanmaken op Pi Main (incl. subuid/subgid-check en `loginctl enable-linger`). - [ ] `podman-compose up -d` daadwerkelijk draaien als de `umami`-user op Pi Main. - [ ] ufw-regel toevoegen voor poort `3001/tcp` (analoog aan de bestaande `3000/tcp`-regel) op Pi Main. - [ ] Systemd user-service instellen (onder de `umami`-user) voor auto-start na reboot. - [ ] DNS-record + certbot voor `analytics.slpsoftware.nl` op de reverse-proxy-Pi. - [ ] Standaard Umami-wachtwoord direct wijzigen na eerste login. - [ ] Website aanmaken in Umami voor de testomgeving en het Website ID overnemen. - [ ] `VITE_UMAMI_SCRIPT_URL` en `VITE_UMAMI_WEBSITE_ID_TEST` als Gitea repository variables instellen. - [ ] Zodra productie wordt opgezet: aparte website aanmaken in Umami voor `slpsoftware.nl` en het Website ID als `VITE_UMAMI_WEBSITE_ID_PRODUCTION` instellen (zie stap 4/5 hierboven — vereist vóór de eerste `deploy_production`-run). ## Vervolgstappen voor toekomstige self-hosted diensten Dit dedicated-user-patroon (stap 0 hierboven) is bewust generiek gehouden zodat het hergebruikt kan worden: een volgende self-hosted dienst op dezelfde of een andere Pi kan op dezelfde manier zijn eigen user krijgen (bv. `useradd --create-home`, subuid/subgid controleren, `loginctl enable-linger `, eigen `~/.config/systemd/user/.service`), zodat diensten onderling geïsoleerd blijven zonder dat dit ten koste gaat van de al beperkte rekenkracht van een Raspberry Pi (rootless Podman zelf blijft immers systeembreed gedeeld, alleen de user-context verandert per dienst).