Când analizezi traficul unui blog tehnic în 2026, realitatea te lovește dur: scraperii de IA inflată statisticile până la 2.000 de „vizitatori” pe zi, lăsându-te fără date actionabile. Vincent Bernat, developer Debian și autor al blogului tehnic vincent.bernat.ch, a rezolvat problema migând de la GoAccess (analiză de loguri) la GoatCounter autogestionat, cu un client JavaScript personalizat care filtrează boții prin interacțiune utilizator, un proxy local pe fiecare dintre cele cinci servere web pentru reziliență, și backup continuu al bazei de date SQLite cu Litestream. Această arhitectură — implementată integral pe NixOS cu Colmena pentru deployment declarativ — demonstră cum poți obține analitică respectuoasă cu privacy-ul fără a renunța la acuratețe sau control. Dacă administrezi infrastructură pentru clienți sau proiecte proprii, abordarea lui Bernat oferă un model replicabil pentru servicii self-hosted robuste. Pentru servere dedicate optimizate pentru astfel de workloads, https://www.dedicated-servers.ro/ oferă hardware configurabil cu livrare rapidă în România și Europa.
De ce GoatCounter și cum filtrezi boții fără cookies
GoatCounter se distinge prin filozofia privacy by design: nu stochează adrese IP, nu folosește cookies, și generează identificatori de sesiune valizi 8 ore bazate pe hash-ul User-Agent și IP. Binärul unic (scris în Go) și baza de date SQLite îl fac ușor de deployat și monitorizat. Alternativele — Umami, Plausible, Rybbit — sunt valide dar mai complexe; GoatCounter rămâne cea mai ușoară soluție self-hosted pentru bloguri și site-uri mici.
Problema majoră în 2024-2025: scraperii de IA (GPTBot, ClaudeBot, PerplexityBot și altele) mentin User-Agent-uri legitime și se ascundă după proxy-uri rezidențiale. Filtrarea clasica pe UA sau IP e ineficientă. Soluția lui Bernat, inspirată de Bear Blog: contorizezi un „hit” doar după prima interacțiune umană — touchmove, mousemove, keydown sau pointerdown. Codul JavaScript e minimal (~2 KB gzipped) și folosește fetch cu keepalive: true pentru a nu bloca navigarea:
const count = ({ event, title } = {}) => {
const params = new URLSearchParams({
p: event || location.pathname,
t: title || document.title,
r: document.referrer,
q: location.search,
s: document.documentElement.clientWidth,
e: !!event,
rnd: Math.random().toString(36).slice(2, 7),
});
fetch(`/count?${params}`, { keepalive: true }).catch(() => {});
};
let sendHit = () => (sendHit = () => {}, count());
["touchmove", "mousemove", "keydown", "pointerdown"].forEach(eventName =>
document.addEventListener(eventName, sendHit, { once: true, passive: true })
);
Rezultatul: de la ~2.000 „vizitatori”/zi raportați de GoAccess (majoritate boți), GoatCounter numără sub 200 de oameni reali/zi. Googlebot, deși rulează Chromium headless, nu interacționează cu pagina și nu declanșează evenimentele — więc nu e contorizat. Aproximativ 2% din vizitatori au JavaScript dezactivat; pentru ei, Bernat folosește un fallback CSS clever: pseudo-clasa :hover pe <body> încarcă o imagine /count?... doar după interacțiune, înregistrând vizita cu referrer „NoJS”.
Proxy local pe fiecare server: reziliență și evadare de ad-blockere
Blogul lui Bernat rulează pe cinci servere web distribuite geografic (Paris, Helsinki, Nürnberg, Ashburn, Chicago), dar GoatCounter rulează pe unul singur. Dacă instanța centrală cade, pierzi date. Soluția: un proxy local pe fiecare server, ascultând pe același endpoint /count, care bufferizează hit-urile în memorie (capacitate pentru câteva zile downtime) și le trimit în batch-uri către backend-ul principal prin API-ul autentificat /api/v0/count.
Pentru mai multe detalii despre această parte a subiectului, vezi Nextcloud 30 pe VPS dă timeout? Repară PHP-FPM, Redis, cron, MariaDB și reverse proxy-ul ca instanța să nu moară dupa 3 luni.
Proxy-ul e expus prin nginx pe același domeniu ca blogul (location = /count { proxy_pass http://127.0.0.3:8087/count; ... }), evadând ad-blockerele care blochează domenii cunoscute de analytică. Bernat argumentează: GoatCounter e privacy-friendly, deci respectă alegerea cititorului chiar dacă nu onorează blocarea la nivel de DNS. Codul proxy-ului a fost propus upstream (PR #909) dar respins de maintainer ca „prea niched” — o decizie pe care Bernat o respectă, menținând fork-ul propriu.
Configurarea nginx pe fiecare server web:
location = /count {
access_log off;
proxy_pass http://127.0.0.3:8087/count;
proxy_pass_request_headers off;
proxy_set_header Accept-Language $http_accept_language;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header X-Real-Ip $remote_addr;
}
Proxy-ul rulează în container systemd-nspawn efemer pe 127.0.0.3:8087, cu rate-limiting 10/1 (10 requesturi/secundă/IP) pentru a preveni abuzul. Cheia API e gestionată ca secret prin Colmena (vezi secțiunea Deploy).
Deploy declarativ pe NixOS cu Colmena și containere systemd-nspawn
Toate cele cinci servere rulează NixOS — distribuție Linux declarativă unde configurația e cod, reproductibilă și versionabilă. Bernat folosește Colmena (tool stateless de deployment NixOS) pentru a aplica configurația pe flota. Toată configurația e publică pe GitHub: vincentbernat/nixops-take1.
Izolarea serviciilor se face prin containere systemd-nspawn efemere — instanțe NixOS stripped-down care partajează network namespace-ul cu hostul (simplitate > izolare completă). Un modul NixOS custom (modules/container.nix) abstractează boilerplate-ul: containere efemere (ephemeral = true), auto-start, fără rețea privată (privateNetwork = false), și servicii non-esentiale dezactivate (console-getty, systemd-logind, systemd-oomd).
Persistența datelor (baza de date SQLite GoatCounter) se realizează prin bind mounts de pe host (/var/db/goatcounter) expuse în container via opțiunea mounts customă. Secretele (chei API, credențiale GeoIP, credențiale SFTP pentru backup) sunt gestionate de Colmena Keys: comenzi locale (ex: pass show ...) care extrag secrete din password manager și le deployează pe servere în /var/keys/, montate read-only în containere la /etc/<nume>.env.
Exemplu configurare container GoatCounter server (ascultă pe 127.0.0.4:8088):
{ pkgs, config, ... }: {
imports = [ ./modules/container.nix ];
config.luffy.containers.goatcounter = {
mounts = [ "/var/db/goatcounter" ];
keys."goatcounter.env" = [ "${pkgs.runtimeShell}" "-c" "pass show personal/nixops/secrets | grep '^GOATCOUNTER_GEODB='" ];
config = {
services.goatcounter = {
enable = true;
address = "127.0.0.4";
port = 8088;
proxy = true;
extraArgs = [ "-db=sqlite+/var/db/goatcounter/db.sqlite" ];
};
systemd.services.goatcounter.serviceConfig = {
EnvironmentFile = "/etc/goatcounter.env";
SupplementaryGroups = [ "keys" ];
DynamicUser = true;
Restart = "always";
ExecStartPre = "+${pkgs.coreutils}/bin/chown -R goatcounter:goatcounter /var/db/goatcounter";
};
};
};
}
GoatCounter e pachetat în Nixpkgs; Bernat suprascrie src și vendorHash pentru a folosi fork-ul propriu cu codul proxy (branch feature/proxy). Un modul dedicat (modules/goatcounter.nix) encapsulează definirea containerului, serviciul systemd, și secretele — izolând configurația de schimbările upstream.
Nginx expune interfața GoatCounter pe goatcounter.luffy.cx cu TLS forțat, și blochează endpoint-ul /count (return 404) pentru că proxy-ul local îl gestionează.
Backup continuu SQLite cu Litestream pe Hetzner Storage Box
Baza de date SQLite a GoatCounter e criticală — pierderea ei înseamnă pierderea istoricului analytic. Litestream face replicare streaming a WAL (Write-Ahead Log) comprimat către destinații remote. Bernat îl rulează într-un container systemd-nspawn separat, cu mount-uri pentru directoarele bazelor de date și secrete pentru SFTP.
Configurația Litestream (în modules/litestream.nix):
- Snapshot-uri complete la 24h, reținute 15 zile (360h)
- Trei niveluri de compactare a fișierelor de tranzacții: 5 minute, 30 minute, 3 ore
- Auto-recuperare activată (cu avertisment: poate cauza pierdere de date minoră la restart, dar Bernat preferă backup-uri ininterrupte)
- Replică stocată pe Hetzner Storage Box via SFTP, într-un director per host (
web02/goatcounter/,web03/goatcounter/, etc.) - Credențiale citite din
/etc/litestream.env(populat din Colmena Keys)
services.litestream = {
enable = true;
environmentFile = "/etc/litestream.env";
settings = {
auto-recover = true;
snapshot = { interval = "24h"; retention = "360h"; };
levels = [ { interval = "5m"; } { interval = "30m"; } { interval = "3h"; } ];
dbs = lib.mapAttrsToList (name: path: {
inherit path;
replica = {
type = "sftp";
host = "${SQLITE_BACKUP_HOST}";
user = "${SQLITE_BACKUP_USER}";
password = "${SQLITE_BACKUP_PASSWORD}";
"host-key" = "${SQLITE_BACKUP_HOSTKEY}";
path = "${config.networking.hostName}/${name}";
};
}) cfg.databases;
};
};
Restaurarea e testabilă: oprești containerele, muti baza coruptă, rulezi litestream restore cu configurația și variabilele de mediu corecte, restartezi. Pentru o bază de 20 MiB, procesul durează ~20 minute — lent, dar fiabil.
–
Pași practici pentru implementare
- Instalează GoatCounter pe un server NixOS (sau VM) — folosește pachetul din Nixpkgs sau compilează din sursă cu fork-ul proxy
- Configurează containerul
systemd-nspawncuprivateNetwork = false,ephemeral = true, și bind mount pentru/var/db/goatcounter - Scrie clientul JavaScript personalizat cu filtrare pe interacțiune (
touchmove,mousemove,keydown,pointerdown) și fallback CSS:hoverpentru NoJS - Deploy-ează proxy-ul local pe fiecare server web frontend (nginx →
127.0.0.3:8087/count), cu rate-limiting și buffer în memorie - Configurează Litestream într-un container separat pentru backup continuu SQLite către Hetzner Storage Box (sau alt backend S3/SFTP) cu snapshot-uri zilnice și compactare multi-nivel
- Gestionează secretele cu Colmena Keys — extrage din password manager (
pass), deployează în/var/keys/, montează read-only în containere la/etc/*.env - Testează restaurarea periodic: oprește serviciile, rulează
litestream restore -o /tmp/test.db, verifică integritatea, șterge testul
–
Concluzie: Arhitectura prezentată demonstrează că analitica self-hosted modernă nu trebuie să fie complexă sau invazivă. Combinând GoatCounter (privacy-first, lightweight), un client JS bazat pe dovezi de umanitate, proxy-uri locale pentru reziliență, și Litestream pentru backup-uri incrementale automate, obții date accurate fără să compromiți etica sau controlul. NixOS și Colmena transformă această stack într-o configurație declarativă, versionabilă, și replicabilă pe oricâte servere. Dacă construiești infrastructură pentru clienți care cer suveranitate de date și simplitate operațională, acest pattern e un punct de plecare excelent. Pentru hardware dedicat care rulează NixOS nativ și sprijină containere systemd-nspawn performanți, https://www.serverspan.ro/ și https://www.dedicated-servers.ro/ oferă opțiuni configurabile cu livrare în România și Europa.
Ghid ServerSpan relevant: Cron vs systemd timers pentru backup-uri: care mai pornește după un reboot la ora 3 dimineața?.