monotux.tech


Goatcounter on fly.io

fly.io, sqlite, LiteStream, GoatCounter

Yet another container running on fly.io, this time a web analytics platform called goatcounter, backed by a LiteStream replication for disaster recovery.

This time I’ve setup a repository (inspired by fspoettel/linkding-on-fly) for setting this up, which was a lot of fun! I hope this might be useful for someone else.

I’ve been running goatcounter for my two blogs for a few months, and it’s been great on the free tier (1 shared CPU, 256 MB of RAM). Sure, my blog doesn’t receive a lot of visitors1 but if you need performance, you are probably not looking at the fly.io free tier :-)

Overview #

This setup works like this:

  1. Goatcounter runs as a FireCracker vm on fly.io, backed by a SQLite database2
  2. LiteStream wraps the goatcounter process3 and continuously reads and backs up the wal to a S3 compatible storage

If our flyio volume would die, or we would migrate to another VM provider, we should be able to automatically restore our data to the new host. I’ve tried to write entrypoint.sh to do this automatically, and through my happy testing this seems to work. :-)

Beyond that, I think I’ve outlined the procedure in the project readme, which I’ll just copy in below.

Prerequisites #

Usage #

  1. Clone or fork this repository.

    git clone https://github.com/oscarcarlsson/goatcounter-on-fly.git && cd goatcounter-on-fly
    
  2. Run the command below and answer the questions - you don’t need Redis nor Postgres! This will generate a fly.toml but won’t try to launch anything yet. Make sure you change the name for the application as well.

    flyctl launch --no-deploy --memory 256 --name CHANGEME
    

    If you want, you can always specify the region to launch the VM in. You can list all regions by running: flyctl platform regions

  3. Create a volume for the database, change to your preferred region:

    flyctl volumes create goatcounter_data --region CHANGEME --size 1
    
  4. Open fly.toml and add the following to the env section:

    [env]
      # Change these!
      GOATCOUNTER_DOMAIN = 'goatcounter.example.com'
      GOATCOUNTER_EMAIL = 'john.doe@example.com'
    
      # Change these for your S3 provider!
      LITESTREAM_REPLICA_ENDPOINT = ''
      LITESTREAM_REPLICA_BUCKET = ''
    
      # These can be blank
      GOATCOUNTER_SMTP = ''
      GOATCOUNTER_DEBUG = ''
    
      # Don't change these
      GOATCOUNTER_LISTEN = '0.0.0.0:8080'
      GOATCOUNTER_DB = '/data/goatcounter.sqlite3'
      LITESTREAM_REPLICA_PATH = 'goatcounter_replica.sqlite3'
    

    Also make sure that the mount section looks something like this:

    [[mounts]]
    source = 'goatcounter_data'
    destination = '/data'
    processes = ['app']
    
  5. Nearly done! Now, create secrets for your secrets:

    flyctl secrets set GOATCOUNTER_PASSWORD="changeme" LITESTREAM_ACCESS_KEY_ID="changeme" LITESTREAM_SECRET_ACCESS_KEY="changeme"
    

    The secret called GOATCOUNTER_PASSWORD will be the initial password for the admin account.

  6. Now, try to deploy the application!

    flyctl deploy && flyctl logs
    

    If nothing goes wrong, visit your goatcounter instance and login using admin and the password you’ve set using the GOATCOUNTER_PASSWORD secret.

Hugo integration #

Exactly how you plug this into your theme will be left as an exercise for the reader, but what you need to do is include below in your header somehow:

<!-- Replace with your goatcounter DNS... -->
<script data-goatcounter="https://stats.monotux.tech/count" async src="//stats.monotux.tech/count.js"></script>

In my custom theme I have a configuration parameter called customJS, into which I’ve just added the above.

Conclusion #

Hopefully this might help someone else to setup a goatcounter instance on fly.io.


  1. At least I know how few visitors I go get :-) ↩︎

  2. This is persisted to a FlyIO volume, even though we in theory could skip this as the data is easy to automagically restore from backups! ↩︎

  3. As described in the LiteStream documentation for running everything in one container ↩︎