Asked a simple question about my own sites — "what does traffic look like?" — and found the honest answer was nothing. My reverse proxy had been running for months with zero access logging. Here's how I closed that gap with a self-hosted analytics stack, start to finish.
It's easy to assume a reverse proxy is logging requests just because it's healthy and serving traffic fine.
Mine wasn't. A quick check of the container's own logs showed only internal housekeeping messages — TLS
storage cleanup, certificate maintenance — never a single HTTP request. No log directive had
ever been added to any site block. Worth checking directly rather than assuming, especially on infrastructure
that's been stable long enough to stop thinking about.
Three real options, roughly in order of weight:
Went with Umami: a real dashboard without Plausible's operational overhead.
postgresql-latest and mysql-latest are published now. A small Postgres sidecar
container ended up being the actual lightweight option, not the extra step it looked like on paper.
A single small container/VM running both services via Docker Compose:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: "<generated>"
volumes:
- ./pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "3050:3000"
environment:
APP_SECRET: "<generated>"
DATABASE_URL: "postgresql://umami:<password>@db:5432/umami"
depends_on:
db:
condition: service_healthy
admin/umami account. Log in once via the API, immediately set a new password for
that account, and confirm the old credentials are rejected — before adding any public DNS record or reverse
proxy rule for the service. A newly public admin panel sitting on default credentials, even briefly, is a
real exposure window.
If you're already running other *.yourdomain.com subdomains behind Cloudflare with an Origin CA
certificate (see the reverse proxy migration guide for how that's set up),
a new subdomain needs zero new certificate work — just a new site block reusing the existing wildcard cert:
analytics.example.com {
tls /config/origin-cert.pem /config/origin-key.pem
reverse_proxy 192.168.1.X:3050
}
Reload (not restart) the reverse proxy after adding it — a graceful reload validates the config and applies it without dropping any of your other live domains' connections. Worth confirming a handful of your existing domains still respond immediately after, not just the new one.
Umami gives each site you register a small script tag with a unique website ID:
<script defer src="https://analytics.example.com/script.js" data-website-id="..."></script>
Drop it before </head> on every page you want tracked. For a static multi-page site, a
one-line script across all your HTML files is faster and less error-prone than editing each by hand — and
lets you confirm none were silently missed.
A running container proves nothing about whether analytics actually work end-to-end — DNS, the reverse proxy route, and the tracking script all have to line up correctly first. Umami's collect API can be hit directly to simulate a real pageview:
curl -X POST http://localhost:3050/api/send \
-H "Content-Type: application/json" \
-d '{"type":"event","payload":{"website":"<website-id>","url":"/","hostname":"example.com"}}'
Then check the stats endpoint (or the dashboard) actually shows it. Only once a real pageview shows up as a real number is the pipeline confirmed working — not before.
None of the above makes the analytics domain publicly reachable by itself. If you're on Cloudflare, every
subdomain typically needs its own explicit DNS record — there's usually no wildcard record silently covering
new ones. An A record pointing at the same origin your other subdomains use, proxied the same way, is enough
— but it's easy to build and verify everything else first and forget this last, unglamorous step. Confirm
public resolution and a real 200 from the script URL before calling it done.