SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

OpenAnalytics self-hosted su VPS: requisiti e

Prima di installare OpenAnalytics verifica l'ingombro reale: ClickHouse, Postgres, Valkey, 4 GB di RAM, 25 GB liberi e quattro record DNS.

Ingombro prima del primo passaggio

Per eseguire OpenAnalytics in modalità self-hosted serve un VPS Linux con circa 4 GB di RAM, 25 GB di spazio libero su disco, Docker con il plugin Compose e quattro record DNS già configurati per puntare al server. Questa è l’informazione principale e va indicata prima del primo comando, non dopo.

Lo stack comprende sei servizi applicativi e tre archivi dati. Postgres contiene il piano di controllo: account, siti, chiavi API e link di condivisione. ClickHouse contiene gli eventi grezzi e i dati aggregati utilizzati dalla dashboard. Valkey viene eseguito due volte: una volta come coda durevole degli eventi e una volta come cache che può essere persa, perché questi due ruoli richiedono policy di espulsione opposte. Solo un processo, il gateway per le query, può leggere ClickHouse e verifica una firma Ed25519 su ogni envelope di query prima di eseguirla.

Se cercavi un singolo binario e un singolo file di configurazione, questa soluzione non fa per te. GoatCounter è l’opzione con un singolo binario in questa categoria: un eseguibile Go, SQLite per impostazione predefinita e nessun database esterno. Lo stack più complesso offre funnel, metriche Web Vitals, attribuzione dei ricavi dal proprio account Stripe e un server MCP (model context protocol). Scegliere tra gli strumenti di analisi self-hosted è l’articolo che valuta questo compromesso. Questa guida presuppone che tu abbia già preso la decisione.

Punta prima i record DNS al server

Prima di iniziare, 4 sottodomini devono risolvere verso l'indirizzo IP pubblico del server, perché Caddy richiede i certificati Let's Encrypt al primo avvio e la challenge fallisce se il nome non è ancora risolvibile.

  • app.example.com pubblica la dashboard.
  • api.example.com pubblica l'API e i callback OAuth.
  • c.example.com pubblica il collector e lo script tracker.
  • rt.example.com pubblica lo stream realtime.

Usa 4 record A oppure 1 record A e 3 record CNAME che puntano a quest'ultimo. Prima di continuare, verifica con dig +short app.example.com. Un nome aggiunto da un minuto può essere ancora memorizzato come NXDOMAIN dal resolver utilizzato da Let's Encrypt; se il primo tentativo di ottenere il certificato fallisce, attendi e controlla i log di Caddy. Eseguire nuovamente l'installazione non accelera la propagazione DNS.

Come eseguire il self-hosting di OpenAnalytics con Docker Compose

Esegui il checkout di una release contrassegnata. Il branch predefinito è il ramo in cui avviene lo sviluppo, mentre un tag di release corrisponde alle immagini pubblicate. I comandi seguenti presuppongono che Docker e il plugin Compose siano già installati; l'argomento esecuzione dei servizi Docker Compose su un VPS descrive questa procedura.

git clone https://github.com/OpenLabs-so/openanalytics
cd openanalytics
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./generate-secrets.sh --domain example.com --email you@example.com --with-geoip
docker compose pull && docker compose up -d

sed '/-/d' nella riga di checkout esclude i tag delle versioni preliminari, così viene selezionata la versione stabile più recente invece di una release candidate. --with-geoip scarica il database delle città DB-IP durante la generazione. Se lo ometti, ogni evento contiene un paese null e la vista geografica non mostra alcun dato. Puoi aggiungerlo in seguito eseguendo infra/selfhost/geoip/fetch-dbip.sh, impostando GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb in env/collector.env e ricreando il collector con docker compose up -d --force-recreate collector. Il database viene aggiornato ogni mese, quindi ripeti il download mensilmente; in caso contrario, i dati sulle città diventano obsoleti.

Esegui il backup dei secret generati prima di procedere

Il generatore scrive tre elementi. .env contiene i nomi di dominio e i riferimenti alle immagini. env/*.env contiene un file di secret per ogni servizio. docker-compose.override.yml contiene tre coppie di chiavi Ed25519 come scalari YAML multilinea, perché un PEM su più righe non può essere inserito in un file env. Tutti questi file sono esclusi da Git e non possono essere rigenerati con gli stessi valori.

Copia subito questi file fuori dalla macchina. La perdita di ciascun elemento ha conseguenze specifiche:

  • Se perdi le password degli store, non puoi più accedere a Postgres e ClickHouse. Puoi reimpostarle solo dall'interno dei container.
  • Se perdi OA_CREDENTIAL_KEYRING, tutte le credenziali di terze parti memorizzate diventano irrecuperabili. Chiunque abbia collegato un account Stripe deve collegarlo di nuovo.
  • Se perdi ANONYMOUS_IDENTITY_SECRET, l'identità dei visitatori viene ricalibrata: tutti i visitatori di ieri vengono conteggiati come nuovi e l'interruzione è visibile nei grafici.
  • Se perdi AUTH_SECRET, tutte le sessioni vengono invalidate e tutti devono effettuare nuovamente l'accesso.
  • Se perdi una chiave privata di firma, devi ruotare la coppia. Non viene perso alcun dato.

Due secret devono essere identici a livello di byte in due file ciascuno. ANONYMOUS_IDENTITY_SECRET compare in collector.env e worker.env, perché il collector calcola l'hash del visitatore e il worker lo scrive. OA_CREDENTIAL_KEYRING compare in api.env e worker.env. Tutto il resto è associato intenzionalmente a un solo servizio. Se a un servizio viene assegnato un secret che non deve possedere, il servizio termina invece di avviarsi.

Avviare lo stack e verificarlo

grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose ps

migrate applica gli schemi di Postgres e ClickHouse, quindi termina. Per questo, un container migrate arrestato è lo stato finale corretto. tracker-build compila oa.js in un volume che Caddy espone, quindi termina anch’esso. Tutti gli altri servizi dovrebbero mostrare healthy in docker compose ps. Un servizio che si riavvia in un ciclo quasi sempre non supera la convalida dell’ambiente. Il log stampa tutti i problemi in un unico elenco, invece di mostrarne uno a ogni riavvio. Le due cause più comuni sono una variabile lasciata vuota, che viene rifiutata e non trattata come non impostata, e un secret inserito nel file del servizio sbagliato.

Su arm64, oppure quando si usa un branch, non sono disponibili immagini pubblicate e occorre eseguire la build locale con docker compose up -d --build. Un host con 4 GB esaurisce la memoria durante questa build. Aggiungere prima lo swap, necessario solo durante la build:

fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

La build richiede circa dieci minuti. Il download richiede pochi minuti. Per questo esistono le immagini della release.

Rivendica immediatamente il primo account

Apri https://app.example.com. Un'istanza a cui nessuno ha ancora effettuato l'accesso non mostra un modulo di accesso: propone di creare il primo account. Questo account mantiene permanentemente i privilegi amministrativi ed è l'unico a poter visualizzare la schermata delle impostazioni dell'istanza. Dopo la sua creazione, il percorso risponde con 409, quindi nessuno può accedere successivamente senza autorizzazione. Esegui questa operazione non appena lo stack è operativo, non la settimana successiva.

Installa il tracker

Aggiungi un sito nella dashboard per ottenere il tag. Il formato è fisso:

<script
  async
  src="https://c.example.com/oa.js"
  data-key="YOUR_TRACKING_KEY"
  data-collector="https://c.example.com"
></script>

Inseriscilo nell'elemento head della pagina. La chiave di tracciamento è pubblica per progettazione, quindi deve trovarsi nell'HTML, dove chiunque può leggerla. Lo script installa window.oa. Le chiamate come oa("track", ...) vengono accodate da uno stub e svuotate quando il file viene caricato; un evento personalizzato generato in anticipo non viene quindi perso. Se un altro componente della pagina usa già window.oa, il tracker viene installato come window.openanalytics.

Poi verifica l'intero percorso end-to-end:

curl -s https://c.example.com/oa.js -o /dev/null -w '%{http_code} %{size_download}\n'
curl -s https://api.example.com/health | head -c 200
docker compose logs --tail=50 worker | grep -i batch

Il primo comando dovrebbe stampare 200 e alcuni kilobyte. Carica una pagina del sito, quindi cerca una riga di batch nei log del worker entro pochi secondi. Il collector risponde 202 non appena accetta un evento; 202 significa accodato, non memorizzato. È il worker a trasferire gli eventi in ClickHouse. Se gli eventi vengono accettati ma non compare nulla nella dashboard, il worker è bloccato. Una profondità della coda Valkey che continua ad aumentare lo conferma. Le cause più comuni sono credenziali ClickHouse errate in worker.env oppure un grant mancante su una tabella aggiunta da una migrazione recente.

Mantieni pubblico il collector e proteggi la dashboard con l'autenticazione

Caddy è incluso nel file compose e ottiene autonomamente i certificati per tutti e quattro i nomi, quindi nel percorso predefinito non devi configurare alcun proxy. Se sul server è già in esecuzione un reverse proxy Nginx, anteponi allo stack il infra/selfhost/nginx.conf.example fornito e mantieni invariata la gestione degli header:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header CF-Connecting-IP "";
proxy_set_header True-Client-IP "";
proxy_set_header Fly-Client-IP "";

Il collector calcola l'hash giornaliero dei visitatori a partire dall'indirizzo IP del client, quindi deve ricavare tale indirizzo dalla connessione e mai da un header. Inoltrare CF-Connecting-IP da un hop non attendibile consente a qualsiasi chiamante di dichiarare un indirizzo arbitrario, alterando la geolocalizzazione e aumentando artificialmente il conteggio dei visitatori.

L'accesso è separato in base al nome host. c. e rt. devono essere raggiungibili da ogni visitatore di ogni sito monitorato, quindi non anteporre mai l'autenticazione di base o un elenco di indirizzi IP consentiti a questi due endpoint. app. e api. devono invece essere raggiungibili soltanto dagli utenti autenticati. È l'autenticazione dell'applicazione a proteggere la dashboard: l'accesso con password è attivo per impostazione predefinita tramite AUTH_PASSWORD_SIGNIN=enabled in env/api.env, mentre i pulsanti Google o GitHub vengono visualizzati soltanto quando per il provider sono presenti sia il client ID sia il client secret. I magic link richiedono un trasporto per la posta elettronica. In sua assenza, l'API registra l'invio soltanto in una outbox: il messaggio non viene consegnato e non viene generato alcun errore.

Un'impostazione determina il funzionamento della dashboard. AUTH_TRUSTED_ORIGINS in env/api.env deve corrispondere esattamente all'origine della dashboard. Se il valore è errato o assente, l'API non emette header CORS (condivisione delle risorse tra origini), il browser rifiuta ogni chiamata e la dashboard visualizza il layout senza dati, mentre docker compose ps segnala che tutto è operativo.

Mentre modifichi la configurazione del proxy, gestisci anche il traffico automatizzato. I crawler raggiungono il collector come qualsiasi altro client e le relative visualizzazioni delle pagine vengono registrate in ClickHouse e incluse nei tuoi numeri. Bloccare i crawler AI sul server evita che una parte di questo traffico raggiunga il database, prima che comprometta sia l'accuratezza sia lo spazio su disco.

Non viene utilizzato alcun cookie. L’identità del visitatore è un hash con salt. Il salt cambia ogni giorno e gli indirizzi IP originali non vengono mai memorizzati. La geolocalizzazione viene risolta localmente tramite il file DB-IP presente sul disco del server. Nessuna richiesta relativa a un visitatore lascia quindi l’host.

Il vantaggio è l’assenza di un identificatore persistente sul dispositivo del visitatore. È proprio questo elemento che fa rientrare un tracker nelle norme UE sull’obbligo di consenso previsto dalla normativa ePrivacy. Per questo motivo, configurazioni basate esclusivamente su dati aggregati come questa vengono comunemente utilizzate senza banner di consenso. Il GDPR continua comunque a disciplinare i dati che memorizzi e il periodo di conservazione. La valutazione del tuo caso spetta al tuo consulente legale, non a un README.

Il costo è l’assenza di un’identità coerente tra giorni diversi. Poiché il salt cambia, una persona che visita il sito lunedì e di nuovo mercoledì viene conteggiata come due visitatori. È un comportamento previsto e non esiste un modo per aggirarlo. I conteggi dei visitatori unici giornalieri sono affidabili. I conteggi dei visitatori unici settimanali e mensili vengono calcolati a partire da quelli giornalieri e sovrastimano la portata. Pertanto, qualsiasi valore relativo ai “visitatori di ritorno” su periodi lunghi non misura ciò che indica l’etichetta. Le sessioni e i percorsi sono affidabili all’interno della stessa giornata. La rotazione di ANONYMOUS_IDENTITY_SECRET produce lo stesso effetto del passaggio a un nuovo giorno. Considera quindi questa rotazione una modifica dei dati, non una normale attività di manutenzione.

Il collector rispetta Do Not Track e Global Privacy Control, il segnale del browser che comunica al sito di non vendere né condividere dati personali. Il tag script include impostazioni proprie per lo stesso scopo: data-respect-gpc, data-respect-dnt e data-require-consent, che sospende tutta la raccolta finché non viene concesso il consenso e memorizza la scelta in localStorage con la chiave oa.consent. L’impostazione di data-storage="none" disabilita completamente l’archiviazione nel browser.

Perché il disco si riempie dopo sei mesi

È questo che mette fuori uso un server di analytics self-hosted. Di solito, però, gli eventi non sono la causa.

Inizia dalle immagini. Una release ne pubblica dieci, che occupano complessivamente circa 13 GB su disco. Durante un upgrade, la nuova generazione viene scaricata prima della rimozione di quella precedente. Per un certo periodo, quindi, sono presenti entrambe. Questo rappresenta gran parte dei 25 GB richiesti, prima ancora che venga visualizzata una singola pagina.

Poi ci sono gli snapshot. snapshot.sh arresta lo stack, archivia entrambi i volumi dati insieme a tutti i secret e riavvia lo stack. In questo caso, le copie a freddo sono le uniche sicure, perché ClickHouse esegue il merge delle parti in background e una copia creata durante un merge non è coerente. upgrade.sh ne crea automaticamente uno prima di ogni upgrade. Gli archivi si accumulano quindi sullo stesso disco finché non ne limiti il numero.

./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3

Su un host vicino al limite di spazio, recupera la generazione precedente prima dell'upgrade. L'operazione è sicura mentre lo stack è in esecuzione, perché le immagini usate dai container attivi sono ancora referenziate:

docker image prune -a -f

Poi ci sono gli eventi. ClickHouse comprime molto i dati colonnari, quindi il volume degli eventi non elaborati cresce più lentamente di quanto molti si aspettino. Inoltre, le tabelle di rollup lette dalla dashboard sono piccole rispetto alla tabella non elaborata. Misura invece di fare supposizioni:

docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouse

Per ottenere il valore relativo a ogni tabella, esegui questo comando usando le credenziali ClickHouse generate in infra/selfhost/env/:

SELECT table, formatReadableSize(sum(bytes_on_disk)) AS size, sum(rows) AS row_count
FROM system.parts
WHERE active
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;

Rileva questo valore nella prima settimana e di nuovo nella quarta settimana. Due misurazioni permettono di calcolare il tasso di crescita, che indica quando è necessario aumentare la dimensione del volume. Ad agosto 2026, la guida al self-hosting non documenta alcuna impostazione di retention o time-to-live per gli eventi non elaborati. Dimensiona quindi il disco in base al tasso misurato, senza presumere che le righe meno recenti scadano automaticamente.

Prima che causi problemi, è utile conoscere una trappola relativa alle eliminazioni. L'eliminazione di un sito o di un account mette in coda un'attività per il worker. Il worker richiede che CLICKHOUSE_MAINTENANCE_USER e CLICKHOUSE_MAINTENANCE_PASSWORD siano impostati e che in ClickHouse esista un utente oa_maintenance corrispondente. In loro assenza, l'eliminazione rimane accodata per sempre. Il sito scompare dalla dashboard, ma tutte le righe restano sul disco: sembra quindi che la pulizia sia stata eseguita, mentre lo spazio non viene recuperato.

Aggiornamenti e i tre costi

git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.sh

upgrade.sh stampa tre costi prima di procedere. Il downtime è reale: gli eventi generati mentre il collector è fermo vanno persi, perché il tracker non li ritenta. Il rollback comporta una perdita di dati, perché rollback.sh --to backups/<snapshot> sostituisce integralmente entrambi gli store ed elimina tutte le righe scritte dopo la creazione dello snapshot. Il terzo costo è lo spazio su disco, occupato dall'insieme degli snapshot descritto sopra.

Due regole di riavvio sono facili da applicare in modo errato. Avviare il query gateway prima dell'API, perché un'API più recente invia campi di query che un gateway meno recente rifiuta. ClickHouse richiede invece una ricreazione del container, non un semplice riavvio, perché docker compose restart riutilizza l'ambiente originale del container e ignora senza segnalarlo la modifica apportata:

docker compose up -d --force-recreate clickhouse

La dashboard presenta lo stesso tipo di problema. Le tre origini NEXT_PUBLIC_* in env/web.env sono compilate nel bundle del browser e sostituite all'avvio del container. Pertanto, per correggere una dashboard che chiama l'hostname errato, usare docker compose up -d --force-recreate web e mai restart. Il log del container web stampa le origini con cui è stato avviato. È il modo più rapido per verificare che la modifica sia stata applicata.

Se ClickHouse non si avvia dopo una modifica alla configurazione, leggere la prima riga del log. Una riga che inizia con oa-entrypoint: indica che l'entrypoint ha rifiutato un valore impostato dall'utente. In tutti gli altri casi, di solito il file di configurazione contiene XML non valido. La causa più comune è un doppio trattino all'interno di un commento XML, che in quel contesto non è consentito.

AGPL-3.0 e il nome

Il codice è distribuito con licenza AGPL-3.0. Se lo esegui senza modifiche per i tuoi siti, non hai alcun obbligo di pubblicazione. L'obbligo scatta quando modifichi il codice ed esegui la versione modificata come servizio di rete: in questo caso la licenza richiede di offrire il codice sorgente modificato agli utenti del servizio. Questo vale sia quando fornisci ai clienti dashboard sulla tua istanza, sia quando integri il software in un prodotto che vendi. Mantenere le modifiche in un fork pubblico è sufficiente e non richiede altre procedure.

Il marchio è distinto dal codice. Il nome "OpenAnalytics" e il dominio del progetto indicano l'istanza gestita dagli autori e non rientrano nella concessione della licenza. La tua distribuzione esegue il software senza utilizzare il marchio; assegna quindi al servizio un nome proprio prima di proporlo ai clienti paganti.

FAQ

Posso eseguire OpenAnalytics su un VPS da 1 GB?

No. Il progetto richiede circa 4 GB di RAM e 25 GB di spazio libero su disco, perché una singola installazione esegue sei servizi applicativi oltre a Postgres, ClickHouse e due istanze Valkey. ClickHouse, da solo, non è un processo leggero. Su una macchina da 1 GB i container si avviano, poi il kernel OOM killer ne termina uno, di solito ClickHouse. Se il piano da 1 GB è un vincolo inderogabile, usa uno strumento a binario singolo come GoatCounter, che utilizza SQLite senza un database esterno.

È una questione da sottoporre al tuo legale, ma i fatti tecnici sono a tuo favore. Non viene usato alcun cookie, l’identità del visitatore è rappresentata da un hash con salt che cambia ogni giorno e gli indirizzi IP grezzi non vengono mai memorizzati. Non viene quindi scritto nulla di persistente che permetta di identificare il visitatore. Il GDPR disciplina comunque i dati che memorizzi e il periodo di conservazione. Se vuoi subordinare esplicitamente la raccolta al consenso, imposta data-require-consent nel tag script: il tracker non raccoglie dati finché non viene concesso il consenso e salva la risposta in localStorage con oa.consent.

Perché gli eventi restituiscono 202 ma non compaiono mai nella dashboard?

202 indica che il collector ha accettato e messo in coda l’evento, non che lo abbia memorizzato. Il worker svuota la coda e inserisce gli eventi in ClickHouse. Una dashboard vuota con richieste completate correttamente indica quindi un problema del worker. Leggi docker compose logs --tail=50 worker e monitora la profondità della coda Valkey. Se la coda continua a crescere, il worker è bloccato. Le cause più comuni sono credenziali ClickHouse errate in worker.env oppure un grant mancante su una tabella creata da una migrazione recente.

Perché la dashboard è vuota quando tutti i container risultano integri?

Controlla prima AUTH_TRUSTED_ORIGINS in env/api.env. Deve corrispondere esattamente all’origine della dashboard. Se non corrisponde, l’API non invia gli header CORS; il browser rifiuta quindi ogni chiamata e mostra il layout funzionante senza dati. Controlla poi i tre valori NEXT_PUBLIC_* in env/web.env, che vengono sostituiti all’avvio del container web. Per correggerli devi eseguire docker compose up -d --force-recreate web, perché un semplice riavvio conserva i valori precedenti.

AGPL-3.0 mi impedisce di offrire questo software ai clienti?

No, ma impone una condizione. Se esegui il codice senza modificarlo, non devi nulla a nessuno. Se lo modifichi e utilizzi la versione modificata come servizio per altre persone, devi offrire a quegli utenti il codice sorgente modificato; un fork pubblico soddisfa questo requisito. Separatamente, il nome "OpenAnalytics" non è concesso in licenza insieme al codice. Qualsiasi prodotto venduto deve quindi avere un nome proprio.