Certains liens de cette page sont des liens d'affiliation : si vous vous inscrivez via ces liens, ce site touche une commission, sans surcoût pour vous. Les prix et les recommandations ne changent pas.

Il y a onze jours, nous avons installé un serveur d’analytics respectueux de la vie privée à côté d’un site qui tournait déjà. Pas de nouvelle machine, pas de service managé, pas de bandeau cookies. Cela a pris une vingtaine de minutes, il consomme aujourd’hui 340 MB de RAM, et sa base de données pèse 9.5 MB.

Il a aussi enregistré 96 pageviews en onze jours, alors que le serveur web placé devant lui a journalisé 2016 réponses de pages réussies.

Les deux chiffres sont réels, ils viennent de la même machine, et l’écart entre eux est ce que cet article a de plus utile. Commençons par l’installation, parce que c’est la partie facile.

Pourquoi l’auto-héberger

Trois raisons, dans l’ordre où elles comptent vraiment.

Vous payez déjà pour la machine. Les analytics hébergés se facturent au mois et suivent le volume de pageviews, donc la facture grimpe exactement quand le site commence à fonctionner. Un container à côté d’un site existant coûte ce que coûtent 340 MB de RAM, ce qui sur un serveur à 5,49 € est une erreur d’arrondi. Nous avons dimensionné le reste du parc dans notre avis sur Hetzner Cloud et dans le comparatif des VPS.

Pas de bandeau cookies. Umami ne dépose pas de cookies et ne construit pas de profils inter-sites. C’est la différence entre un site qui s’ouvre sur du contenu et un site qui s’ouvre sur une boîte de dialogue de consentement, et cela vaut plus que n’importe quelle métrique que cette boîte protège.

Les données restent dans votre Postgres. Vous pouvez les interroger en SQL, les sauvegarder avec le reste, et les supprimer quand vous voulez. Pas de quota d’export, pas de palier de rétention.

Le contre-argument honnête : vous exploitez désormais une base de données et une application que personne d’autre ne met à jour pour vous. Si vous ne lancerez pas docker compose pull une fois par mois, prenez la version hébergée et arrêtez de lire.

Le fichier compose, pour une machine qui fait déjà tourner quelque chose

C’est la partie que la plupart des guides ratent en supposant une machine vide. Le cas réaliste, c’est un serveur qui a déjà un reverse proxy et un site, et l’objectif est d’ajouter deux containers sans toucher à ce qui fonctionne.

services:
  umami-db:
    image: postgres:17-alpine
    container_name: umami-db
    restart: unless-stopped
    env_file: [.env.umami]
    volumes:
      - umami_db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 10s
      timeout: 5s
      retries: 10

  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    restart: unless-stopped
    env_file: [.env.umami]
    depends_on:
      umami-db:
        condition: service_healthy

volumes:
  umami_db:

Deux détails font un vrai travail ici.

Aucun des deux containers ne publie de port. Ils parlent au reverse proxy via le réseau compose et ne sont joignables depuis Internet qu’à travers lui. Si vous ajoutez ports: ["3000:3000"] par réflexe, vous venez d’exposer sur un port public une page de connexion admin avec un mot de passe par défaut, et le pare-feu est la seule chose qui vous sépare d’une mauvaise soirée.

Le healthcheck n’est pas de la décoration. Umami exécute ses migrations au premier démarrage. Lancez-le contre un Postgres encore en cours d’initialisation et le premier démarrage échoue sur une erreur de connexion, vous laissant déboguer un problème qui se règle tout seul au redémarrage. condition: service_healthy supprime la course.

Le fichier .env.umami à côté :

POSTGRES_DB=umami
POSTGRES_USER=umami
POSTGRES_PASSWORD=<long random string>
DATABASE_URL=postgresql://umami:<same string>@umami-db:5432/umami
APP_SECRET=<another long random string>

DATABASE_URL est la seule variable dont l’application a réellement besoin. APP_SECRET signe les tokens d’authentification, et la documentation est explicite : chaque installation doit avoir le sien. Générez les deux de la même façon et ne les réutilisez jamais :

openssl rand -hex 32

Gardez ce fichier en chmod 600. Il contient le mot de passe de la base à deux endroits.

Le brancher sur Caddy

Si vous faites déjà tourner Caddy comme nous l’avons décrit dans notre guide Caddy et Docker en production, toute l’intégration tient en cinq lignes :

stats.yourdomain.dev {
    encode zstd gzip

    header {
        X-Content-Type-Options nosniff
        Referrer-Policy no-referrer
        -Server
    }

    reverse_proxy umami:3000
}

Caddy résout umami via le réseau compose et obtient le certificat à la première requête. Il n’y a aucune configuration TLS à écrire, et c’est toute la raison de l’utiliser sur une petite machine.

Faites pointer l’enregistrement DNS vers le serveur avant de recharger, sinon la demande de certificat échoue et Caddy patiente un moment avant de réessayer.

L’étape à ne pas repousser

Umami crée un compte par défaut au premier démarrage : utilisateur admin, mot de passe umami. C’est documenté, donc tous les scanners d’Internet le connaissent aussi.

Connectez-vous, changez le mot de passe, puis prouvez que l’ancien a disparu :

curl -s -o /dev/null -w '%{http_code}\n' \
  -X POST https://stats.yourdomain.dev/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"umami"}'

401 est la réponse que vous voulez. Toute autre valeur signifie que le changement n’a pas pris, et que vous avez un dashboard public avec un mot de passe publié.

Ajoutez ensuite votre site dans l’interface et collez le snippet fourni dans le head de votre HTML :

<script defer src="https://stats.yourdomain.dev/script.js"
        data-website-id="your-website-id"></script>

Le website id n’est pas un secret. Il indique à quel site appartient un pageview, rien de plus.

À quoi ressemblent onze jours de chiffres réels

Voici le coût complet de ce dispositif, mesuré sur une machine en production plutôt qu’estimé :

MétriqueValeur
Mémoire du container Umami302 MB
Mémoire du container Postgres37 MB
Taille de la base, 96 pageviews9.5 MB
CPU, les deux containers, régime stable0.00%
Uptime depuis l’installation, redémarrages11 jours, zéro

Les 9.5 MB sont presque uniquement du schéma vide. Postgres ne grossit pas linéairement avec une poignée d’événements, donc ne déduisez pas un coût par pageview de ce chiffre. C’est la mémoire qui compte pour le dimensionnement : prévoyez environ 350 MB et vous n’aurez pas de surprise.

Maintenant, la partie inconfortable

Même machine, mêmes onze jours, deux sources de vérité qui se contredisent violemment.

L’access log a enregistré 2016 réponses HTML réussies. Umami a enregistré 96 pageviews sur 26 sessions. Si vous prenez ces chiffres au pied de la lettre, les analytics ont vu 4.8% de votre trafic et vous devriez paniquer.

Ne paniquez pas. Décortiquez plutôt le log.

Filtrer les user agents contenant bot, spider, crawl, curl, python, wget ou headless retire 1143 de ces 2016 requêtes. Plus de la moitié du trafic d’un site neuf est de l’automatisation ouvertement déclarée, et cela sans compter celle qui ment.

Parmi les 873 restantes, 196 sont GoogleOther - un crawler de Google dont le user agent ne contient aucune occurrence de la chaîne bot. Si votre analyse de logs fait un grep sur “bot”, vous avez compté en silence un crawler comme un lecteur. À garder en tête la prochaine fois qu’un dashboard construit sur des access logs vous dit quelque chose de flatteur.

Le reste contient encore des scrapers sans étiquette. Une forte concentration d’une seule et même version ancienne de Safari, par exemple, est une empreinte, pas une audience.

La vraie comparaison, c’est donc 96 pageviews enregistrés contre quelques centaines de requêtes plausiblement humaines, et l’écart a trois sources, par ordre de taille décroissant :

  1. L’automatisation qui n’exécute pas JavaScript. Elle apparaît dans le log et jamais dans les analytics. C’est l’essentiel de l’écart, et ici ce sont les analytics qui ont raison.
  2. Les trackers bloqués. Certaines listes de filtres bloquent selon un motif de nom d’hôte, et un hôte appelé stats. est un motif. L’auto-hébergement ne protège pas, il retire seulement le domaine tiers bien connu. Le projet documente comment proxifier le script sous votre propre domaine et déplacer le endpoint de collecte avec COLLECT_API_ENDPOINT si vous devez combler cet écart.
  3. Le prefetch et les lecteurs sans JS. Petit, réel, impossible à corriger.

La conclusion n’est pas “les logs sont honnêtes et les analytics mentent”. C’est qu’ils mesurent des populations différentes : le log compte des requêtes, les analytics comptent des navigateurs qui exécutent du script. Le log d’un site neuf est surtout fait de robots, ce qui explique pourquoi lire les logs bruts donne à un jeune site l’impression d’être populaire et constitue le moyen le plus simple de se mentir sur sa traction.

Si vous voulez un seul chiffre pour vous guider les premiers mois, prenez les sessions des analytics et traitez le log comme un outil de débogage.

Consentement, brièvement et prudemment

Les analytics sans cookies évitent l’exigence de consentement liée au stockage ou à la lecture d’informations sur l’appareil du visiteur, c’est ce qui rend le bandeau obligatoire pour le tracking classique. Cela ne rend pas le traitement invisible : une adresse IP arrive toujours à votre serveur, comme pour chaque requête vers chaque site.

Conséquences pratiques, sans valeur de conseil juridique : indiquez dans votre page de confidentialité ce que vous collectez, restez sur de l’agrégé, et renseignez IGNORE_IP pour vos propres adresses afin que votre dashboard ne soit pas surtout vous. Si votre situation est réglementée, demandez à quelqu’un de qualifié plutôt qu’à un blog.

Maintenance, c’est tout le contrat que vous avez signé

docker compose pull umami umami-db
docker compose up -d umami umami-db

Voilà la tâche mensuelle. Les migrations de schéma s’exécutent automatiquement au démarrage du container, ce qui est pratique et explique aussi exactement pourquoi le volume compte : umami_db est la seule chose que vous ne pouvez pas recréer à partir de ce fichier. Sauvegardez-le avec le reste de vos données et testez la restauration une fois, sinon vous gardez une copie non testée de 9.5 MB et un faux sentiment de sécurité.

Après la première semaine, faites encore une chose : ouvrez le dashboard et comparez-le à ce que vous savez être vrai. Si vous avez visité le site vingt fois vous-même et que le dashboard en affiche deux, quelque chose entre la balise script et le collecteur est bloqué, et mieux vaut l’apprendre maintenant que dans trois mois, quand vous prendrez des décisions à partir de ce chiffre.