SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-27

Come installare OpenAnalytics su un VPS

Prima di iniziare verifica i requisiti reali: ClickHouse, Postgres, Valkey, 4 GB di RAM, 25 GB liberi e quattro record DNS. Poi installazione e spazio su disco.

L’impatto delle risorse, prima del primo passaggio

Per eseguire OpenAnalytics in modalità self-hosted servono 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. Questo è il requisito principale e va indicato prima del primo comando, non dopo.

Lo stack comprende sei servizi applicativi e tre archivi dati. Postgres gestisce il control plane: account, siti, chiavi API e link di condivisione. ClickHouse archivia gli eventi grezzi e i dati aggregati utilizzati dal dashboard. Valkey viene eseguito due volte: una volta come coda durevole degli eventi e una volta come cache di cui il sistema può fare a meno, perché questi due ruoli richiedono policy di eviction opposte. Solo un processo, il query gateway, può leggere da ClickHouse e verifica una firma Ed25519 su ogni contenitore di query prima di eseguirlo.

Se cercavi un singolo binario e un solo file di configurazione, questa soluzione non fa al caso tuo. GoatCounter è l’opzione a singolo binario in questa categoria: un unico eseguibile Go, SQLite per impostazione predefinita e nessun database esterno. Lo stack più complesso offre funnel, web vitals, attribuzione dei ricavi dal tuo 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.

Configurare prima i record DNS verso il server

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

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

Usare quattro record A oppure un record A e tre record CNAME che puntano a quest'ultimo. Prima di continuare, verificare con dig +short app.example.com. Un nome aggiunto da poco può essere ancora memorizzato come NXDOMAIN dal resolver utilizzato da Let's Encrypt. Se il primo tentativo di ottenere il certificato fallisce, attendere e controllare 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 da un tag. Il branch predefinito è riservato allo sviluppo; una release tag corrisponde alle immagini pubblicate. I comandi seguenti presuppongono che Docker e il plugin Compose siano già installati. La procedura per eseguire servizi Docker Compose su un VPS illustra questa configurazione.

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. In questo modo viene selezionata la versione stabile più recente, anziché una release candidate. --with-geoip scarica il database delle città DB-IP durante la generazione. Se lo ometti, ogni evento contiene un paese nullo e la vista geografica rimane completamente vuota. 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. Ripeti quindi il download mensilmente, altrimenti i dati sulle città diventano obsoleti.

Eseguire 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 a blocchi, perché un PEM multilinea non può essere memorizzato in un file env. Tutti questi file sono esclusi da Git e nessuno può essere rigenerato con gli stessi valori.

Copia subito questi file fuori dalla macchina. La perdita di ciascun elemento ha una conseguenza specifica:

  • Se perdi le password degli store, non puoi più accedere a Postgres e ClickHouse. Il reset è possibile solo dall'interno dei container.
  • Se perdi OA_CREDENTIAL_KEYRING, tutte le credenziali di terze parti archiviate 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 accedere di nuovo.
  • Se perdi una chiave privata di firma, devi ruotare la coppia. Non perdi 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 contenere, il servizio termina invece di avviarsi.

Avvia lo stack e controllalo

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

migrate applica gli schemi PostgreSQL e ClickHouse, quindi termina. Perciò, lo stato corretto finale è un container migrate arrestato. tracker-build compila oa.js in un volume che Caddy pubblica e termina a sua volta. Tutto il resto dovrebbe risultare healthy in docker compose ps. Un servizio che si riavvia continuamente di solito fallisce 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 errato.

Su arm64 o partendo da un branch, non sono disponibili immagini pubblicate e la compilazione viene eseguita localmente con docker compose up -d --build. Un host con 4 GB esaurisce la memoria durante la compilazione. Aggiungi prima lo swap, necessario soltanto durante la compilazione:

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

La compilazione richiede circa dieci minuti. Il pull richiede pochi minuti, motivo per cui esistono le immagini della release.

Crea immediatamente il primo account

Apri https://app.example.com. Un'installazione 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'installazione. Dopo la sua creazione, il percorso restituisce 409, quindi nessun altro può accedere dopo di te. Esegui questa operazione non appena lo stack è operativo, non la settimana successiva.

Installare il tracker

Aggiungi un sito nella dashboard per ottenere il tag. La struttura è fissa:

<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 e le chiamate come oa("track", ...) vengono accodate da uno stub e scaricate quando il file è stato caricato; in questo modo un evento personalizzato generato in anticipo non viene perso. Se un altro componente della pagina usa già window.oa, il tracker viene installato in window.openanalytics. Se lo stesso sito è disponibile anche come servizio onion, non includere il tag in quella build, perché uno script caricato da c.example.com riporta sul clearnet il visitatore di Tor Browser e collega i due indirizzi nello stesso caricamento della pagina.

Controlla quindi 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 relativa a un batch nel log del worker entro pochi secondi. Il collector restituisce 202 nel momento in cui accetta un evento, mentre 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 appena aggiunta da una migration.

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

Caddy è incluso nel file compose e ottiene autonomamente i certificati per tutti e quattro i nomi, quindi con il percorso predefinito non devi configurare alcun proxy. Se il server esegue già un reverse proxy Nginx, usa invece il infra/selfhost/nginx.conf.example fornito per pubblicare lo stack 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 acquisire tale indirizzo dalla connessione e mai da un header. Inoltrare CF-Connecting-IP da un hop non attendibile consente a qualunque chiamante di dichiarare un indirizzo arbitrario, alterando la geolocalizzazione e gonfiando contemporaneamente il conteggio dei visitatori.

L'accesso è separato in base al nome host. c. e rt. devono essere raggiungibili da ogni visitatore di tutti i siti monitorati, quindi non configurare mai l'autenticazione di base o una allowlist di indirizzi IP davanti a questi due endpoint. app. e api. devono invece essere raggiungibili soltanto dagli utenti autenticati. È l'autenticazione dell'applicazione a proteggere il 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 mostrati soltanto quando per il provider esistono sia il client ID sia il client secret. I magic link richiedono un trasporto email; in sua assenza l'API scrive soltanto l'invio in una outbox, quindi il messaggio non viene consegnato e non viene generato alcun errore. Se le altre applicazioni self-hosted sono già protette da un unico accesso Authentik, decidi subito se includere anche questo dashboard oppure mantenere account separati, perché il primo account creato qui diventa definitivamente quello privilegiato.

Un'unica impostazione determina se il dashboard funziona. AUTH_TRUSTED_ORIGINS in env/api.env deve corrispondere esattamente all'origine del dashboard. Se è errata o assente, l'API non invia alcun header CORS (condivisione delle risorse tra origini), il browser rifiuta ogni chiamata e viene mostrato un dashboard con il layout ma senza dati, mentre docker compose ps segnala che tutto funziona correttamente.

Mentre modifichi la configurazione del proxy, gestisci anche il traffico automatizzato. I crawler raggiungono il collector come qualsiasi altro client e le visualizzazioni delle pagine finiscono in ClickHouse e nei tuoi conteggi. Bloccare i crawler AI sul server consente di escluderne una parte dal database prima che comprometta l'accuratezza dei dati e consumi spazio su disco.

Non viene usato 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 determinata localmente usando il file DB-IP presente sul disco del server, quindi nessuna richiesta relativa a un visitatore lascia mai l'host. Mantenere le ricerche locali elimina il fornitore esterno, non i dati: è lo stesso limite che si presenta quando esegui una tua istanza di SearXNG e l'indirizzo IP del server diventa quello visibile ai motori di ricerca.

Il vantaggio è l'assenza di un identificatore persistente sul dispositivo del visitatore. È proprio questo elemento che fa rientrare un tracker nelle regole UE sul consenso previste dalla direttiva ePrivacy. Per questo motivo, configurazioni basate soltanto su dati aggregati come questa vengono spesso eseguite senza un banner per il consenso. Il GDPR continua ad applicarsi ai dati che memorizzi e al periodo di conservazione. La valutazione del tuo caso spetta al tuo consulente legale, non a un README.

Il costo è la perdita dell'identità tra giorni diversi. Quando il salt cambia, una persona che visita il sito lunedì e poi di nuovo mercoledì viene conteggiata come 2 visitatori, per progettazione e senza possibilità di aggirare il comportamento. I conteggi dei visitatori unici giornalieri sono affidabili. I conteggi unici settimanali e mensili vengono ricavati da quelli giornalieri e sovrastimano la portata. Pertanto, qualsiasi valore relativo ai «visitatori di ritorno» su intervalli lunghi non misura ciò che indica l'etichetta. Sessioni e percorsi sono affidabili all'interno della stessa giornata. La rotazione di ANONYMOUS_IDENTITY_SECRET produce lo stesso effetto del passaggio a un nuovo giorno, quindi trattala come una modifica dei dati e non come una normale attività di manutenzione.

Il collector rispetta Do Not Track e Global Privacy Control, il segnale del browser che indica a un sito di non vendere né condividere i dati personali. Il tag script dispone di proprie opzioni 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 risposta in localStorage con la chiave oa.consent. L'impostazione di data-storage="none" disattiva completamente l'archiviazione nel browser.

Perché il disco si riempie dopo sei mesi

Questo è ciò che mette fuori uso un server di analytics self-hosted, e di solito gli eventi non sono la causa.

Inizia dalle immagini. Una release ne pubblica dieci, che occupano complessivamente circa 13 GB su disco. Un aggiornamento scarica la nuova generazione prima di rimuovere quella precedente, quindi per un certo periodo sono presenti due generazioni. Questo rappresenta gran parte dei 25 GB necessari, prima ancora che arrivi una sola visualizzazione di pagina.

Poi ci sono gli snapshot. snapshot.sh arresta lo stack, archivia entrambi i volumi dei dati insieme a tutti i secret e riavvia. In questo caso le copie a freddo sono le uniche sicure, perché ClickHouse esegue il merge delle parti in background e una copia acquisita durante un merge non è coerente. upgrade.sh ne crea automaticamente uno prima di ogni aggiornamento, quindi gli archivi si accumulano 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, recupera la generazione precedente prima dell'aggiornamento. Questa operazione è sicura mentre lo stack è in esecuzione, perché le immagini utilizzate dai container attivi sono ancora referenziate:

docker image prune -a -f

Poi ci sono gli eventi. ClickHouse comprime in modo efficace i dati colonnari, quindi il volume degli eventi non elaborati cresce più lentamente di quanto molti si aspettino e le tabelle di aggregazione utilizzate dal 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 il dato relativo alle singole tabelle, esegui questo comando usando le credenziali ClickHouse che il generatore ha scritto 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 il dato nella prima settimana e di nuovo nella quarta. Due punti consentono di calcolare un tasso di crescita, che indica quando è necessario ridimensionare il volume. A partire da agosto 2026, la guida all'installazione self-hosted non documenta alcuna impostazione di conservazione 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.

C'è una trappola relativa alle eliminazioni che è utile conoscere prima che causi problemi. L'eliminazione di un sito o di un account accoda il lavoro per il worker, che richiede l'impostazione di CLICKHOUSE_MAINTENANCE_USER e CLICKHOUSE_MAINTENANCE_PASSWORD e l'esistenza in ClickHouse di un utente oa_maintenance corrispondente. In loro assenza, la coda di eliminazione resta indefinitamente. Il sito scompare dal dashboard, ma tutte le righe restano sul disco: l'interfaccia mostra quindi l'aspetto di una pulizia senza restituire spazio effettivo.

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 inviati mentre il collector è fermo vanno persi, perché il tracker non li ritenta. Il rollback causa perdita di dati, perché rollback.sh --to backups/<snapshot> sostituisce integralmente entrambi gli archivi ed elimina ogni riga scritta dopo la creazione dello snapshot. Il terzo costo è lo spazio su disco, rappresentato dalla raccolta di snapshot descritta sopra.

È facile applicare in modo errato due regole di riavvio. Avviare il query gateway prima dell'API, perché un'API più recente invia campi di query che un gateway meno recente rifiuta. ClickHouse richiede inoltre una ricreazione del container, non un 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. Di conseguenza, per correggere una dashboard che contatta l'hostname errato si usa docker compose up -d --force-recreate web, mai restart. Il log del container web stampa le origini con cui è stato avviato, quindi questo è 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 genere, qualsiasi altro messaggio indica che il file di configurazione contiene XML non valido. La causa più comune è la presenza di un doppio trattino all'interno di un commento XML, dove non è consentito.

AGPL-3.0 e il nome

Il codice è distribuito con licenza AGPL-3.0. L'esecuzione del codice senza modifiche per i propri siti non comporta alcun obbligo di pubblicazione. L'obbligo scatta quando si modifica il codice e si esegue la versione modificata come servizio di rete: la licenza richiede quindi di offrire il codice sorgente modificato agli utenti di quel servizio. Questo vale sia quando si forniscono dashboard ai clienti sulla propria istanza, sia quando si integra il codice in un prodotto venduto. Mantenere le modifiche in un fork pubblico soddisfa questo obbligo senza ulteriori adempimenti.

Il marchio è distinto dal codice. Il nome "OpenAnalytics" e il dominio del progetto identificano l'istanza gestita dagli autori e non fanno parte della concessione prevista dalla licenza. La propria distribuzione esegue il software senza utilizzare il marchio; assegnare quindi al servizio un nome distinto 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 distribuzione esegue sei servizi applicativi insieme 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 l'out-of-memory killer del kernel ne termina uno, di solito ClickHouse. Se il vincolo rigido è un piano da 1 GB, usa uno strumento a binario singolo come GoatCounter, che usa SQLite senza un database esterno.

È una questione da sottoporre al tuo legale, ma i fatti tecnici sono favorevoli. Non viene usato alcun cookie, l'identità del visitatore è un hash con salt che cambia ogni giorno e gli indirizzi IP non elaborati non vengono mai archiviati. Di conseguenza, non viene scritto nulla di persistente che consenta di identificare il visitatore. Il GDPR disciplina comunque i dati che archivi e il periodo di conservazione. Se vuoi subordinare esplicitamente la raccolta al consenso, imposta data-require-consent sul tag script: il tracker non raccoglie dati finché non viene concesso il consenso e salva la risposta in localStorage con il valore oa.consent.

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

202 indica che il collector ha accettato e accodato l'evento, non che lo abbia archiviato. Il worker svuota la coda e inserisce gli eventi in ClickHouse. Pertanto, una dashboard vuota con richieste riuscite indica 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 sono integri?

Controlla prima AUTH_TRUSTED_ORIGINS in env/api.env. Deve corrispondere esattamente all'origine della dashboard. Se non corrisponde, l'API non emette 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 è necessario docker compose up -d --force-recreate web, perché un semplice riavvio mantiene i valori precedenti.

AGPL-3.0 impedisce di offrire questo prodotto 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 tali utenti il codice sorgente modificato. È sufficiente pubblicarlo in un fork. Separatamente, il nome "OpenAnalytics" non è concesso in licenza insieme al codice. Pertanto, ciò che vendi deve avere un nome proprio.