skip to content
Aaron Becker

Self-Hosted Site Analytics with Umami, Docker, and Traefik

/ 10 min read

why self-host analytics?

Website analytics fulfill a basic human need to feel seen and appreciated. It’s validating to know that there are people who visit your site, and it’s useful to know what they’re taking the time to read so you can focus on making your site more valuable to them.

Google Analytics is ubiquitous but invasive

Most content publishers just use Google Analytics to track website traffic. GA is free, easy to set up, and widely supported. Google makes it easy to integrate GA because they want your data. Using Google Analytics compromises your users’ privacy because Google can track them across the web, even if they’re not on your site.

GA uses cookies, which requires you to show a cookie consent banner to comply with laws in the EU, California, and elsewhere. Because it’s so invasive, Google Analytics is commonly blocked by ad-blockers and privacy-conscious browsers, making the data it collects less accurate.

I do want an occasional pat on the back, but I’d like to avoid being complicit in Google’s surveillance panopticon if at all possible.

self-hosting to the rescue

Fortunately, stroking your ego need not come at the price of selling out your users’ privacy. As an added benefit, your self-hosted analytic server is less likely to be blocked by privacy-conscious users, so your visitor counts are more likely to be accurate. If you have a server running Docker, you have a bevy of privacy-focused, open source analytics solutions to choose from.

In a previous post, I compared Umami, Plausible, and Matomo, three of the most popular self-hosted Google Analytics alternatives, deciding that Umami was the best choice for my needs. In this post I’ll walk you through the process of integrating an analytics solution, in my case Umami, into a self-hosted stack built with docker compose and fronted by a traefik reverse proxy.

prerequisites & overview

In this guide I assume that you:

  • Have a domain name, with the ability to add DNS records.
  • Have a server running Docker, docker compose, and Traefik, with a basic understanding of how to use them.
  • Have one or more websites to track that you can add the Umami tracking script to.

There are five key configuration stages for setting up self-hosted analytics:

  1. Adding DNS records for your analytics endpoint.
  2. Configuring the docker compose stack that will run the Umami analytics service.
  3. Deploying the stack to your server and setting up the admin account in the Umami dashboard.
  4. Registering your site with Umami and getting the tracking code.
  5. Adding the analytics script to your website(s).

step 1: DNS configuration

You will need to add a DNS record for the subdomain or domain at which you plan to access your analytics endpoint and dashboard. Your analytics endpoint needs a public URL to receive data from the scripts that run in client browsers. Using a subdomain is common because it foregoes the expense of an additional domain. Moreover, ad blockers seem less likely to block API traffic when the endpoint is on the same root domain as your website.

If you only plan to use a single server, a wildcard DNS record that directs all subdomains to your server will work. Here’s some example documentation from namecheap on how to set up a wildcard subdomain DNS record. I won’t cover this in detail because specifics depend on your hosting and DNS providers (often the same).

step 2: docker compose stack

I used Umami’s example docker compose file as a starting point. I placed this docker-compose.yml file in a new directory called umami-analytics inside the git repository where I keep my docker compose stacks.

I opted to include environment variables directly in the docker compose file rather than in a separate .env file since there are no API keys or other sensitive information that I would want to keep out of the repo. Comments in the docker compose file explain some of the choices I made.

umami-analytics/compose.yml
# The `traefik` network allows containers to connect with the Traefik reverse proxy (and from there to the internet).
# It must already exist; I create the network as part of a bash script that brings my server infrastructure online.
networks:
traefik:
name: traefik
external: true
volumes:
# this volume is used to persist the PostgreSQL database data.
umami-db-data:
services:
umami:
# since we're only ever going to have one instance of this container, we give it a static name.
container_name: umami
image: ghcr.io/umami-software/umami:postgresql-latest
# `default` network for this stack only allows communication between umami and db containers.
networks:
- traefik
- default
# we don't bind an external port b/c traefik handles routing to the container;
# expose tells Traefik which port on the container to route traffic to.
expose:
- 3000
environment:
# `DATABASE_URL` is a prisma connection string.
# this database URL must match the `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD` environment variables defined on the db container.
# 5432 is the default port for PostgreSQL.
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
# `APP_SECRET` must contain a random string that you come up with. It's used to generate unique values.
# In older versions of Umami, this variable was called `HASH_SALT`.
# You can run the terminal command `openssl rand -base64 32` to generate a 32-character random string.
APP_SECRET: <your random string here>
# `COLLECT_API_ENDPOINT` and `TRACKER_SCRIPT_NAME` help evade ad blockers.
# Generic names are less likely to be blocked.
# see https://umami.is/docs/environment-variables and https://umami.is/docs/bypass-ad-blockers for more info.
# `COLLECT_API_ENDPOINT` renames the endpoint that scripts send data to from `/api/send` to COLLECT_API_ENDPOINT. Will be appended to analytics dashboard URL.
COLLECT_API_ENDPOINT: /api/get
# `TRACKER_SCRIPT_NAME` renames the tracker script from `umami.js` to TRACKER_SCRIPT_NAME. Should include `.js` extension.
TRACKER_SCRIPT_NAME: fetch.js
# TRAEFIK LABELS
labels:
traefik.enable: "true"
# SUBDOMAIN IS DEFINED HERE. You can use any subdomain that's not already in use.
# As mentioned above, generic names are less likely to be blocked by ad blockers.
# replace YOUR_DOMAIN.TLD with your actual domain!
traefik.http.routers.umami.rule: "Host(`stats.YOUR_DOMAIN.TLD`)"
# You should have a global HTTP => HTTPS redirect enabled in your Traefik configuration.
# See tip below for notes on setting up Traefik.
traefik.http.routers.umami.entrypoints: "websecure"
# This tells Traefik to use the certificate resolver that you set up in your Traefik configuration.
# I used the name from a tutorial and haven't bothered to change it.
traefik.http.routers.umami.tls.certResolver: "myResolver"
# I have a compression middleware set up; otherwise the tracker script is not compressed.
# See tip below for notes on setting up Traefik.
traefik.http.routers.umami.middlewares: "compress-gzip@file"
depends_on:
db:
condition: service_healthy
restart: always
# umami ships with a `/api/heartbeat` endpoint,
# which returns a 200 status code if the service is healthy.
# this command will be run by the docker daemon within the container's own context.
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
db:
image: postgres:15-alpine
# `default` network, for communication within this compose stack only.
networks:
- default
environment:
# these environment variables must be kept in sync with the `DATABASE_URL` environment variable defined on the umami container.
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5

step 3: deploy the stack

I use a bash script to deploy this stack, but the process essentially consists of SSHing into the server, navigating to the umami-analytics directory, and running docker compose up -d. I use git with SSH agent forwarding to allow me to remote into my machine and pull changes from the repo without keeping my GitHub private key on the server. See the section on deploying my astro blog for details and an example script.

step 4: register your site with Umami and get the tracking code

Follow these steps from the Umami documentation to add your site to your new analytics dashboard:

  1. Log in to the Umami dashboard (with your updated admin password).
  2. Click on Settings in the header.
  3. Navigate to Websites on the left and click on the Add Website button.
  4. Enter a name and domain for your site. The name doesn’t have to be the same as the domain, but it can be, or it can be something descriptive like My Blog. Domain should NOT include the protocol (http or https), only the domain name.

Once you’ve added your site, it’ll appear in the list of websites under Settings. Click on the Edit button next to your site and copy the Tracking code to add to your site’s <head> section.

Umami tracking code example

step 5: add the analytics script to your website(s)

You’ll need to add the tracking code you copied in the previous step to the <head> section of each page that you want to track. Details will vary depending on your site’s framework. In my Astro blog, I added the code to the BaseHead.astro component, which generates the <head> metadata for every page:

astro_blog/src/components/BaseHead.astro
{
import.meta.env.PROD && (
<>
{/* Analytics: self-hosted umami;
your tracker code will contain actual values for STATS_SCRIPT_URL and STATS_WEBSITE_ID (redacted here for security) */}
<script defer src="{STATS_SCRIPT_URL}" data-website-id="{STATS_WEBSITE_ID}"></script>
</>
)
}

conclusion

Umami has a clean, privacy-focused design that doesn’t burden your users or server with heavy dependencies. By allowing you to rename the tracker script and the API endpoint, it helps evade ad blockers and ensures your visitor stats are more complete and accurate than other open source analytics solutions.

Umami’s support for Docker and docker compose makes it easy to integrate into a self-hosted stack. Traefik makes it easy to route traffic to the Umami container and automatically handles HTTPS and even compression. Integrating the tracking code into your site is as easy as adding a script tag to your root layout or page template.

Hopefully this post has shown you that the technical barriers to hosting your own analytics platform are low enough that you can do it yourself if you’re so inclined. You can own and process your own traffic data without relying on third parties to do it for you. You don’t have to settle for Google Analytics!