Plausible Analytics
Plausible to self-hosted alternatywa dla Google Analytics. Lekki skrypt trackingowy (~1 kB), brak cookies, zgodność z GDPR bez bannerów. Dashboard czytelny i szybki, bez dziesiątek zakładek i raportów GA4.
Community Edition działa na PostgreSQL (metadane, konfiguracja) + ClickHouse (zdarzenia, pageviews).
Architektura
graph LR
I[Internet<br/>:443] --> T[Traefik]
T -->|"/js/* /api/event<br/>(publiczny)"| P[Plausible<br/>:8000]
T -->|"dashboard<br/>authelia-auth"| P
P --> PG[PostgreSQL<br/>plausible_db]
P --> CH[ClickHouse<br/>plausible_events_db]
Ruch do Plausible przechodzi przez Traefik z podziałem na dwa routery (szczegóły w sekcji Traefik poniżej):
- publiczny (
/js/*,/api/event): skrypt trackingowy i endpoint zbierania zdarzeń. Bez Authelia, bo muszą być dostępne dla odwiedzających stronę. - chroniony (reszta, w tym dashboard): za middleware
authelia-authz Authelia .
Konfiguracja
docker-compose.yml
services:
plausible:
image: ghcr.io/plausible/community-edition
container_name: plausible
stop_grace_period: 10s
restart: unless-stopped
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
depends_on:
- plausible_db
- plausible_events_db
env_file:
- .env
networks: [traefik_network, plausible_internal]
labels:
com.centurylinklabs.watchtower.enable: true
plausible_db:
image: postgres:17-alpine
container_name: plausible_db
stop_grace_period: 10s
restart: unless-stopped
volumes:
- ./db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=<haslo>
networks: [plausible_internal]
plausible_events_db:
image: clickhouse/clickhouse-server:26.3-alpine
container_name: plausible_events_db
stop_grace_period: 10s
restart: unless-stopped
volumes:
- ./event-data:/var/lib/clickhouse
- ./event-logs:/var/log/clickhouse-server
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
- ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
networks: [plausible_internal]
networks:
traefik_network:
external: true
plausible_internal:
driver: bridge
.env
BASE_URL=https://analytics.robertolechowski.com
SECRET_KEY_BASE=<losowy-ciag-min-64-bajty>
HTTP_PORT=8000
DATABASE_URL=postgres://postgres:<haslo>@plausible_db:5432/plausible_db
CLICKHOUSE_DATABASE_URL=http://default:XXXXXXXX@plausible_events_db:8123/plausible_events_db
DISABLE_REGISTRATION=invite_only
Konfiguracja ClickHouse
ClickHouse domyślnie generuje dużo logów wewnętrznych. Dwa pliki XML redukują to do minimum.
/etc/clickhouse-server/config.d/logging.xml
<clickhouse>
<listen_host>0.0.0.0</listen_host>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<query_thread_log remove="remove"/>
<query_log remove="remove"/>
<text_log remove="remove"/>
<trace_log remove="remove"/>
<metric_log remove="remove"/>
<asynchronous_metric_log remove="remove"/>
<session_log remove="remove"/>
<part_log remove="remove"/>
</clickhouse>
/etc/clickhouse-server/users.d/logging.xml
<clickhouse>
<profiles>
<default>
<log_queries>0</log_queries>
<log_query_threads>0</log_query_threads>
</default>
</profiles>
<users>
<default>
<password>XXXXXXXX</password>
<networks>
<ip>::/0</ip>
</networks>
<profile>default</profile>
<access_management>1</access_management>
</default>
</users>
</clickhouse>
Router Traefik
Konfiguracja w pliku dynamic/analytics.yaml na hoście z Traefik .
http:
routers:
plausible-public:
rule: "Host(`analytics.robertolechowski.com`) && (PathPrefix(`/js/`) || PathPrefix(`/api/event`))"
entryPoints: [websecure]
tls:
certResolver: letsencrypt
service: plausible-service
priority: 100
plausible:
rule: "Host(`analytics.robertolechowski.com`)"
entryPoints: [websecure]
tls:
certResolver: letsencrypt
service: plausible-service
middlewares: [authelia-auth]
services:
plausible-service:
loadBalancer:
servers:
- url: "http://plausible:8000/"
plausible-public (priority 100) przepuszcza /js/* i /api/event bez Authelia (skrypt i endpoint zdarzeń muszą być publiczne), reszta ruchu idzie przez authelia-auth.
Integracja trackera (proxy przeciw adblockerom)
Plausible jest lekki i nie używa cookies, ale jego domyślny endpoint (/js/script.js + /api/event na zewnętrznym hoście typu plausible.io lub własnej subdomenie analytics.*) trafia na większość list filtrów (EasyPrivacy, uBlock Origin, Brave Shields). Skutek: część odwiedzających w ogóle nie jest liczona.
Standardowe obejście, które rekomenduje sam Plausible, to proxy przez własną domenę — tracker ładuje się i wysyła zdarzenia pod ścieżką, której listy filtrów nie znają. Zamiast wstawiać <script src="https://analytics..."> w <head>, używam paczki npm @plausible-analytics/tracker, a endpoint wskazuję na lokalną ścieżkę pod tą samą domeną.
Frontend
import { init } from '@plausible-analytics/tracker';
init({
domain: 'blog.robertolechowski.com',
endpoint: '/proxy/pla/api/event',
});
Backend
Backend (Flask lub nginx) przekazuje żądania trackera do https://analytics.robertolechowski.com, podbijając nagłówki, których potrzebuje Plausible do geolokalizacji (X-Forwarded-For) i deduplikacji per domena (X-Forwarded-Host). Bez nich wszystkie zdarzenia wyglądałyby jakby przychodziły z IP serwera bloga.
Flask blueprint
Plik app/plausible_proxy/blueprint.py:
@bp.route("/proxy/pla/api/event", methods=["POST"])
def proxy_event():
url = f"{UPSTREAM}/api/event"
headers = {
"Content-Type": request.content_type or "application/json",
"User-Agent": request.headers.get("User-Agent", ""),
"X-Forwarded-For": request.headers.get("X-Forwarded-For") or request.remote_addr,
"X-Forwarded-Host": request.headers.get("Host", ""),
}
req = urllib.request.Request(url, data=request.get_data(), headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=5) as resp:
return Response(resp.read(), status=resp.status)
Nginx
Fragment nginx.conf z projektu Mandelbrot:
# Plausible analytics proxy — hides real endpoint from adblockers
location = /api/event {
proxy_pass https://analytics.robertolechowski.com/api/event;
proxy_http_version 1.1;
proxy_buffering on;
proxy_ssl_server_name on;
proxy_set_header Host analytics.robertolechowski.com;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
Struktura katalogów
plausible/
├── docker-compose.yml
├── .env
├── clickhouse/
│ ├── clickhouse-config.xml
│ └── clickhouse-user-config.xml
├── db-data/ # PostgreSQL data (gitignore)
├── event-data/ # ClickHouse data (gitignore)
└── event-logs/ # ClickHouse logs (gitignore)
Ignorowanie własnego ruchu
Domyślnie Plausible liczy też moje własne wizyty z domowego komputera, co zaburza statystyki. Rozwiązanie: flaga w localStorage przeglądarki, którą tracker respektuje i pomija wysyłkę zdarzeń.
W konsoli przeglądarki (DevTools → Console) na śledzonej domenie:
localStorage.plausible_ignore = "true"