Easy Privacy-Friendly Self-Hosted Website Analytics with GoatCounter

A quick overview of setting up GoatCounter website analytics with Postgresql, NGINX, and LetsEncrypt on Debian 11 (Bullseye).

Per goatcounter.com, “it aims to offer easy to use and meaningful privacy-friendly web analytics as an alternative to Google Analytics or Matomo.” I chose it because I wanted something that didn’t require a GPDR notice and was easy to deploy and simple to use. My initial search for self-hosted web analytics tools led me to Plausible and Matomo. Either would have worked, but I’m hosting on a small VPS and wanted to keep things lightweight so installing docker for Plausible or PHP for Matomo were not ideal for my use case. I know the language a tool is written in shouldn’t be the most important factor when choosing a tool, but I’ve had good luck with tools written in Go or Rust hitting the sweet spot for simplicity, features, and ease of deployment. So I searched google for “golang web analytics” and GoatCounter came up near the top.

GoatCounter checked my boxes for feature set, simplicity, and ease of deployment. It can be deployed with a single binary file and a systemd service unit file, but I also connected it to an existing Postgresql database that I use for Commento already to get a little (unnecessary) performance boost without much extra complexity over the default SQLite deployment.

All of this is being performed on a virtual private server running Debian 11 (Bullseye) hosted by Linode. Replace danmc.net with your domain as appropriate.

Setup User and Database

First setup a new user to run the service:

sudo adduser goatcounter

This creates a new system user and group and creates a home directory /home/goatcounter/.

Next, create a new database and user:

sudo -i -u postgres # this gets you into a command prompt as the postgres superuser
createuser --interactive --pwprompt goatcounter  # this creates the new postgres user
# enter a password for the user and answer "n" for no to all of the questions.
createdb --owner goatcounter goatcounter  # this creates a new db owned by goatcounter
exit  # exit the postgres user login session

Install GoatCounter

Start a login session as user goatcounter:

sudo -i -u goatcounter
cd /home/goatcounter/
wget https://github.com/arp242/goatcounter/releases/download/v2.2.3/goatcounter-dev-linux-amd64.gz
# replace above with latest from https://github.com/arp242/goatcounter/releases
gzip -d goatcounter*
chmod +x goatcounter*  # make it executable
ln -s goatcounter* goatcounter-latest  # for easy updates without changing service file
./goatcounter-latest db create site -vhost stats.danmc.net -user.email YOUR_EMAIL -db "postgresql+user=goatcounter host=/run/postgresql dbname=goatcounter sslmode=disable"
# replace YOUR_EMAIL as appropriate. Enter a password that will be used along with your email to login.
exit  # exit the goatcounter login session

Setup systemd Service

Create a systemd service unit file to run goatcounter:

cat << EOF | sudo tee /etc/systemd/system/goatcounter.service
Description=goatcounter daemon service
After=network.target postgresql.service

ExecStart=/home/goatcounter/goatcounter-latest serve -listen localhost:8081 -tls http -db "postgresql+user=goatcounter host=/run/postgresql dbname=goatcounter sslmode=disable"


A few things to note:

Now start the service:

sudo systemctl daemon-reload  # tells systemd to check for new unit files
sudo systemctl enable goatcounter.service  # make it run on boot
sudo systemctl start goatcounter.service  # start it now
sudo systemctl status goatcounter.service  # check for errors

The service should now be running on http://localhost:8081.

NGINX and LetsEncrypt Setup

I won’t go thru a detailed NGINX setup in this post but briefly:

sudo apt install nginx-extras certbot python3-certbot-nginx
# nginx can probably be used instead of nginx-extras if you want
cat << EOF | sudo tee /etc/nginx/sites-available/danmc.net
server {
    server_name www.danmc.net;
    return 301 https://danmc.net$request_uri;
server {
    server_name danmc.net;
    location / {
            root /srv/www/danmc.net/public;
            index index.html;
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/danmc.net /etc/nginx/sites-enabled/
sudo nginx -t  # to check that config is fine
sudo systemctl reload nginx
# make sure your server firewall allows tcp ports 80 and 443
# make sure your DNS setup points both @ and www to your server
sudo certbot --nginx -d danmc.net -d www.danmc.net

Add a stub server section to your enabled NGINX config:

cat << EOF | sudo tee -a /etc/nginx/sites-available/danmc.net
server {
    server_name stats.danmc.net;
    location / {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $http_host;
    location /loader {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $http_host;

            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 86400;

Note the “location /loader {…}” section. It is required to allow the websocket connection for the dashboard to be properly proxied.

In your DNS config (usually through the website of whoever you purchased your domain name from, e.g., namecheap.com), add an A record to point the stats sub-domain to your server. Wait an appropriate amount of time for the DNS to propagate (5-30 minutes is normally enough), then:

sudo certbot --nginx -d stats.danmc.net

That should install a new cert and auto-update and reload your nginx config.

Finishing Touches

Go to https://stats.danmc.net and login to GoatCounter with the email and password specified above. The dashboard will show the <script> tag to add to the template for all of your website pages. I added it to the bottom of my footer snippet; this ensures the script is loaded on every page.

After redeploying your site, try visiting a page on your site, then come back and refresh the dashboard. You should see the page hits registered.

I also went into settings and there is a setting for “Ignore IPs”. I added my home IP to this so my visits do not get tracked. A few other settings:

Thats it. Happy tracking!