A lightweight, self-hosted, cookieless web analytics and link shortener. One small Rust binary, a single SQLite file, and about 11 MB of RAM.
Important
This project was created entirely with AI, but the application has been thoroughly tested.
It was built primarily for my personal use, so I will not be merging pull requests or adding features unless I need them myself. If you want changes, feel free to fork this repository.
I had used Umami Analytics for about two years. It does the job well, but over time Umami kept adding features I don't need, and I kept running it anyway.
Recently my VPS memory sat around 91% (of 4 GB), so I looked for what I could cut to free some up and found that Umami, together with its PostgreSQL database, was quietly using around 480 MB:
So I decided to build my own analytics with only the features I need, and keep it light on the VPS. Beacon currently uses about 11 MB of memory on the same server:
- Cookieless and privacy-friendly — no cookies, no consent banner, no raw IP stored. Visitors are counted with a pseudonymous daily identifier.
- Core Web Vitals — real-user LCP, INP, CLS, FCP and TTFB with p50/p75/p95 on a dedicated Performance page.
- Link shortener — create short links on your own domains, each with its own analytics.
- Shareable public dashboards — per site and per link, with selectable sections and a minimum-count threshold so small numbers stay private.
- The usual reports — pages, referrers, UTM sources, countries, devices, browsers, operating systems, plus a realtime view.
- GeoIP — country, region and city from the bundled DB-IP Lite database.
- Multiple sites and custom domains, multi-user with owner/admin/viewer roles.
- Bot filtering and IP-based rate limiting on the public endpoints.
- A public read API for pulling aggregate stats into your other apps (see below).
- Single-node and SQLite-backed. Rate limits are per-instance and in-memory; it is not designed for multi-replica horizontal scaling.
- Because there are no cookies, returning-visitor identity resets daily by design.
- The public API is aggregate-only and requires enabling public sharing per site.
- No plugin system — it is built primarily for the author's own use.
Beacon exposes three ports:
| Port | Purpose |
|---|---|
3001 |
Admin dashboard and API (published on host loopback by default) |
3002 |
Public origin: tracker script (/data.js), event ingestion (/api/receive), and shared public pages |
3003 |
Short-link redirects |
Put a reverse proxy (Traefik, Caddy, Nginx, Cloudflare…) in front, routing your admin hostname to 3001 and your public analytics hostname to 3002.
Deploy with Docker Compose
- Copy
compose.yamlfrom this repo to your server. - Set the bootstrap credentials (they create the owner account on first start; the password must be 12–256 characters):
export [email protected] export BEACON_ADMIN_PASSWORD='a-long-random-password'
- Start it:
docker compose up -d
- The admin dashboard is published on
127.0.0.1:3001. Reach it through your reverse proxy, or openhttp://localhost:3001on the host. Change your password from Settings → Account after the first login.
Deploy with Coolify
-
Add a new resource → Docker Compose Empty.
-
Paste the contents of
coolify.yamlfrom this repo. -
Set
BEACON_ADMIN_EMAILandBEACON_ADMIN_PASSWORDin the environment. -
Open the Domains page and add two domains, each pointing to a container port:
- Admin dashboard →
https://admin.example.com:3001 - Public analytics (tracker + shared pages) →
https://analytics.example.com:3002
(Add a third for
:3003only if you use short links.) - Admin dashboard →
-
Set
BEACON_PUBLIC_URLto your public analytics domain (e.g.https://analytics.example.com). -
Click Deploy.
Set these as environment variables.
| Variable | Default | Description |
|---|---|---|
BEACON_ADMIN_EMAIL |
— | Owner account email, created on first start only. |
BEACON_ADMIN_PASSWORD |
— | Owner password (12–256 chars), first start only. Change it after logging in. |
BEACON_PUBLIC_URL |
— | Public origin (the collector port), e.g. https://analytics.example.com. Required, or the tracker snippet and public pages are disabled. |
BEACON_PROXY_HOPS |
0 |
Trusted reverse proxies in front: 0 direct, 1 Traefik, 2 Cloudflare + Traefik. |
BEACON_ADMIN_BIND |
127.0.0.1 |
Host interface the admin port is published on (Docker Compose). Set to 0.0.0.0 to reach it directly from the network. |
BEACON_ADMIN_PORT |
3001 |
Admin dashboard and API. |
BEACON_COLLECTOR_PORT |
3002 |
Tracker ingestion and public pages. |
BEACON_REDIRECT_PORT |
3003 |
Short-link redirects. |
BEACON_SECURE_COOKIES |
true |
Secure flag on session cookies. Browsers drop them over plain HTTP (except localhost). |
BEACON_RETENTION_DAYS |
0 |
Delete raw events older than N days once a day (daily aggregates are kept). 0 keeps everything. |
BEACON_SESSION_HOURS |
168 |
Admin session lifetime in hours. |
RUST_LOG |
server=info,tower_http=info |
Log level. |
Copy the exact snippet from Site settings → Tracker. It looks like:
<script defer src="https://analytics.example.com/data.js" data-site="YOUR_SITE_PUBLIC_ID"></script>When a site (or link) has public sharing enabled, other apps can read its aggregate stats without authentication. All requests are served from your public origin (the collector port) and are rate limited.
GET /api/public/sites/{public_slug}/overview?from=<unix_seconds>&to=<unix_seconds>
GET /api/public/sites/{public_slug}/{dimension} # path | referrer | country | browser | os | device
GET /api/public/links/{public_slug}/overview
GET /api/public/links/{public_slug}/{dimension}from/to are optional (default last 30 days). Only the sections you enabled for the page are returned.
IP geolocation by DB-IP.