OpenAnalytics zelf hosten op een VPS: de vereisten
Wilt u OpenAnalytics zelf hosten? U heeft minimaal 4 GB RAM en 25 GB schijfruimte nodig. Lees hier de volledige systeemvereisten, de stack met ClickHouse en de installatie.
De voetafdruk, vóór de eerste stap
Om OpenAnalytics zelf te hosten heeft u een Linux VPS nodig met ongeveer 4 GB RAM, 25 GB vrije schijfruimte, Docker met de Compose-plugin en vier DNS-records die al naar de server verwijzen. Dat is de eerlijke samenvatting, en die hoort vóór het eerste commando te staan in plaats van erna.
De stack bestaat uit zes applicatieservices en drie datastores. Postgres beheert het control plane: accounts, sites, API-keys en deellinks. ClickHouse bevat de ruwe events en de rollups die het dashboard uitleest. Valkey draait twee keer: één keer als duurzame event-wachtrij en één keer als cache die verloren mag gaan, omdat deze twee taken tegenovergestelde eviction-policies vereisen. Slechts één proces, de query gateway, mag ClickHouse uitlezen; deze verifieert een Ed25519-handtekening op elk query-envelope voordat deze wordt uitgevoerd.
Als u op zoek was naar één binary en één configuratiebestand, dan is dit niet wat u zoekt. GoatCounter is de single-binary optie in deze categorie: één Go-executable, standaard SQLite, en helemaal geen externe database. De zwaardere stack biedt u funnels, web vitals, omzetattributie vanuit uw eigen Stripe-account en een MCP (model context protocol) server. De keuze tussen zelf-gehoste analysetools is het artikel waarin deze afweging wordt besproken. Deze handleiding gaat ervan uit dat u uw keuze al heeft gemaakt.
Wijs eerst vier DNS-records naar de server
Vier subdomeinen moeten naar het publieke IP-adres van de server verwijzen voordat u begint. Caddy vraagt namelijk bij de eerste start Let's Encrypt-certificaten aan en de challenge mislukt als een naam nog niet naar het juiste adres wijst.
app.example.combedient het dashboard.api.example.combedient de API en de OAuth-callbacks.c.example.combedient de collector en het tracker-script.rt.example.combedient de realtime stream.
Gebruik vier A-records, of één A-record en drie CNAMEs die daarnaar verwijzen. Controleer dit met dig +short app.example.com voordat u verdergaat. Een naam die u zojuist heeft toegevoegd, kan nog als NXDOMAIN in de cache staan bij de resolver die Let's Encrypt toevallig gebruikt. Als een eerste certificaataanvraag mislukt, is het daarom verstandig om te wachten en de Caddy-logs te raadplegen. Het opnieuw uitvoeren van de installatie versnelt de DNS-propagatie niet.
OpenAnalytics zelf hosten met Docker Compose
Controleer een getagde release. De standaard branch is waar de ontwikkeling plaatsvindt, en een release-tag komt overeen met de daadwerkelijk gepubliceerde images. De onderstaande commando's gaan ervan uit dat Docker en de Compose-plugin al zijn geïnstalleerd, wat wordt behandeld in het draaien van Docker Compose-services op een VPS.
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 -dDe sed '/-/d' in de checkout-regel filtert pre-release tags, zodat u op de nieuwste stabiele versie terechtkomt in plaats van op een release candidate. --with-geoip haalt de DB-IP city-database op tijdens het genereren. Slaat u dit over, dan bevat elk event een leeg landveld, waardoor de geografische weergave niets toont. U kunt dit later toevoegen door infra/selfhost/geoip/fetch-dbip.sh uit te voeren, GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb in env/collector.env in te stellen en vervolgens de collector opnieuw aan te maken met docker compose up -d --force-recreate collector. Die database wordt maandelijks ververst, dus herhaal het ophalen maandelijks om te voorkomen dat uw stadsgegevens verouderen.
Maak een back-up van de gegenereerde geheimen voordat u verdergaat
De generator schrijft drie zaken weg. .env bevat de domeinnamen en de image-referenties. env/*.env bevat één bestand met geheimen per service. docker-compose.override.yml bevat drie Ed25519-sleutelparen als YAML-blokscalars, omdat een PEM-bestand met meerdere regels niet in een env-bestand kan staan. Alles wordt genegeerd door git en niets hiervan kan opnieuw worden gegenereerd met dezelfde waarden.
Kopieer deze bestanden nu van de machine af. Elk verlies heeft specifieke gevolgen:
- Verliest u de wachtwoorden van de opslag, dan bent u buitengesloten van Postgres en ClickHouse; deze zijn alleen vanuit de containers te resetten.
- Verliest u
OA_CREDENTIAL_KEYRING, dan zijn alle opgeslagen inloggegevens van derden onherstelbaar. Iedereen die een Stripe-account heeft gekoppeld, moet dit opnieuw doen. - Verliest u
ANONYMOUS_IDENTITY_SECRET, dan wordt de identiteit van bezoekers opnieuw vastgesteld: de bezoekers van gisteren tellen allemaal als nieuw en de onderbreking is zichtbaar in de grafieken. - Verliest u
AUTH_SECRET, dan wordt elke sessie ongeldig gemaakt en moet iedereen opnieuw inloggen. - Verliest u een private signing key, dan roteert u het paar. Er gaat niets verloren.
Twee geheimen moeten byte-identiek zijn in twee verschillende bestanden. ANONYMOUS_IDENTITY_SECRET komt voor in collector.env en worker.env, omdat de collector de bezoekers-hash berekent en de worker deze wegschrijft. OA_CREDENTIAL_KEYRING komt voor in api.env en worker.env. Al het overige is bewust beperkt tot precies één service. Een service die een geheim ontvangt dat hij niet mag bezitten, stopt in plaats van op te starten.
De stack opstarten en controleren
grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose psmigrate past de Postgres- en ClickHouse-schema's toe en sluit daarna af, dus een gestopte migrate-container is de correcte eindstatus. tracker-build compileert oa.js naar een volume dat door Caddy wordt geserveerd en sluit eveneens af. Alle overige services horen de status healthy te hebben in docker compose ps. Een service die in een lus herstart, faalt vrijwel altijd bij de validatie van de omgevingsvariabelen; het logbestand toont elk probleem in één lijst in plaats van één per herstart. De twee meest voorkomende oorzaken zijn een variabele die leeg is gelaten (wat wordt geweigerd in plaats van behandeld als niet ingesteld) en een secret die in het verkeerde servicebestand is geplaatst.
Op arm64, of bij gebruik van een branch, zijn er geen gepubliceerde images beschikbaar en bouwt u deze lokaal met docker compose up -d --build. Een host met 4 GB geheugen komt tijdens dit bouwproces tekort aan werkgeheugen. Voeg eerst swap toe; dit is alleen nodig tijdens het bouwen:
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabHet bouwen duurt ongeveer tien minuten. Het ophalen van images duurt enkele minuten, wat de reden is dat er release-images bestaan.
Claim direct het eerste account
Open https://app.example.com. Een implementatie waar nog niemand op is ingelogd, toont geen inlogformulier: het biedt de mogelijkheid om het eerste account aan te maken. Dat account is permanent het bevoorrechte account en is het enige account dat het scherm met implementatie-instellingen ziet. Zodra dit account bestaat, geeft de route 409 als antwoord, zodat niemand na u kan inloggen. Doe dit direct zodra de stack gezond is, en niet pas de week daarna.
De tracker installeren
Voeg een site toe in het dashboard en u ontvangt de tag. De structuur staat vast:
<script
async
src="https://c.example.com/oa.js"
data-key="YOUR_TRACKING_KEY"
data-collector="https://c.example.com"
></script>Plaats deze in de head van de pagina. De tracking key is ontworpen als publiek, dus deze hoort in uw HTML thuis waar iedereen hem kan inzien. Het script installeert window.oa en aanroepen zoals oa("track", ...) worden in een wachtrij geplaatst door een stub en verwerkt zodra het bestand is geladen. Hierdoor gaat een vroegtijdig afgevuurd custom event niet verloren. Als een ander element op de pagina al eigenaar is van window.oa, installeert de tracker zichzelf als window.openanalytics. Als dezelfde site ook bereikbaar is als een onion service, laat de tag dan weg uit die build. Een script dat wordt opgehaald vanaf c.example.com trekt een Tor Browser-bezoeker namelijk terug naar het clearnet en koppelt de twee adressen aan elkaar tijdens hetzelfde paginaladen.
Controleer vervolgens het volledige pad van begin tot eind:
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 batchDe eerste opdracht hoort 200 en enkele kilobytes terug te geven. Laad een pagina op uw site en zoek binnen enkele seconden naar een batch-regel in het worker-logboek. De collector antwoordt met 202 zodra deze een event accepteert, en 202 betekent dat het event in de wachtrij staat, niet dat het is opgeslagen. De worker verplaatst events naar ClickHouse. Als events worden geaccepteerd maar niet verschijnen in het dashboard, betekent dit dat de worker geblokkeerd is; een Valkey-wachtrij die blijft groeien bevestigt dit. De gebruikelijke oorzaken zijn onjuiste ClickHouse-inloggegevens in worker.env of ontbrekende rechten op een tabel die zojuist door een migratie is toegevoegd.
Houd de collector publiek en het dashboard achter authenticatie
Caddy wordt meegeleverd in het compose-bestand en verkrijgt zelfstandig certificaten voor alle vier de namen, dus het standaardpad vereist geen proxy-werkzaamheden van uw kant. Als de server al een nginx reverse proxy draait, plaats de stack dan achter de meegeleverde infra/selfhost/nginx.conf.example en behoud de afhandeling van headers intact:
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 "";De collector leidt de dagelijkse bezoekershash af van het IP-adres van de client; dit adres moet daarom direct uit de verbinding worden gehaald en nooit uit een header. Het doorgeven van CF-Connecting-IP via een onbetrouwbare hop stelt elke aanroeper in staat om elk willekeurig adres te claimen, wat zowel de geolocatie corrumpeert als de bezoekersaantallen onterecht verhoogt.
Toegang wordt strikt gescheiden op basis van hostnaam. c. en rt. moeten bereikbaar zijn voor elke bezoeker van elke site die u meet; plaats daarom nooit basic auth of een IP-allowlist voor deze twee. app. en api. hoeven alleen bereikbaar te zijn voor gebruikers die inloggen. De eigen authenticatie van de applicatie beschermt het dashboard: inloggen met een wachtwoord is standaard ingeschakeld via AUTH_PASSWORD_SIGNIN=enabled in env/api.env, en de Google- of GitHub-knoppen verschijnen alleen wanneer zowel de client ID als de client secret voor die provider aanwezig zijn. Magic links vereisen een mail-transport; zonder dit schrijft de API de verzending enkel naar een outbox, waardoor er niets wordt afgeleverd en er geen foutmeldingen optreden. Als uw andere zelfgehoste applicaties al achter één enkele Authentik-login staan, bepaal dan tijdig of dit dashboard zich daarbij aansluit of eigen accounts behoudt, aangezien het eerste account dat u hier aanmaakt permanent het bevoorrechte account is.
Eén instelling bepaalt of het dashboard überhaupt functioneert. AUTH_TRUSTED_ORIGINS in env/api.env moet exact overeenkomen met de dashboard-origin. Indien deze onjuist of afwezig is, verstuurt de API geen CORS-headers (cross-origin resource sharing), weigert de browser elk verzoek en krijgt u een dashboard dat de layout wel laadt maar geen data toont, terwijl docker compose ps rapporteert dat alles in orde is.
Wanneer u de proxy-configuratie aanpast, pak dan ook geautomatiseerd verkeer aan. Crawlers benaderen de collector zoals elke andere bezoeker, en hun pageviews belanden in ClickHouse en in uw statistieken. Het blokkeren van AI-crawlers op serverniveau houdt een deel daarvan uit de database voordat het u zowel nauwkeurigheid als schijfruimte kost.
Wat cookieless hier betekent en wat het u kost
Er is geen cookie. De identiteit van een bezoeker is een salted hash, de salt roteert elke dag en ruwe IP-adressen worden nooit opgeslagen. Geolocatie wordt lokaal opgelost aan de hand van het DB-IP-bestand op uw eigen schijf, waardoor er nooit een opzoekopdracht over een bezoeker de host verlaat. Het lokaal houden van opzoekopdrachten verwijdert de leverancier, niet de data; dit is dezelfde beperking die optreedt wanneer u uw eigen SearXNG-instantie draait en het IP-adres van uw server degene is die de zoekmachines zien.
Wat dit u oplevert, is de afwezigheid van een identificatiecode die op het apparaat van de bezoeker wordt bewaard. Dit is precies het punt dat een tracker onder de EU ePrivacy-toestemmingsregels laat vallen. Opstellingen die uitsluitend met geaggregeerde gegevens werken, zoals deze, worden om die reden vaak zonder toestemmingsbanner gebruikt. De AVG (GDPR) blijft van toepassing op alles wat u opslaat en hoe lang u dat doet; uw eigen juridisch adviseur bepaalt uw situatie, niet een README.
Wat het u kost, is identiteit over meerdere dagen. De rotatie van de salt betekent dat een persoon die op maandag en opnieuw op woensdag langskomt, als twee bezoekers wordt geteld. Dit is een bewuste keuze en er is geen omweg mogelijk. Dagelijkse unieke aantallen zijn betrouwbaar. Wekelijkse en maandelijkse unieke aantallen worden opgebouwd uit dagelijkse cijfers en zullen het bereik overschatten; elk cijfer voor "terugkerende bezoekers" over een langere periode meet dus niet wat de naam suggereert. Sessies en trajecten zijn betrouwbaar binnen één dag. Het roteren van ANONYMOUS_IDENTITY_SECRET heeft hetzelfde effect als een daggrens; beschouw die rotatie daarom als een wijziging in de data in plaats van als routineonderhoud.
De collector respecteert Do Not Track en Global Privacy Control, het browsersignaal dat een site vertelt om geen persoonlijke gegevens te verkopen of te delen. De script-tag bevat eigen schakelaars voor hetzelfde doel: data-respect-gpc, data-respect-dnt en data-require-consent, die alle verzameling tegenhouden totdat toestemming is verleend en het antwoord onthouden in localStorage onder de sleutel oa.consent. Het instellen van data-storage="none" schakelt browseropslag volledig uit.
Waarom de schijf na zes maanden volloopt
Dit is de voornaamste oorzaak van problemen bij een zelfgehoste analytics-server; de events zelf zijn meestal niet de reden.
Begin bij de images. Een release publiceert er tien, die samen ongeveer 13 GB op de schijf in beslag nemen. Een upgrade haalt de nieuwe generatie binnen voordat de oude wordt verwijderd, waardoor u tijdelijk twee generaties vasthoudt. Dat beslaat het grootste deel van de 25 GB vereiste opslagruimte, nog voordat er één paginaweergave is verwerkt.
Daarna volgen de snapshots. snapshot.sh stopt de stack, archiveert beide datavolumes inclusief alle secrets, en start de stack opnieuw. Alleen koude kopieën zijn hier veilig, omdat ClickHouse op de achtergrond onderdelen samenvoegt en een kopie die tijdens zo'n samenvoeging wordt gemaakt, niet consistent is. upgrade.sh maakt automatisch een snapshot voor elke upgrade, waardoor de archieven zich op dezelfde schijf ophopen totdat u een limiet instelt.
./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3Op een host die bijna zijn limiet heeft bereikt, verwijdert u de vorige generatie voordat u de upgrade uitvoert. Dit is veilig terwijl de stack draait, omdat images die actieve containers ondersteunen nog steeds worden gerefereerd:
docker image prune -a -fVervolgens de events zelf. ClickHouse comprimeert kolomgebaseerde data sterk, waardoor het volume aan ruwe events langzamer groeit dan de meeste mensen verwachten. De rollup-tabellen die het dashboard uitleest, zijn klein in vergelijking met de ruwe tabel. Meet in plaats van te gokken:
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhouseVoer voor het cijfer per tabel het volgende uit met de ClickHouse-inloggegevens die de generator onder infra/selfhost/env/ heeft geschreven:
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;Neem die meting in week één en opnieuw in week vier. Twee meetpunten geven u een groeipercentage, en een groeipercentage vertelt u wanneer het volume moet worden vergroot. De handleiding voor zelfhosting vermeldt per augustus 2026 geen instelling voor retentie of time-to-live voor ruwe events. Stem de schijfgrootte daarom af op uw gemeten groeisnelheid in plaats van ervan uit te gaan dat oude rijen automatisch verlopen.
Er is één valkuil bij het verwijderen die u moet kennen voordat deze problemen veroorzaakt. Het verwijderen van een site of account plaatst taken in de wachtrij voor de worker. Die worker heeft CLICKHOUSE_MAINTENANCE_USER en CLICKHOUSE_MAINTENANCE_PASSWORD nodig, met een bijbehorende oa_maintenance-gebruiker in ClickHouse. Zonder deze instellingen blijven de verwijderingstaken voor eeuwig in de wachtrij staan. De site verdwijnt uit het dashboard, maar elke rij blijft op de schijf staan, waardoor het lijkt alsof de opschoning is geslaagd terwijl er geen ruimte wordt vrijgemaakt.
Upgrades en de drie kosten
git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.shupgrade.sh toont drie kosten voordat de actie wordt uitgevoerd. Downtime is reëel: events die worden geprobeerd terwijl de collector offline is, gaan verloren, omdat de tracker deze niet opnieuw verstuurt. Een rollback leidt tot dataverlies, aangezien rollback.sh --to backups/<snapshot> beide stores volledig vervangt en elke rij verwijdert die is geschreven nadat de snapshot is gemaakt. Schijfruimte is de derde kostenpost, in de vorm van de eerder beschreven snapshot-stapel.
Twee regels voor het herstarten worden vaak verkeerd begrepen. Start de query gateway vóór de API, omdat een nieuwere API query-velden verstuurt die een oudere gateway afwijst. En ClickHouse vereist een recreate in plaats van een restart, omdat docker compose restart de oorspronkelijke omgeving van de container hergebruikt en uw wijzigingen stilzwijgend negeert:
docker compose up -d --force-recreate clickhouseHet dashboard bevat een vergelijkbare valkuil. De drie NEXT_PUBLIC_*-origins in env/web.env worden gecompileerd in de browser-bundle en vervangen wanneer de container start. Een dashboard dat de verkeerde hostname aanroept, wordt daarom hersteld met docker compose up -d --force-recreate web en nooit met restart. Het logbestand van de webcontainer toont de origins waarmee deze is gestart; dit is de snelste manier om te bevestigen dat de correctie is doorgevoerd.
Als ClickHouse weigert te starten na een configuratiewijziging, lees dan de eerste regel van het logbestand. Een regel die begint met oa-entrypoint: geeft aan dat de entrypoint een door u ingestelde waarde afwijst. Elke andere melding betekent meestal dat het configuratiebestand ongeldige XML bevat. De meest voorkomende oorzaak is een dubbel koppelteken binnen een XML-commentaar, wat daar niet is toegestaan.
AGPL-3.0 en de naam
De code is gelicentieerd onder de AGPL-3.0. Het ongewijzigd draaien van de code voor uw eigen sites creëert geen enkele publicatieverplichting. De verplichting begint wanneer u de code wijzigt en die gewijzigde versie als netwerkdienst aanbiedt: de licentie vereist dan dat u uw gewijzigde broncode aanbiedt aan de gebruikers van die dienst. Dit geldt voor het aanbieden van dashboards aan cliënten op uw instantie, en het geldt voor het bundelen ervan in een product dat u verkoopt. Het bijhouden van uw wijzigingen in een publieke fork voldoet aan deze eis zonder verdere procedures.
Het merk staat los van de code. De naam "OpenAnalytics" en het gehoste domein van het project identificeren de instantie die door de auteurs wordt beheerd, en zij maken geen deel uit van de licentieverlening. Uw implementatie draait de software zonder het merk te voeren, dus geef de dienst een eigen naam voordat u deze aanbiedt aan betalende klanten.
FAQ
Kan ik OpenAnalytics draaien op een 1 GB VPS?
Nee. Het project vereist ongeveer 4 GB RAM en 25 GB vrije schijfruimte, omdat één deployment zes applicatieservices naast Postgres, ClickHouse en twee Valkey-instanties draait. ClickHouse is op zichzelf geen licht proces. Op een 1 GB-server starten de containers, waarna de kernel out-of-memory killer er een beëindigt, meestal ClickHouse. Als een 1 GB-abonnement de harde beperking is, gebruik dan een tool die uit één binary bestaat, zoals GoatCounter, die op SQLite draait zonder externe database.
Heb ik een cookiebanner nodig bij OpenAnalytics?
Dat is een vraag voor uw jurist, al zijn de technische feiten in uw voordeel. Er wordt geen cookie geplaatst, de identiteit van de bezoeker is een salted hash die dagelijks roteert, en ruwe IP-adressen worden nooit opgeslagen; er wordt dus niets duurzaams geschreven om de bezoeker te identificeren. De AVG bepaalt nog steeds wat u opslaat en hoe lang u dit bewaart. Als u wilt dat verzameling expliciet wordt afgeschermd, stel dan data-require-consent in op de script-tag: de tracker verzamelt dan niets totdat toestemming is verleend en bewaart het antwoord in localStorage onder oa.consent.
Waarom geven events 202 terug, maar verschijnen ze nooit in het dashboard?
202 betekent dat de collector het event heeft geaccepteerd en in de wachtrij heeft geplaatst, niet dat het is opgeslagen. De worker verwerkt die wachtrij naar ClickHouse; een leeg dashboard bij succesvolle verzoeken wijst dus op een probleem met de worker. Lees docker compose logs --tail=50 worker en controleer de diepte van de Valkey-wachtrij. Een wachtrij die blijft groeien betekent dat de worker geblokkeerd is. De gebruikelijke oorzaken zijn onjuiste ClickHouse-inloggegevens in worker.env of een ontbrekend recht op een tabel die door een recente migratie is aangemaakt.
Waarom is het dashboard leeg terwijl elke container gezond is?
Controleer eerst AUTH_TRUSTED_ORIGINS in env/api.env. Dit moet exact overeenkomen met de dashboard-origin. Als dit niet het geval is, verstuurt de API geen CORS-headers, waardoor de browser elk verzoek weigert en u een werkende lay-out ziet zonder data. Het tweede punt om te controleren zijn de drie NEXT_PUBLIC_*-waarden in env/web.env, die worden ingevuld wanneer de web-container start. Het corrigeren hiervan vereist docker compose up -d --force-recreate web, omdat een gewone herstart de oude waarden behoudt.
Belet AGPL-3.0 mij om dit aan klanten aan te bieden?
Nee, het stelt één voorwaarde. Draai de code ongewijzigd en u bent niemand iets verschuldigd. Wijzigt u de code en draait u die gewijzigde versie als een dienst die anderen gebruiken, dan moet u die gebruikers uw gewijzigde broncode aanbieden, waar een publieke fork aan voldoet. Daarnaast is de naam "OpenAnalytics" niet samen met de code gelicentieerd, dus alles wat u verkoopt heeft een eigen naam nodig.