Auto-héberger OpenAnalytics sur un VPS : prérequis
Avant l’installation, vérifiez les vrais prérequis : ClickHouse, Postgres, Valkey, 4 GB de RAM, 25 GB libres et quatre enregistrements DNS.
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 : les comptes, les sites, les clés API et les 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 rôles 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 cherchiez un seul 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 tunnels de conversion, les métriques Web Vitals, l’attribution des revenus depuis 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 ce 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 lancement. Le challenge échoue si le nom ne 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 de tracking.rt.example.comsert le flux temps réel.
Utilisez quatre enregistrements A, ou un enregistrement A et trois 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, puis consultez les logs Caddy. Relancer l’installation n’accélère pas la propagation DNS.
Comment auto-héberger OpenAnalytics avec Docker Compose
Récupérez une release marquée par un tag. La branche par défaut sert au développement, tandis qu’un tag de release correspond aux images effectivement 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 sed '/-/d' de la commande de checkout exclut les tags de pré-release. Vous obtenez ainsi la version stable la plus récente plutôt qu’une release candidate. --with-geoip récupère la base de données géographique DB-IP pendant la génération. Si vous omettez cette étape, chaque événement contient un pays null et la vue géographique reste complètement vide. Vous pouvez l’ajouter ultérieurement 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 actualisée chaque mois. Répétez donc la récupération chaque mois, sinon vos données géographiques deviennent obsolètes.
Sauvegardez les secrets générés avant d’aller plus loin
Le générateur écrit trois éléments. .env contient les noms de domaine et les références d’image. 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 fichier PEM multiligne ne peut pas être stocké dans un fichier env. Tous ces fichiers sont exclus de Git, et aucune de ces données ne peut être régénérée à l’identique.
Copiez maintenant ces fichiers hors de la machine. La perte de chaque élément a une conséquence précise :
- Si vous perdez les mots de passe des stores, vous ne pouvez plus accéder à Postgres et ClickHouse. Leur réinitialisation est possible uniquement depuis l’intérieur des conteneurs.
- Si vous perdez
OA_CREDENTIAL_KEYRING, tous les identifiants tiers enregistrés sont irrécupérables. Toute personne ayant connecté un compte Stripe doit donc le connecter à nouveau. - Si vous perdez
ANONYMOUS_IDENTITY_SECRET, l’identité des visiteurs est recalculée à partir d’une nouvelle base. Les visiteurs d’hier sont tous comptés comme de nouveaux visiteurs, et la rupture est visible dans les graphiques. - Si vous perdez
AUTH_SECRET, toutes les sessions sont invalidées. Tout le monde doit donc se reconnecter. - Si vous perdez une clé privée de signature, faites tourner la paire. Aucune donnée n’est perdue.
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 on transmet 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 s’arrête. Un conteneur migrate arrêté est donc l’état final attendu. tracker-build compile oa.js dans un volume servi par Caddy, puis s’arrête é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 par redémarrage. Les deux causes habituelles sont une variable laissée vide, qui est rejetée au lieu d’être traitée comme non définie, 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 effectuer la build localement avec docker compose up -d --build. Un hôte doté de 4 GB de mémoire arrive à court de mémoire pendant cette build. Ajoutez d’abord du swap. Il est nécessaire uniquement pendant la build :
fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstabLa build prend environ dix minutes. Le pull 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é. C’est le seul compte qui peut accéder à l’écran des paramètres du déploiement. Une fois ce compte créé, la route répond 409. Personne ne peut donc passer 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 la section 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 dans une file d’attente par un stub, puis exécutés lorsque le fichier est chargé. Un événement personnalisé déclenché plus 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.
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 les journaux du worker dans les secondes qui suivent. Le collector renvoie 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 qu’aucune donnée 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.
Conserver le collecteur public et protéger le dashboard par 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 la machine 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 statistiques de visiteurs en même temps.
L’accès se divise 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’adresses IP autorisées devant ces deux noms. app. et api. doivent uniquement être accessibles aux personnes qui se connectent. L’authentification intégrée à l’application protège le dashboard : 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 de messagerie. Sans celui-ci, l’API écrit seulement le message à envoyer dans une outbox : rien n’est distribué et aucune erreur n’est générée.
Un paramètre détermine à lui seul si le dashboard fonctionne. AUTH_TRUSTED_ORIGINS dans env/api.env doit correspondre exactement à l’origine du dashboard. Si cette valeur est incorrecte ou absente, l’API n’émet aucun en-tête CORS (cross-origin resource sharing), le navigateur refuse tous les appels et le dashboard affiche sa mise en page sans aucune donnée, tandis que docker compose ps indique que tout fonctionne correctement.
Pendant que vous modifiez la configuration du proxy, traitez aussi le trafic automatisé. Les crawlers utilisent le collecteur comme n’importe quel autre client. Leurs pages vues sont donc enregistrées dans ClickHouse et intégrées à vos statistiques. Bloquer les crawlers d’IA au niveau du serveur permet d’en exclure une partie de la base de données avant qu’ils ne nuisent à 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 sel est renouvelé 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. Aucune recherche concernant un visiteur ne quitte donc l’hôte.
Cela vous évite de conserver un identifiant sur l’appareil du visiteur. C’est précisément cet élément qui soumet un tracker aux règles européennes ePrivacy relatives au consentement. Les configurations limitées aux données agrégées, comme celle-ci, sont généralement utilisées sans bannière de consentement pour cette raison. Le RGPD s’applique toujours aux données que vous stockez et à leur durée de conservation. C’est votre conseil juridique qui évalue votre situation, pas un README.
Le coût est l’absence d’identité interjournalière. Comme le sel est renouvelé, une personne qui visite le site lundi puis mercredi est comptée comme deux visiteurs. C’est le comportement prévu et il n’existe aucun moyen de le contourner. Les nombres de visiteurs uniques par jour sont fiables. Les nombres de visiteurs uniques par semaine et par mois sont calculés à partir des données quotidiennes et surestiment donc la portée. Toute statistique de « visiteur revenant » calculée sur une longue période ne mesure donc pas ce que son intitulé 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 journalière. Considérez donc cette rotation comme une modification des données, et non comme une opération de maintenance courante.
Le collecteur respecte Do Not Track et Global Privacy Control, le signal du navigateur qui demande à un site de ne pas vendre ni partager les données personnelles. La balise de script possède ses propres options pour le même objectif : data-respect-gpc, data-respect-dnt et data-require-consent. Cette dernière bloque 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 généralement hors service 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 dix, pour environ 13 GB sur le disque. Lors d’une mise à niveau, la nouvelle génération est téléchargée avant la suppression de l’ancienne. Pendant un certain temps, deux générations sont donc présentes. Cela représente 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 deux volumes de données avec tous les secrets, puis redémarre. Les copies à froid sont les seules sûres ici, car ClickHouse fusionne des parts en arrière-plan et une copie effectuée 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 limitiez.
./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 sont toujours 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 des événements augmente donc plus lentement que prévu, et les tables d’agrégation lues par le dashboard sont petites par rapport à la table brute. Mesurez au lieu d’estimer :
docker system df -v
docker compose exec clickhouse df -h /var/lib/clickhousePour obtenir la taille par 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 à nouveau pendant la quatrième. Deux mesures suffisent pour calculer un taux de croissance, qui indique quand le volume doit être augmenté. 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é, sans 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 met une tâche en file d’attente pour le 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 en file d’attente. 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 collecteur sont perdus, car le tracker ne réessaie pas de les envoyer. Le rollback entraîne une perte de données, car rollback.sh --to backups/<snapshot> remplace entièrement les deux stores et supprime chaque ligne écrite après la création de ce snapshot. Le disque constitue le troisième coût : il s’agit de l’accumulation de snapshots décrite 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 d’origine 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 compilées dans le bundle du navigateur et remplacées au démarrage du conteneur. Un dashboard qui utilise le mauvais hostname se corrige donc avec docker compose up -d --force-recreate web, et jamais avec restart. Le log du conteneur web affiche les origines utilisées à son 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 du XML invalide. La cause la plus fréquente est la présence d’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. Si vous l’exécutez sans modification pour vos propres sites, aucune obligation de publication ne s’applique. 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 inclut la mise à disposition de dashboards 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, mais ne font pas partie des droits accordés par la licence. Votre déploiement exécute le logiciel sans reprendre la marque. Donnez donc au service son propre nom 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 de deux instances Valkey. ClickHouse n’est pas un petit processus. Sur une machine de 1 GB, les conteneurs démarrent, puis l’out-of-memory killer du kernel en arrête généralement un, le plus souvent ClickHouse. Si un forfait de 1 GB est une contrainte stricte, utilisez un outil à binaire unique tel que 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 régit néanmoins les données que vous stockez et la durée de conservation. Si vous voulez conditionner explicitement la collecte au consentement, définissez data-require-consent sur la balise de 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é l’événement et l’a placé dans la queue, pas qu’il l’a stocké. Le worker vide cette queue 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 queue Valkey. Une queue qui continue de grossir signifie que le worker est bloqué. Les causes habituelles sont de mauvais identifiants ClickHouse 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 opérationnels ?
Vérifiez d’abord AUTH_TRUSTED_ORIGINS dans env/api.env. Cette valeur doit correspondre exactement à l’origine du dashboard. Si ce n’est pas le cas, l’API n’émet aucun en-tête CORS. Le navigateur refuse alors tous les appels, et vous voyez une interface fonctionnelle sans données. Vérifiez ensuite les trois valeurs NEXT_PUBLIC_* dans env/web.env, qui sont substituées au démarrage du conteneur web. Pour les corriger, vous devez effectuer 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, elle impose une condition. Si vous exécutez le code sans modification, 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 à remplir cette obligation. Par ailleurs, le nom « OpenAnalytics » n’est pas concédé sous licence avec le code. Tout produit que vous vendez doit donc avoir son propre nom.