Hosting Your Own Goatcounter Behind Nginx (on Ubuntu)

This past weekend I added Goatcounter, a simple website analytics tracker, to my site. In keeping with the spirit of self-hosting, DIY experimentation that's been guiding my development here, I'm hosting Goatounter myself rather than using the centralized service. And maybe it doesn't go without saying that using Goatcounter in the first place is a conscious choice against corporate surveillance-capitalist services like the ubiquitous urchin, Google Analytics. While this wasn't overly complicated I didn't see any posts laying out how to set up Goatcounter behind Nginx on a Linux host (my server runs Ubuntu); hopefully this guide will prove useful to anyone looking to do the same.

Goatcounter consists of the server -- available as a self-contained binary written in Go -- and the client code, which is a javascript file. I'm hosting both locally to be as self-sufficient as possible and make sure my client and server versions stay synced up. I chose the subdomain for this deployment.

The client-side is much simpler so I'll get that out of the way before talking about what I did to get the server running as desired. First I downloaded the script with a simple wget. It lives in a static folder in my eleventy project directory that's configured for passthrough copy - everything in there is directly copied to the output when running the eleventy command. Then in the base page layout I added the <script> tag like so:

  <script async src="/static/count.js?v=1.3.2" data-goatcounter=""></script>

And that's all it needs! Note I added a get parameter with the current Goatcounter version; this way when I update the script I can bust browser caching by editing the version number. And yes it would be just a little rude for you to interject that this is a wildly premature optimization for assuming that someone would visit my site enough to have the script cached. It's fine.

Now. The server.

Again we start by downloading the file. I've got it sitting comfortably at /usr/local/bin/goatcounter. The binary expects to be run with no competition for ports 80 and 443, but I already have Nginx serving a variety of apps and services on this host, so I set up a reverse proxy to Goatcounter.

Here's /etc/nginx/sites-available/goatcounter.conf:

server {
	listen 80;
	listen [::]:80;
    location ~* {
        return 301 https://$server_name$request_uri;

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/;
    ssl_certificate_key /etc/letsencrypt/live/;

	include snippets/ssl-params.conf;

    access_log /var/log/nginx/stats.access.log test;
    error_log /var/log/nginx/stats.error.log debug;

    location / {
        proxy_set_header Host $host;
        proxy_pass http://localhost:8085;

A few notes on this:

  • I have a wildcard cert from Let's Encrypt / Certbot for this domain, and all traffic should be handled via https
  • I had to add a DNS record for the subdomain.
  • Without the proxy_set_header directive, Goatcounter tries to record a hit for localhost - doesn't ultimately matter much but this way it looks like we expect -- if you don't care you can skip this directive and create your site as localhost in that step below.
  • You can choose any port that's open I guess; Goatcounter's default is 8081.

Then all that's left for this side of setup is ln -s /etc/nginx/sites-{available,enabled}/goatcounter.conf and service nginx reload.

The goatcounter binary can be run from the cli, which is necessary for setup (goatcounter create -d and handy for testing, but if we want it to run unattended the obvious answer is to give it a systemd unit. I'm pretty new to exploring writing my own unit files for tasks and I have to thank Nick Sellen for tipping me off to some of the possibilities available there. This is a pretty straightforward case though: we have a command we want to run automatically on startup/reboot, etc. Here's my unit file at /etc/systemd/system/goatcounter.service:

Description="simple opensource analytics server - cf"

ExecStart=/usr/local/bin/goatcounter serve \
-listen localhost:8085 \
-db "sqlite://home/noah/db/goatcounter.sqlite3" \
-tls none \
-port 8085


Yes, I set up the db in my home directory. Probably not the "most correct" choice but it's fine. The options here match up with what we've configured above: set the full path to the database file, listen at the location specified in nginx's proxy_pass directive, specify the port, and disable goatcounter's built-in TLS support since that's managed by nginx. With this file in place and enabled by systemctl enable goatcounter.service, everything is set!

I'm really happy with this setup. Goatcounter is lightweight and easy to use. I can now log in at and see that I have visited my site several times. Hopefully those stats will get more interesting over time! If you use this guide to set up analytics for your site, or if you see errors or room for improvement in my configuration, I'd love to hear from you! And please don't forget to support further development of Goatcounter.

← Posts