Auto-héberger OpenAnalytics sur un VPS
Avant l’installation, prévoyez 4 GB de RAM, 25 GB libres et quatre entrées DNS. Découvrez les services requis, puis ce qui finit par remplir le disque.
L’empreinte, avant la première étape
Pour auto-héberger OpenAnalytics, vous avez besoin d’un VPS Linux avec environ 4 GB de RAM, 25 GB d’espace disque libre, de Docker avec le plugin Compose et de quatre enregistrements DNS pointant déjà vers le serveur. C’est le point essentiel à connaître avant la première commande, et non après.
La stack comprend six services applicatifs et trois magasins de données. Postgres contient le plan de contrôle : comptes, sites, clés API et liens de partage. ClickHouse contient les événements bruts et les agrégats lus par le dashboard. Valkey est exécuté deux fois : une instance sert de file d’événements persistante, l’autre de cache que le système peut perdre, car ces deux usages nécessitent des politiques d’éviction opposées. Un seul processus, la passerelle de requêtes, peut lire ClickHouse. Il vérifie une signature Ed25519 sur chaque enveloppe de requête avant de l’exécuter.
Si vous cherchez un binaire et un seul fichier de configuration, ce n’est pas la bonne solution. GoatCounter est l’option à binaire unique dans cette catégorie : un exécutable Go, SQLite par défaut et aucune base de données externe. Cette stack plus lourde apporte les funnels, les web vitals, l’attribution des revenus à partir de votre propre compte Stripe et un serveur MCP (model context protocol). Choisir entre les outils d’analytics auto-hébergés est l’article qui examine ce compromis. Ce guide part du principe que vous avez déjà fait votre choix.
Pointez d’abord les enregistrements DNS vers le serveur
Les quatre sous-domaines doivent résoudre vers l’adresse IP publique du serveur avant toute autre étape, car Caddy demande les certificats Let’s Encrypt lors de son premier démarrage. Le challenge échoue si le nom ne se résout pas encore.
app.example.comsert le dashboard.api.example.comsert l’API et les callbacks OAuth.c.example.comsert le collector et le script tracker.rt.example.comsert le flux realtime.
Utilisez quatre enregistrements A, ou un enregistrement A et trois enregistrements CNAME qui pointent vers celui-ci. Vérifiez le résultat avec dig +short app.example.com avant de continuer. Un nom ajouté il y a une minute peut encore être mis en cache comme NXDOMAIN par le resolver utilisé par Let’s Encrypt. Si la première demande de certificat échoue, attendez et consultez les logs Caddy. Relancer l’installation n’accélère pas la propagation DNS.
Héberger OpenAnalytics avec Docker Compose
Récupérez une release taguée. La branche par défaut sert au développement, tandis qu’un tag de release correspond aux images publiées. Les commandes ci-dessous supposent que Docker et le plugin Compose sont déjà installés. Le guide exécuter des services Docker Compose sur un VPS couvre ce point.
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 -dLe paramètre sed '/-/d' de la commande de checkout exclut les tags de préversion. Vous utilisez ainsi la version stable la plus récente plutôt qu’une release candidate. --with-geoip récupère la base de données des villes DB-IP pendant la génération. Si vous l’omettez, chaque événement contient un pays nul et la vue géographique reste entièrement vide. Vous pouvez l’ajouter plus tard en exécutant infra/selfhost/geoip/fetch-dbip.sh, en définissant GEOIP_DB_PATH=/geoip/dbip-city-lite.mmdb dans env/collector.env, puis en recréant le collector avec docker compose up -d --force-recreate collector. Cette base de données est mise à jour chaque mois. Répétez donc la récupération chaque mois pour éviter que les données de villes ne deviennent obsolètes.
Sauvegardez les secrets générés avant de continuer
Le générateur écrit trois éléments. .env contient les noms de domaine et les références d’images. env/*.env contient un fichier de secrets par service. docker-compose.override.yml contient trois paires de clés Ed25519 sous forme de scalaires YAML multilignes, car un PEM multiligne ne peut pas être stocké dans un fichier env. Tous ces fichiers sont ignorés par Git et aucune de leurs valeurs ne peut être régénérée à l’identique.
Copiez ces fichiers hors de la machine maintenant. La perte de chaque élément a une conséquence précise :
- Si vous perdez les mots de passe des stores, vous n’avez plus accès à Postgres et ClickHouse. Vous ne pouvez les réinitialiser que depuis l’intérieur des conteneurs.
- Si vous perdez
OA_CREDENTIAL_KEYRING, aucun identifiant tiers enregistré ne peut être récupéré. Toute personne ayant connecté un compte Stripe doit donc le connecter de nouveau. - Si vous perdez
ANONYMOUS_IDENTITY_SECRET, l’identité des visiteurs est recalculée : les visiteurs d’hier sont tous comptabilisés comme nouveaux, et la rupture est visible dans les graphiques. - Si vous perdez
AUTH_SECRET, toutes les sessions sont invalidées et tout le monde doit se connecter de nouveau. - Si vous perdez une clé privée de signature, faites une rotation de la paire. Rien n’est perdu.
Deux secrets doivent être identiques octet par octet dans deux fichiers chacun. ANONYMOUS_IDENTITY_SECRET apparaît dans collector.env et worker.env, car le collector calcule le hash du visiteur et le worker l’écrit. OA_CREDENTIAL_KEYRING apparaît dans api.env et worker.env. Tous les autres secrets sont volontairement limités à un seul service, et un service auquel est transmis un secret qu’il ne doit pas détenir s’arrête au lieu de démarrer.
Démarrez la stack et vérifiez-la
grep OA_IMAGE .env
docker compose pull
docker compose up -d
docker compose logs -f migrate
docker compose psmigrate applique les schémas Postgres et ClickHouse, puis se termine. Un conteneur migrate arrêté est donc l’état final attendu. tracker-build compile oa.js dans un volume servi par Caddy, puis se termine également. Tous les autres services doivent afficher healthy dans docker compose ps. Un service qui redémarre en boucle échoue presque toujours lors de la validation de l’environnement. Le journal affiche tous les problèmes dans une seule liste, au lieu d’en afficher un à chaque redémarrage. Les deux causes habituelles sont une variable laissée vide, qui est rejetée au lieu d’être traitée comme absente, et un secret placé dans le mauvais fichier de service.
Sur arm64 ou depuis une branche, aucune image n’est publiée. Vous devez donc construire localement avec docker compose up -d --build. Un hôte doté de 4 GB de mémoire arrive à court de mémoire pendant cette construction. Ajoutez d’abord du swap. Il est nécessaire uniquement pendant la construction :
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabLa construction prend environ dix minutes. Le téléchargement prend quelques minutes, ce qui explique l’existence des images de release.
Réclamez immédiatement le premier compte
Ouvrez https://app.example.com. Un déploiement auquel personne ne s’est encore connecté n’affiche pas de formulaire de connexion : il propose de créer le premier compte. Ce compte est définitivement le compte privilégié, et c’est le seul qui peut afficher l’écran des paramètres du déploiement. Une fois ce compte créé, la route renvoie 409. Personne ne peut donc accéder à l’interface après vous. Faites-le dès que la stack est opérationnelle, pas la semaine suivante.
Installer le tracker
Ajoutez un site dans le dashboard pour obtenir le tag. Sa forme est fixe :
<script
async
src="https://c.example.com/oa.js"
data-key="YOUR_TRACKING_KEY"
data-collector="https://c.example.com"
></script>Placez-le dans le head de la page. La clé de tracking est publique par conception. Elle doit donc figurer dans votre HTML, où n’importe qui peut la lire. Le script installe window.oa. Les appels comme oa("track", ...) sont placés en file d’attente par un stub, puis exécutés une fois le fichier chargé. Un événement personnalisé déclenché tôt n’est donc pas perdu. Si un autre élément de la page utilise déjà window.oa, le tracker s’installe à la place sous window.openanalytics. Si le même site répond aussi comme service onion, n’incluez pas le tag dans cette version. Un script chargé depuis c.example.com ramène un visiteur de Tor Browser vers le clearnet et associe les deux adresses lors du même chargement de page.
Vérifiez ensuite l’ensemble du chemin, de bout en bout :
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 batchLa première commande doit afficher 200 et quelques kilo-octets. Chargez une page de votre site, puis recherchez une ligne de batch dans le log du worker dans les secondes qui suivent. Le collector répond 202 dès qu’il accepte un événement. 202 signifie qu’il est mis en file d’attente, pas qu’il est stocké. Le worker transfère les événements dans ClickHouse. Si des événements sont acceptés, mais que rien n’apparaît dans le dashboard, le worker est bloqué. Une profondeur de file Valkey qui continue d’augmenter le confirme. Les causes habituelles sont des identifiants ClickHouse incorrects dans worker.env ou un grant manquant sur une table ajoutée par une migration récente.
Laissez le collecteur public et protégez le tableau de bord par une authentification
Caddy est inclus dans le fichier compose et obtient lui-même les certificats pour les 4 noms. Le chemin par défaut ne nécessite donc aucune configuration de proxy de votre part. Si le serveur exécute déjà un reverse proxy Nginx, placez plutôt la stack derrière le infra/selfhost/nginx.conf.example fourni et conservez sa gestion des en-têtes :
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 "";Le collecteur calcule le hash quotidien des visiteurs à partir de l’adresse IP du client. Il doit donc récupérer cette adresse depuis la connexion, jamais depuis un en-tête. Transmettre CF-Connecting-IP depuis un relais non approuvé permet à n’importe quel appelant de déclarer n’importe quelle adresse. Cela fausse la géolocalisation et gonfle les nombres de visiteurs en même temps.
L’accès se sépare clairement selon le nom d’hôte. c. et rt. doivent être accessibles à tous les visiteurs de tous les sites que vous mesurez. Ne placez donc jamais d’authentification basic ou de liste d’autorisation IP devant ces 2 noms. app. et api. doivent seulement être accessibles aux personnes qui se connectent. L’authentification intégrée à l’application protège le tableau de bord : la connexion par mot de passe est activée par défaut via AUTH_PASSWORD_SIGNIN=enabled dans env/api.env, et les boutons Google ou GitHub apparaissent uniquement si l’identifiant client et le secret client existent pour le fournisseur concerné. Les magic links nécessitent un transport mail. Sans celui-ci, l’API écrit uniquement le message à envoyer dans une outbox : rien n’est remis et aucune erreur n’est signalée. Si vos autres applications auto-hébergées utilisent déjà une connexion Authentik unique, déterminez rapidement si ce tableau de bord doit les rejoindre ou conserver ses propres comptes, car le premier compte que vous créez ici reste définitivement le compte privilégié.
Un seul réglage détermine si le tableau de bord fonctionne. AUTH_TRUSTED_ORIGINS dans env/api.env doit correspondre exactement à l’origine du tableau de bord. S’il est incorrect ou absent, l’API n’émet aucun en-tête CORS (cross-origin resource sharing), le navigateur refuse tous les appels et le tableau de bord affiche sa mise en page sans aucune donnée, alors que docker compose ps indique que tout fonctionne correctement.
Profitez de la configuration du proxy pour traiter le trafic automatisé. Les crawlers atteignent le collecteur comme n’importe quel autre visiteur, et leurs pages vues sont enregistrées dans ClickHouse et dans vos statistiques. Bloquer les crawlers IA sur le serveur permet d’en exclure une partie de la base de données avant qu’ils ne dégradent à la fois la précision des données et l’espace disque.
Ce que signifie l’absence de cookies ici et ce qu’elle vous coûte
Il n’y a pas de cookie. L’identité du visiteur est un hash salé. Le salt change chaque jour et les adresses IP brutes ne sont jamais stockées. La géolocalisation est déterminée localement à partir du fichier DB-IP présent sur votre propre disque. Ainsi, aucune requête concernant un visiteur ne quitte jamais l’hôte. Conserver les requêtes localement supprime le fournisseur, pas les données. C’est la même limite que lorsque vous exécutez votre propre instance SearXNG et que l’adresse IP de votre serveur devient celle que voient les moteurs de recherche.
Vous évitez ainsi de conserver un identifiant sur l’appareil du visiteur. C’est précisément ce qui soumet un tracker aux règles européennes ePrivacy relatives au consentement. C’est pourquoi les configurations limitées aux données agrégées, comme celle-ci, sont généralement utilisées sans bandeau de consentement. Le RGPD s’applique toujours aux données que vous stockez et à leur durée de conservation. Votre conseil juridique détermine votre situation, pas un README.
En contrepartie, vous perdez l’identification d’un jour à l’autre. À cause de la rotation du salt, une personne qui visite le site lundi puis mercredi est comptée comme 2 visiteurs, volontairement et sans solution de contournement. Les décomptes des visiteurs uniques quotidiens sont fiables. Les décomptes hebdomadaires et mensuels sont calculés à partir des décomptes quotidiens et surestiment la portée. Toute mesure des « visiteurs de retour » sur une longue période ne mesure donc pas ce que son libellé indique. Les sessions et les parcours sont fiables à l’intérieur d’une même journée. La rotation de ANONYMOUS_IDENTITY_SECRET produit le même effet qu’une limite entre deux jours. Considérez donc cette rotation comme une modification des données, et non comme une simple opération d’entretien.
Le collector respecte Do Not Track et Global Privacy Control, le signal du navigateur qui indique à un site de ne pas vendre ni partager de données personnelles. La balise script possède ses propres options pour le même objectif : data-respect-gpc, data-respect-dnt et data-require-consent. Cette dernière suspend toute collecte jusqu’à l’obtention du consentement et mémorise la réponse dans localStorage, sous la clé oa.consent. Définir data-storage="none" désactive entièrement le stockage dans le navigateur.
Pourquoi le disque se remplit au bout de six mois
C’est ce qui met à genoux un serveur d’analytics auto-hébergé, et les événements n’en sont généralement pas la cause.
Commencez par les images. Une release en publie 10, pour environ 13 GB sur disque. Lors d’une mise à niveau, la nouvelle génération est téléchargée avant la suppression de l’ancienne. Vous conservez donc deux générations pendant un certain temps. Cela représente déjà l’essentiel des 25 GB requis, avant même l’arrivée de la moindre page vue.
Viennent ensuite les snapshots. snapshot.sh arrête la stack, archive les 2 volumes de données ainsi que tous les secrets, puis la redémarre. Ici, seules les copies à froid sont fiables, car ClickHouse fusionne des parts en arrière-plan et une copie prise pendant une fusion n’est pas cohérente. upgrade.sh en crée automatiquement un avant chaque mise à niveau. Les archives s’accumulent donc sur le même disque jusqu’à ce que vous les plafonniez.
./snapshot.sh create --label before-something-risky
./snapshot.sh list
./snapshot.sh --keep 3Sur un hôte proche de la limite, récupérez l’espace occupé par la génération précédente avant la mise à niveau. Cette opération est sûre tant que la stack fonctionne, car les images utilisées par les conteneurs en cours d’exécution restent référencées :
docker image prune -a -fViennent ensuite les événements eux-mêmes. ClickHouse compresse fortement les données en colonnes. Le volume brut d’événements augmente donc plus lentement que la plupart des utilisateurs ne le prévoient, et les tables de rollup lues par le dashboard sont petites par rapport à la table brute. Mesurez au lieu de deviner :
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhousePour obtenir la taille de chaque table, exécutez cette commande avec les identifiants ClickHouse écrits par le générateur sous 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;Effectuez cette mesure pendant la première semaine, puis de nouveau pendant la quatrième. 2 points suffisent pour calculer un taux de croissance, et ce taux vous indique quand redimensionner le volume. En août 2026, le guide d’auto-hébergement ne documente aucun paramètre de rétention ou de time-to-live pour les événements bruts. Dimensionnez donc le disque à partir du taux mesuré, au lieu de supposer que les anciennes lignes expirent automatiquement.
Un piège lié à la suppression mérite d’être connu avant qu’il ne pose problème. La suppression d’un site ou d’un compte place une tâche dans la file du worker. Ce worker nécessite que CLICKHOUSE_MAINTENANCE_USER et CLICKHOUSE_MAINTENANCE_PASSWORD soient définis, ainsi qu’un utilisateur oa_maintenance correspondant dans ClickHouse. Sans ces éléments, la suppression reste indéfiniment dans la file. Le site disparaît du dashboard, mais toutes les lignes restent sur le disque. Vous avez donc l’apparence d’un nettoyage, sans récupérer le moindre espace.
Mises à niveau et trois coûts
git fetch --tags
git checkout "$(git tag -l 'v*' --sort=-v:refname | sed '/-/d' | head -1)"
cd infra/selfhost
./upgrade.shupgrade.sh affiche trois coûts avant d’agir. L’indisponibilité est réelle : les événements envoyés pendant l’arrêt du collector sont perdus, car le tracker ne les réessaie pas. Un rollback entraîne une perte de données, car rollback.sh --to backups/<snapshot> remplace entièrement les deux stockages et supprime toutes les lignes écrites après la création de ce snapshot. Le disque est le troisième coût : il s’agit de l’empilement de snapshots décrit plus haut.
Deux règles de redémarrage sont faciles à appliquer incorrectement. Démarrez la query gateway avant l’API, car une API plus récente envoie des champs de requête qu’une gateway plus ancienne rejette. ClickHouse doit être recréé plutôt que redémarré, car docker compose restart réutilise l’environnement initial du conteneur et ignore silencieusement votre modification :
docker compose up -d --force-recreate clickhouseLe dashboard présente le même piège. Les trois origines NEXT_PUBLIC_* dans env/web.env sont intégrées au bundle du navigateur et remplacées au démarrage du conteneur. Un dashboard qui appelle le mauvais hostname se corrige donc avec docker compose up -d --force-recreate web, jamais avec restart. Le log du conteneur web affiche les origines utilisées au démarrage. C’est le moyen le plus rapide de vérifier que la correction a bien été prise en compte.
Si ClickHouse refuse de démarrer après une modification de configuration, lisez la première ligne de son log. Une ligne qui commence par oa-entrypoint: indique que l’entrypoint rejette une valeur que vous avez définie. Dans les autres cas, le fichier de configuration contient généralement un XML invalide. La cause la plus fréquente est un double tiret dans un commentaire XML, ce qui est interdit.
AGPL-3.0 et le nom
Le code est distribué sous licence AGPL-3.0. L’exécuter sans modification pour vos propres sites ne crée aucune obligation de publication. L’obligation commence lorsque vous modifiez le code et exécutez cette version modifiée comme service réseau : la licence vous impose alors de proposer le code source modifié aux utilisateurs de ce service. Cela couvre la mise à disposition de tableaux de bord aux clients sur votre instance, ainsi que l’intégration du logiciel dans un produit que vous commercialisez. Conserver vos modifications dans un fork public suffit à remplir cette obligation, sans autre démarche.
La marque est distincte du code. Le nom « OpenAnalytics » et le domaine hébergé du projet identifient l’instance exploitée par ses auteurs ; ils ne font pas partie des droits accordés par la licence. Votre déploiement exécute le logiciel sans reprendre la marque. Donnez donc un nom propre au service avant de le proposer à des clients payants.
FAQ
Puis-je exécuter OpenAnalytics sur un VPS de 1 GB ?
Non. Le projet demande environ 4 GB de RAM et 25 GB d’espace disque libre, car un déploiement exécute six services applicatifs en plus de Postgres, ClickHouse et deux instances Valkey. ClickHouse n’est pas un processus léger. Sur une machine de 1 GB, les conteneurs démarrent, puis l’oom-killer du kernel en arrête généralement un, le plus souvent ClickHouse. Si une offre de 1 GB est une contrainte stricte, utilisez un outil monolithique comme GoatCounter, qui fonctionne avec SQLite sans base de données externe.
Ai-je besoin d’une bannière de cookies avec OpenAnalytics ?
Cette question relève de votre avocat, mais les faits techniques vous sont favorables. Il n’y a pas de cookie, l’identité du visiteur est un hash salé qui change chaque jour et les adresses IP brutes ne sont jamais stockées. Aucune donnée durable n’est donc écrite pour identifier le visiteur. Le RGPD encadre néanmoins les données que vous stockez et la durée de leur conservation. Si vous voulez conditionner explicitement la collecte au consentement, définissez data-require-consent sur la balise script : le tracker ne collecte alors rien tant que le consentement n’est pas accordé et conserve la réponse dans localStorage sous oa.consent.
Pourquoi les événements renvoient-ils 202 sans jamais apparaître dans le dashboard ?
202 signifie que le collector a accepté et mis l’événement en file d’attente, pas qu’il l’a stocké. Le worker vide cette file dans ClickHouse. Un dashboard vide malgré des requêtes réussies indique donc un problème au niveau du worker. Consultez docker compose logs --tail=50 worker et surveillez la profondeur de la file Valkey. Une file qui continue de grossir signifie que le worker est bloqué. Les causes habituelles sont des identifiants ClickHouse incorrects dans worker.env ou un grant manquant sur une table créée par une migration récente.
Pourquoi le dashboard est-il vide alors que tous les conteneurs sont sains ?
Vérifiez d’abord AUTH_TRUSTED_ORIGINS dans env/api.env. Cette valeur doit correspondre exactement à l’origine du dashboard. Sinon, l’API n’émet aucun en-tête CORS, le navigateur refuse chaque appel et vous voyez une interface fonctionnelle sans données. Vérifiez ensuite les trois valeurs NEXT_PUBLIC_* dans env/web.env. Elles sont substituées au démarrage du conteneur web. Pour les corriger, vous devez utiliser docker compose up -d --force-recreate web, car un simple redémarrage conserve les anciennes valeurs.
AGPL-3.0 m’empêche-t-elle de proposer ce logiciel à des clients ?
Non, mais cette licence impose une condition. Si vous exécutez le code sans le modifier, vous ne devez rien à personne. Si vous le modifiez et exécutez cette version modifiée comme un service utilisé par d’autres personnes, vous devez fournir votre code source modifié à ces utilisateurs. Un fork public suffit pour satisfaire cette obligation. Par ailleurs, le nom « OpenAnalytics » n’est pas concédé avec le code. Tout produit que vous vendez doit donc avoir son propre nom.