Alcuni link in questa pagina sono link di affiliazione: se ti registri tramite questi link, il sito riceve una commissione senza costi aggiuntivi per te. Prezzi e consigli non cambiano.
Undici giorni fa abbiamo messo un server di analytics orientato alla privacy accanto a un sito che era già in produzione. Nessuna macchina nuova, nessun servizio gestito, nessun cookie banner. Ci sono voluti circa venti minuti, ora usa 340 MB di RAM e il suo database è arrivato a 9.5 MB.
In questi undici giorni ha registrato 96 pageviews, mentre il web server davanti ha loggato 2016 risposte di pagina andate a buon fine.
Entrambi i numeri sono reali, vengono dalla stessa macchina, e il divario tra i due è la cosa più utile di questo articolo. Partiamo dall’installazione, perché l’installazione è la parte facile.
Perché fare self-hosting di questo strumento
Tre motivi, nell’ordine in cui contano davvero.
La macchina la paghi già. Le analytics hosted si pagano a mese e crescono con le pageviews, quindi il conto sale proprio quando il sito comincia a funzionare. Un container accanto a un sito esistente costa quanto costano 340 MB di RAM, che su un server da €5.49 è un arrotondamento. Abbiamo dimensionato il resto della flotta nella nostra recensione di Hetzner e nel confronto tra VPS.
Nessun cookie banner. Umami non imposta cookie e non costruisce profili cross-site. È la differenza tra un sito che si apre con i contenuti e uno che si apre con una finestra di consenso, e vale più di qualunque metrica quella finestra stia proteggendo.
I dati restano nel tuo Postgres. Li interroghi con SQL, ne fai il backup insieme al resto e li cancelli quando vuoi. Nessuna quota di export, nessun livello di retention.
Il controargomento onesto: ora gestisci un database e un’app che nessun altro aggiorna per te. Se non intendi eseguire docker compose pull una volta al mese, usa la versione hosted e smetti di leggere.
Il file compose, per una macchina che ospita già qualcosa
Qui la maggior parte delle guide sbaglia, perché presuppone una macchina vuota. Il caso realistico è un server con già un reverse proxy e un sito, e l’obiettivo è aggiungere due container senza toccare ciò che funziona.
services:
umami-db:
image: postgres:17-alpine
container_name: umami-db
restart: unless-stopped
env_file: [.env.umami]
volumes:
- umami_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 10s
timeout: 5s
retries: 10
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
restart: unless-stopped
env_file: [.env.umami]
depends_on:
umami-db:
condition: service_healthy
volumes:
umami_db:
Due dettagli fanno il lavoro vero.
Nessuno dei due container pubblica una porta. Parlano con il reverse proxy attraverso la rete compose e da internet non sono raggiungibili se non passando da lì. Se per abitudine aggiungi ports: ["3000:3000"], hai appena messo un login di amministrazione su una porta pubblica con una password di default, e solo il firewall ti separa da una brutta serata.
L’healthcheck non è decorazione. Umami esegue le migrazioni al primo avvio. Se lo avvii contro un Postgres ancora in fase di inizializzazione, la prima esecuzione fallisce con un errore di connessione e ti ritrovi a fare debug di un problema che si risolve da solo al riavvio. condition: service_healthy elimina la race condition.
Il file .env.umami accanto:
POSTGRES_DB=umami
POSTGRES_USER=umami
POSTGRES_PASSWORD=<long random string>
DATABASE_URL=postgresql://umami:<same string>@umami-db:5432/umami
APP_SECRET=<another long random string>
DATABASE_URL è l’unica variabile che l’applicazione richiede davvero. APP_SECRET firma i token di autenticazione, e la documentazione è esplicita: ogni installazione deve avere il proprio. Generali entrambi allo stesso modo e non riusarli mai:
openssl rand -hex 32
Tieni quel file a chmod 600. Contiene la password del database in due punti.
Collegarlo a Caddy
Se usi già Caddy come descritto in il setup di Caddy e Docker, l’intera integrazione sono cinque righe:
stats.yourdomain.dev {
encode zstd gzip
header {
X-Content-Type-Options nosniff
Referrer-Policy no-referrer
-Server
}
reverse_proxy umami:3000
}
Caddy risolve umami attraverso la rete compose e ottiene il certificato alla prima richiesta. Non c’è nessuna configurazione TLS da scrivere, ed è esattamente il motivo per usarlo su una macchina piccola.
Punta il record DNS al server prima di ricaricare, altrimenti la richiesta del certificato fallisce e Caddy aspetta un po’ prima di riprovare.
Il passaggio che non devi rimandare
Umami crea un account di default al primo avvio: username admin, password umami. È documentato, il che significa che lo conosce anche ogni scanner di internet.
Accedi, cambia la password, poi verifica che la vecchia non funzioni più:
curl -s -o /dev/null -w '%{http_code}\n' \
-X POST https://stats.yourdomain.dev/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"umami"}'
401 è la risposta che vuoi. Qualsiasi altra cosa significa che la modifica non ha avuto effetto, e hai una dashboard pubblica con la password pubblicata.
Poi aggiungi il tuo sito nella UI e inserisci lo snippet che ti viene fornito nell’head del tuo HTML:
<script defer src="https://stats.yourdomain.dev/script.js"
data-website-id="your-website-id"></script>
Il website id non è un segreto. Identifica a quale sito appartiene una pageview, niente di più.
Come sono fatti undici giorni di numeri reali
Ecco il costo complessivo di questa configurazione, misurato su una macchina in produzione e non stimato:
| Metrica | Valore |
|---|---|
| Memoria del container Umami | 302 MB |
| Memoria del container Postgres | 37 MB |
| Dimensione del database, 96 pageviews | 9.5 MB |
| CPU, entrambi i container, a regime | 0.00% |
| Uptime dall’installazione, riavvii | 11 giorni, zero |
I 9.5 MB sono quasi tutti schema vuoto. Postgres non cresce in modo lineare con una manciata di eventi, quindi non ricavare da quel numero un costo per pageview. Il dato che conta per il capacity planning è la memoria: prevedi circa 350 MB e non avrai sorprese.
Ora la parte scomoda
Stessa macchina, stessi undici giorni, due fonti di verità che si contraddicono violentemente.
L’access log ha registrato 2016 risposte HTML andate a buon fine. Umami ha registrato 96 pageviews in 26 sessioni. Se li prendi alla lettera, le analytics hanno visto il 4.8% del tuo traffico e dovresti andare nel panico.
Niente panico. Smonta invece il log.
Filtrando gli user agent che contengono bot, spider, crawl, curl, python, wget o headless si eliminano 1143 di quelle 2016 richieste. Più della metà del traffico verso un sito nuovo è automazione dichiarata apertamente, e questo prima di contare quella che mente.
Delle 873 rimaste, 196 sono GoogleOther, un crawler di Google il cui user agent non contiene mai la stringa bot. Se la tua analisi del log fa grep di “bot”, hai contato in silenzio un crawler come se fosse un lettore. Vale la pena ricordarselo la prossima volta che una dashboard basata sugli access log ti dice qualcosa di lusinghiero.
Il resto contiene ancora scraper senza etichetta. Una forte concentrazione di una singola, antichissima build di Safari, per esempio, è un’impronta digitale, non un pubblico.
Quindi il confronto reale è 96 pageviews registrate contro qualche centinaio di richieste plausibilmente umane, e il divario ha tre origini, in ordine decrescente di dimensione:
- Automazione che non esegue JavaScript. Compare nel log e mai nelle analytics. È la maggior parte del divario, ed è l’analytics ad avere ragione.
- Tracker bloccati. Alcune filter list bloccano per pattern di hostname, e un host chiamato
stats.è un pattern. Il self-hosting non garantisce l’immunità, rimuove solo il noto dominio di terze parti. Il progetto documenta come servire lo script dal tuo dominio e come spostare l’endpoint di raccolta conCOLLECT_API_ENDPOINTse vuoi colmare questo divario. - Prefetch e lettori senza JS. Piccolo, reale, non risolvibile.
La conclusione non è “i log sono onesti e le analytics mentono”. È che misurano popolazioni diverse: il log conta le richieste, le analytics contano i browser che eseguono script. Il log di un sito nuovo è fatto per lo più di robot, ed è per questo che leggere i log grezzi fa sembrare popolare un sito giovane ed è il modo più facile in assoluto per mentire a se stessi sulla trazione.
Se nei primi mesi vuoi un solo numero con cui orientarti, usa le sessioni delle analytics e considera il log uno strumento di debug.
Consenso, in breve e con cautela
Le analytics senza cookie evitano il requisito di consenso legato alla memorizzazione o alla lettura di informazioni sul dispositivo del visitatore, che è ciò che rende obbligatorio il banner per il tracking convenzionale. Non rendono invisibile il trattamento: un indirizzo IP arriva comunque al tuo server, come per ogni richiesta a qualsiasi sito.
Conseguenze pratiche, non consulenza legale: indica nella pagina privacy cosa raccogli, mantieni i dati aggregati e imposta IGNORE_IP per i tuoi indirizzi, così la dashboard non è fatta soprattutto di te. Se la tua situazione è regolamentata, chiedi a qualcuno di qualificato, non a un blog.
Manutenzione, che è l’intero patto che hai firmato
docker compose pull umami umami-db
docker compose up -d umami umami-db
Questo è il lavoro mensile. Le migrazioni dello schema vengono eseguite automaticamente all’avvio del container, il che è comodo ed è anche esattamente il motivo per cui il volume conta: umami_db è l’unica cosa che non puoi ricreare da questo file. Fai il backup insieme al resto dei tuoi dati e prova il restore almeno una volta, altrimenti stai conservando una copia mai testata di 9.5 MB e una falsa sensazione di sicurezza.
Dopo la prima settimana, fai un’ultima cosa: apri la dashboard e confrontala con ciò che sai essere vero. Se hai visitato il sito tu stesso venti volte e la dashboard ne mostra due, qualcosa tra il tag script e il collector viene bloccato, ed è meglio scoprirlo ora che tra tre mesi, quando prendi decisioni basandoti su quel numero.