SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor

OpenAnalytics zelf hosten op een VPS: de vereisten

Ontdek de exacte systeemvereisten voor OpenAnalytics: 4 GB RAM, 25 GB schijfruimte en vier DNS-records. Leer hoe de stack met ClickHouse, Postgres en Valkey uw disk vult.

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 tegenstrijdige 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. Kiezen tussen zelf-gehoste analytics-tools 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 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.com bedient het dashboard.
  • api.example.com bedient de API en de OAuth-callbacks.
  • c.example.com bedient de collector en het tracker-script.
  • rt.example.com bedient 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 steeds als NXDOMAIN in de cache staan bij de resolver die Let's Encrypt gebruikt. Als een eerste certificaataanvraag mislukt, wacht dan even en controleer de Caddy-logs. 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 -d

De sed '/-/d' in de checkout-regel negeert 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 null-waarde voor het land, waardoor de geografische weergave leeg blijft. 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 per service één bestand met geheimen. docker-compose.override.yml bevat drie Ed25519-sleutelparen als YAML-blok-scalars, 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:

  • Bij verlies van de wachtwoorden voor de opslag bent u buitengesloten van Postgres en ClickHouse; deze zijn alleen vanuit de containers te resetten.
  • Bij verlies van OA_CREDENTIAL_KEYRING zijn alle opgeslagen inloggegevens van derden onherstelbaar; iedereen die een Stripe-account heeft gekoppeld, moet dit opnieuw doen.
  • Bij verlies van ANONYMOUS_IDENTITY_SECRET worden de identiteiten van bezoekers opnieuw vastgesteld: bezoekers van gisteren tellen allemaal als nieuw en deze onderbreking is zichtbaar in de grafieken.
  • Bij verlies van AUTH_SECRET wordt elke sessie ongeldig gemaakt, waardoor iedereen opnieuw moet inloggen.
  • Bij verlies van een private signing key 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 hash van de bezoeker 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 ps

migrate 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 bijna altijd bij de validatie van de omgevingsvariabelen; het logbestand toont alle problemen 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 als 'niet ingesteld' behandeld) 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 aan 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/fstab

Het bouwen duurt ongeveer tien minuten. Het ophalen van images duurt enkele minuten, wat de reden is dat er release-images bestaan.

Claim het eerste account onmiddellijk

Open https://app.example.com. Een implementatie waar nog niemand op is ingelogd, toont geen inlogformulier: in plaats daarvan wordt aangeboden 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 kan zien. Zodra dit account bestaat, geeft de route 409 als antwoord, zodat niemand na u nog toegang kan krijgen. Voer dit uit zodra de stack operationeel is, en niet pas een week later.

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 afgevuurde custom event niet verloren. Als iets anders op de pagina al eigenaar is van window.oa, installeert de tracker zichzelf als window.openanalytics.

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 batch

De eerste opdracht hoort 200 en enkele kilobytes te tonen. Laad een pagina op uw site en zoek binnen enkele seconden naar een batch-regel in het logbestand van de worker. 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 is geblokkeerd; een Valkey-wachtrij die blijft groeien bevestigt dit. De gebruikelijke oorzaken zijn onjuiste ClickHouse-inloggegevens in worker.env, of een ontbrekend recht 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 draait met een nginx reverse proxy, plaats dan de meegeleverde infra/selfhost/nginx.conf.example voor de stack 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 als zowel de client ID als de client secret voor die provider aanwezig zijn. Magic links vereisen een mailtransport; zonder dit schrijft de API de verzending enkel naar een outbox, waardoor er niets wordt afgeleverd en er geen foutmeldingen optreden.

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 (cross-origin resource sharing) headers, weigert de browser elk verzoek en krijgt u een dashboard dat wel de lay-out laadt maar geen data toont, terwijl docker compose ps rapporteert dat alles in orde is.

Nu u toch in de proxy-configuratie werkt, kunt u geautomatiseerd verkeer aanpakken. Crawlers benaderen de collector net als elke andere bezoeker, en hun pageviews belanden in ClickHouse en in uw statistieken. Het blokkeren van AI-crawlers op de server houdt een deel daarvan uit de database voordat het ten koste gaat van zowel de nauwkeurigheid als de schijfruimte.

Wat cookieless hier betekent en wat het u kost

Er wordt geen cookie gebruikt. De identiteit van een bezoeker is een salted hash, de salt roteert dagelijks 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 informatie over een bezoeker de host verlaat.

Wat dit u oplevert, is de afwezigheid van een identificatiecode die op het apparaat van de bezoeker wordt opgeslagen; dit is precies het aspect 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 is nog steeds van toepassing op wat u opslaat en hoe lang u dat doet; uw eigen juridisch adviseur bepaalt uw situatie, niet een README.

Wat het u kost, is identiteitsherkenning over meerdere dagen. De salt-rotatie betekent dat een persoon die op maandag en opnieuw op woensdag langskomt, als twee bezoekers wordt geteld; dit is een bewuste keuze waarvoor geen workaround bestaat. Dagelijkse unieke aantallen zijn accuraat. Wekelijkse en maandelijkse unieke aantallen worden opgebouwd uit dagelijkse gegevens en zullen het bereik overschatten, waardoor elk cijfer voor "terugkerende bezoekers" over een langere periode niet meet wat de naam suggereert. Sessies en trajecten zijn betrouwbaar binnen één dag. Het roteren van ANONYMOUS_IDENTITY_SECRET heeft hetzelfde effect als een daggrens, dus beschouw die rotatie 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 tegenhoudt totdat toestemming is verleend en het antwoord onthoudt 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 reden waarom een zelfgehoste analytics-server uitvalt; de gebeurtenissen zelf zijn meestal niet de oorzaak.

Begin bij de images. Een release publiceert er tien, wat neerkomt op ongeveer 13 GB aan schijfruimte. 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 vereiste 25 GB, nog voordat er één paginaweergave is binnengekomen.

Dan de snapshots. snapshot.sh stopt de stack, archiveert beide datavolumes inclusief alle geheimen, en start opnieuw op. Koude kopieën zijn hier de enige veilige methode, 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 kopie voor elke upgrade, waardoor de archieven zich op dezelfde schijf ophopen totdat u er een limiet op stelt.

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

Op een host die bijna zijn limiet heeft bereikt, kunt u de vorige generatie verwijderen 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 -f

Vervolgens de gebeurtenissen zelf. ClickHouse comprimeert kolomgebaseerde data sterk, waardoor het volume aan ruwe gebeurtenissen langzamer groeit dan de meeste mensen verwachten. De rollup-tabellen die het dashboard leest, 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/clickhouse

Voor het cijfer per tabel voert u dit 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 bevat per augustus 2026 geen instelling voor retentie of time-to-live voor ruwe gebeurtenissen, dus stem de schijfgrootte af op uw gemeten groei in plaats van aan te nemen dat oude rijen vanzelf verlopen.

Er is één valkuil bij het verwijderen die u moet kennen voordat u er last van krijgt. Het verwijderen van een site of account plaatst taken in de wachtrij voor de worker, en 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 er is opgeschoond 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.sh

upgrade.sh toont drie kosten voordat het actie onderneemt. 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. ClickHouse vereist een recreate in plaats van een herstart, omdat docker compose restart de oorspronkelijke omgeving van de container hergebruikt en uw wijzigingen stilzwijgend negeert:

docker compose up -d --force-recreate clickhouse

Het 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 het 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: duidt erop dat de entrypoint een door u ingestelde waarde afwijst. Iets anders 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 netwerkservice draait: de licentie vereist dan dat u uw gewijzigde broncode aanbiedt aan de gebruikers van die service. Dit geldt voor het aanbieden van dashboards aan klanten 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 service 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 implementatie zes applicatieservices draait naast Postgres, ClickHouse en twee Valkey-instanties. ClickHouse is op zichzelf geen licht proces. Op een 1 GB-server starten de containers wel, maar de out-of-memory killer van de kernel beëindigt vervolgens een van de processen, meestal ClickHouse. Als een 1 GB-abonnement de harde beperking is, gebruik dan een tool die uit één enkel binary-bestand bestaat, zoals GoatCounter, dat op SQLite draait zonder externe database.

Heb ik een cookiebanner nodig bij OpenAnalytics?

Dat is een vraag voor uw jurist, waarbij de technische feiten in uw voordeel spreken. Er worden geen cookies gebruikt, de identiteit van de bezoeker is een salted hash die dagelijks roteert, en ruwe IP-adressen worden nooit opgeslagen; er wordt dus niets duurzaams weggeschreven om de bezoeker te identificeren. De AVG (GDPR) bepaalt nog steeds wat u opslaat en hoe lang u dit bewaart. Als u wilt dat gegevensverzameling expliciet wordt goedgekeurd, stel dan data-require-consent in op de script-tag: de tracker verzamelt dan niets totdat toestemming is verleend en slaat het antwoord op in localStorage onder oa.consent.

Waarom geven events een 202-statuscode 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 monitor de lengte 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 ontbrekende rechten 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 origin van het dashboard. 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 webcontainer start. Het corrigeren hiervan vereist docker compose up -d --force-recreate web, omdat een eenvoudige herstart de oude waarden behoudt.

Belet de AGPL-3.0 mij om dit aan klanten aan te bieden?

Nee, er is slechts één voorwaarde verbonden aan de licentie. Draait u de code ongewijzigd, dan bent u 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 in licentie gegeven samen met de code; alles wat u verkoopt, heeft dus een eigen naam nodig.