Count Your Readers Without Spying on Them: Self-Hosted Analytics with GoatCounter

If you publish anything, you want to know whether anyone is reading it. That is a reasonable thing to want, and it is not the same as wanting to know who is reading it.

Google Analytics does not make that distinction. Put it on your site and you have quietly conscripted every visitor into somebody else’s advertising business, in exchange for a dashboard you look at once a week. You get numbers. They get people. It is not a trade most publishers would make consciously if it were stated that plainly, and it is the reason several European data protection authorities have found the arrangement difficult to square with GDPR.

There is a much smaller answer. GoatCounter is a single Go binary that counts pageviews without cookies, without storing IP addresses, and without building a profile of anybody. You run it yourself. The data is yours, it stays on your machine, and there is nothing to hand over because nothing personal was collected.

What you’re building

A small analytics server on a machine you control, serving a dashboard over HTTPS, collecting pageview counts from your site with a script tag under four kilobytes.

What it actually stores, and why that matters

This is the part worth understanding before you install anything, because it is the whole argument.

GoatCounter keeps aggregate tables, not visitor records. Per its own documentation, it does not store IP addresses, does not store the full User-Agent header, and does not store any tracking ID. It derives a country from the IP and then discards the address. It records the browser name and version, not the raw header it came from. Pageviews and referrers are aggregated per hour.

There is no cookie, so there is no cookie banner, because there is nothing to consent to. Uniqueness is approximated with a rotating salted hash rather than a persistent identifier, which means you cannot follow one person across days even if you wanted to. That is a deliberate design ceiling, not an oversight.

The practical consequence: you learn that a page got 40 views from Norway yesterday. You do not learn that a particular person read it twice and then went to look at your contact page. That is the correct amount of knowledge for a publisher to have.

Prerequisites

A small server you control, a VPS is fine, and this runs comfortably on the cheapest tier available. A domain or subdomain pointed at it. Nothing else.

Step 1: Install the binary

GoatCounter ships statically compiled binaries with everything included. Download the release for your platform, drop it in place, and make it executable:

curl -L -o goatcounter https://github.com/arp242/goatcounter/releases/latest/download/goatcounter-linux-amd64
chmod +x goatcounter
sudo mv goatcounter /usr/local/bin/

No runtime to install, no dependency tree, no package manager arguments. This is the single best argument for the whole design.

Step 2: Create the database and your site

SQLite is the sensible default and will carry a personal or small-publication site without complaint. PostgreSQL is supported and is the right call only if you are running high traffic or already have Postgres in the stack.

sudo mkdir -p /var/lib/goatcounter && sudo chown $USER /var/lib/goatcounter

goatcounter db create site \
  -createdb \
  -db 'sqlite+/var/lib/goatcounter/db.sqlite3' \
  -vhost stats.example.com \
  -user.email you@example.com

You will be prompted for a password. That is your dashboard login.

Step 3: Run it as a service

Do not run it in a terminal you will eventually close. A systemd unit at /etc/systemd/system/goatcounter.service:

[Unit]
Description=GoatCounter analytics
After=network.target

[Service]
ExecStart=/usr/local/bin/goatcounter serve \
  -db 'sqlite+/var/lib/goatcounter/db.sqlite3' \
  -listen localhost:8081 \
  -tls proxy
Restart=always
User=goatcounter

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now goatcounter

Note -listen localhost:8081 and -tls proxy. GoatCounter listens only on localhost and expects something in front of it to terminate TLS. That is the sane arrangement.

Step 4: Put a reverse proxy in front

Caddy is the shortest path, because it obtains and renews certificates on its own. The entire Caddyfile:

stats.example.com {
    reverse_proxy localhost:8081
}

Reload Caddy and you have a working HTTPS dashboard. Nginx or Traefik work equally well if you already run one.

Step 5: Add the counter to your site

One line before </body>:

<script data-goatcounter="https://stats.example.com/count"
        async src="//gc.zgo.at/count.js"></script>

If you would rather not load anything from a domain you do not control, self-host that script too, it is small and static, and point src at your own copy. For a privacy-first site that is the more consistent choice.

Step 6: Verify

Load a page on your site, then open your dashboard. The hit should appear within seconds. Then check the thing most people forget:

curl -sI https://stats.example.com | grep -i set-cookie

Empty output is the result you want. No cookie set, nothing to disclose, no banner required.

The honest trade-offs

No tool is free of them, and a guide that pretends otherwise is selling something.

  • You will see lower numbers than Google Analytics reported. This is not a fault, it is the absence of cross-site tracking and persistent identifiers. The numbers are more honest and less flattering. If you publish your traffic figures, as we do, that is a feature.
  • Ad blockers will still eat some hits. Many block analytics scripts by category, regardless of how well-behaved yours is. Expect an undercount. Server-side log analysis is the alternative if you need completeness more than convenience.
  • You are now the operator. Backups, updates and uptime are yours. In practice that is a cron job for the SQLite file and an occasional binary swap, but it is not zero.
  • You lose the deep funnel analysis. No cohorts, no cross-session journeys, no individual paths. If your business model genuinely depends on that, this tool is not for you, and it is worth asking what you are doing to your readers to obtain it.

What you’ve gained

Numbers you can trust, on hardware you control, without conscripting your readers into an advertising network to get them. No consent banner, because there is nothing to consent to. And a defensible answer when somebody asks what you collect: aggregate counts, no personal data, and here is the documentation.

The smaller point, and the one I keep coming back to: a publisher does not need to know who you are. Knowing that the piece was read is enough.

AI disclosure

This article uses AI tools for research and generation with a human in the loop. Drafts are reviewed, edited, and signed off by a named natural person before publication. Editorial responsibility: Thomas A. Kleppestø.

Get the next guide

New privacy and security guides, plus the occasional investigation. No tracking, no spam, unsubscribe any time by replying. We never share your address.

Prefer not to sign up? Just email a tip or a correction to HAL0zum@proton.me, or use our PGP key for sensitive material.