3.9 KiB
storebound.dev site analytics
The site counts visits with a self-hosted Umami on the VDS that serves storebound.dev. Cookieless, no personal data, Do Not Track respected, nothing sent to a third party. This page is the rebuild recipe (no secrets).
What runs where
| Piece | Where |
|---|---|
Umami 3.4.0 + Postgres 15.14 (Docker Compose, restart: unless-stopped) |
/opt/umami on the VDS, 127.0.0.1:3920 only |
Secrets (DB password, APP_SECRET, TWO_FACTOR_ENCRYPTION_KEY) |
/opt/umami/.env, mode 600, generated on the server |
| Script and collect endpoint, first-party | https://storebound.dev/s/p.js, https://storebound.dev/s/e (/etc/apache2/storebound-stats.conf, from tools/vds/apache-storebound-stats.conf, included in the storebound.dev :443 vhost) |
| Dashboard | https://stats.crt.fyi (tools/vds/apache-stats.crt.fyi.conf; *.crt.fyi wildcard DNS through Cloudflare and the crt.fyi wildcard cert) |
| Admin login | the owner's Mac, ~/.storebound-keys/umami.json (0600) |
URLs never contain "umami", "analytics", "track" or "collect" (TRACKER_SCRIPT_NAME=p.js, COLLECT_API_ENDPOINT=/e), so
common filter lists do not block them.
Client IP. Cloudflare sits in front. Apache's mod_remoteip takes CF-Connecting-IP only from Cloudflare's ranges
and passes the result as X-Sb-Client, which Umami reads (CLIENT_IP_HEADER=x-sb-client). A client cannot spoof it by
sending the header to the origin directly. The collect endpoint answers Cache-Control: no-store.
Rebuild
/opt/umami/docker-compose.yml: Umami's upstream compose with the image pinned toghcr.io/umami-software/umami:3.4.0, port127.0.0.1:3920:3000,env_file: .env, andTRACKER_SCRIPT_NAME: p.js,COLLECT_API_ENDPOINT: /e,CLIENT_IP_HEADER: x-sb-client,DISABLE_TELEMETRY: "1",DISABLE_UPDATES: "1"; Postgrespostgres:15.14-alpinewith a named volume, bothrestart: unless-stopped..env(umask 077):POSTGRES_PASSWORD,DATABASE_URL=postgresql://umami:<pw>@db:5432/umami,APP_SECRETandTWO_FACTOR_ENCRYPTION_KEYfromopenssl rand -hex.docker compose up -d, wait for/api/heartbeat.- Log in as
admin/umami, change the password at once (POST /api/me/password) to a generated one and store it in~/.storebound-keys/umami.jsonon the Mac. Create the websitestorebound.dev; its id goes into the script tag. - Apache (back up every file first,
cp -a <file> /root/<name>.bak-<timestamp>). The Cloudflare ranges live only on the server, in/etc/apache2/cloudflare-trusted-proxies.conf; refresh them now and then:ssh root@<server> 'for u in ips-v4 ips-v6; do curl -fs https://www.cloudflare.com/$u | sed "s/^/RemoteIPTrustedProxy /"; echo; done | grep . > /etc/apache2/cloudflare-trusted-proxies.conf' scp tools/vds/apache-storebound-stats.conf root@<server>:/etc/apache2/storebound-stats.conf scp tools/vds/apache-stats.crt.fyi.conf root@<server>:/etc/apache2/sites-available/stats.crt.fyi.conf # in the storebound.dev :443 vhost, after the desktop include: Include /etc/apache2/storebound-stats.conf ssh root@<server> 'a2ensite stats.crt.fyi && apache2ctl configtest && systemctl reload apache2' - Site:
site/index.htmlloadshttps://storebound.dev/s/p.jswithdefer,data-host-url="https://storebound.dev/s",data-domains="storebound.dev"(localhost and previews send nothing) anddata-do-not-track="true".
Events (site/main.js)
| Event | Data |
|---|---|
copy |
what: the copied command or the copied block's id (every Copy button) |
desktop-download |
os (macos, windows, linux), file |
teaser-play, teaser-50, teaser-complete |
once per page view |
outbound |
kind (docs, git, npm, other), host, path |
Upgrade
Read Umami's release notes, change the pinned tag, docker compose pull && docker compose up -d in /opt/umami
(take docker compose exec db pg_dump -U umami umami > /root/umami-<date>.sql first).