The source for the site at
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

99 lines
5.9 KiB

  1. ---
  2. title: Hosting Your Own Goatcounter Behind Nginx (on Ubuntu)
  3. date: 2020-08-26
  4. layout: post
  5. tags:
  6. - software
  7. - sysadmin
  8. - nginx
  9. - self-hosting
  10. description: "How I set up Goatcounter analytics behind Nginx on Ubuntu for my personal site"
  11. ---
  12. 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.
  13. ---
  14. 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.
  15. ## Client
  16. 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:
  17. ```html
  18. <script async src="/static/count.js?v=1.3.2" data-goatcounter=""></script>
  19. ```
  20. 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.
  21. Now. The server.
  22. ## Server
  23. 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.
  24. Here's `/etc/nginx/sites-available/goatcounter.conf`:
  25. ```
  26. server {
  27. listen 80;
  28. listen [::]:80;
  29. server_name;
  30. location ~* {
  31. return 301 https://$server_name$request_uri;
  32. }
  33. }
  34. server {
  35. listen 443 ssl http2;
  36. listen [::]:443 ssl http2;
  37. server_name;
  38. ssl_certificate /etc/letsencrypt/live/;
  39. ssl_certificate_key /etc/letsencrypt/live/;
  40. include snippets/ssl-params.conf;
  41. access_log /var/log/nginx/stats.access.log test;
  42. error_log /var/log/nginx/stats.error.log debug;
  43. location / {
  44. proxy_set_header Host $host;
  45. proxy_pass http://localhost:8085;
  46. }
  47. }
  48. ```
  49. A few notes on this:
  50. * I have a wildcard cert from [Let's Encrypt]( / [Certbot]( for this domain, and all traffic should be handled via https
  51. * I had to add a DNS record for the subdomain.
  52. * 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.
  53. * You can choose any port that's open I guess; Goatcounter's default is 8081.
  54. Then all that's left for this side of setup is `ln -s /etc/nginx/sites-{available,enabled}/goatcounter.conf` and `service nginx reload`.
  55. 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`:
  56. ```ini
  57. [Unit]
  58. Description="simple opensource analytics server - cf"
  59. After=nginx
  60. [Service]
  61. Type=simple
  62. ExecStart=/usr/local/bin/goatcounter serve \
  63. -listen localhost:8085 \
  64. -db "sqlite://home/noah/db/goatcounter.sqlite3" \
  65. -tls none \
  66. -port 8085
  67. Restart=always
  68. RestartSec=10
  69. [Install]
  71. ```
  72. 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!
  73. ## Conclusion
  74. 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](