Skip to content

Self-host Umami analytics with Traefik

Published on

I’ve been looking for a free analytics solution for my blog (I’m cheap!) and stumbled across Umami. While they offer a paid managed option, their free self-hosted version caught my eye - especially since I already have a free Oracle Cloud VM lying around.

In this post, I’ll document how I set it up using Traefik as a reverse proxy. The post is primarily aimed at my future self, but if you find it useful, that’s great too!

Table of contents

Open Table of contents

Prerequisites

Set up Traefik

I was unaware of Traefik until Claude suggested it. Its ability to generate SSL certificates and awareness of Docker containers immediately caught my interest.

After years of using Nginx, I appreciate Traefik’s approach with everything organized in a single docker-compose file instead of complex configuration files.

I initially set up Traefik in the same docker-compose file as Umami, but I later moved it to a separate one. This way, I can use Traefik for other containers without having to duplicate the configuration.

Create Traefik configs

First, I had to create a directory structure for Traefik and its configuration files. It can be named whatever you like, but for this example, I have used ~/traefik.

The goal is to create the following structure:

~/traefik
├── certs
├── docker-compose.yml
├── dynamic
   └── default.yml
└── traefik.yml
See commands to create it
mkdir -p ~/traefik/dynamic
mkdir -p ~/traefik/certs
cd ~/traefik
touch acme.json docker-compose.yml traefik.yml

Then, I added a default config to ~/traefik/dynamic/default.yml. This configuration works with domains managed by Amazon Route 53:

tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true

Next, I added the following to ~/traefik/traefik.yml:

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    directory: "/etc/traefik/dynamic"
    watch: true

certificatesResolvers:
  myresolver:
    acme:
      email: your_email@example.com # replace this!
      storage: acme.json
      httpChallenge:
        entryPoint: web

log:
  level: "WARNING"

Most of the configuration is self-explanatory. The key element is the dynamic configuration folder (/etc/traefik/dynamic), which allows for custom container rules later on.

Then, I created the docker-compose.yml file to mount all the files and directories we created:

Tip

At this stage I am not using the certs directory, but in the future I might want to add custom certificates. So I am adding it now to avoid having to change the docker-compose file later.

---
services:
  traefik:
    image: traefik:v2.10
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ${XDG_RUNTIME_DIR}/docker.sock:/var/run/docker.sock:ro # for rootless docker
      - /home/ubuntu/traefik/acme.json:/acme.json:rw
      - /home/ubuntu/traefik/traefik.yml:/traefik.yml:ro
      - /home/ubuntu/traefik/dynamic:/etc/traefik/dynamic:ro
      - /home/ubuntu/traefik/certs:/etc/traefik/certs:ro
    networks:
      - web
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

networks:
  web:
    external: true # so that we can use it in other docker-compose files

Finally, we need to set the permissions for the acme.json file. This file will store the SSL certificates generated by Traefik.

chmod 600 acme.json

After creating the configuration files, create the web network and start Traefik:

docker network create web
docker compose up -d

Verify Traefik started correctly with:

docker compose logs -f

You should see something like this:

time="2025-03-09T18:10:20Z" level=info msg="Configuration loaded from file: /traefik.yml"

Set up Umami

Umami offers a docker-compose file in their GitHub repository, but I had to make a few changes to get it working with Traefik.

To start, I created a new directory for Umami:

mkdir ~/umami
cd ~/umami

Then, I created a new docker-compose.yml file with the following content:

---
services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    environment:
      DATABASE_URL: postgresql://umami:umami@db:5432/umami
      DATABASE_TYPE: postgresql
      APP_SECRET: your_secret_key # replace this!
    depends_on:
      db:
        condition: service_healthy
    init: true
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
      interval: 5s
      timeout: 5s
      retries: 5
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.umami.rule=Host(`your_domain.com`)" # replace this!
      - "traefik.http.routers.umami.entrypoints=websecure"
      - "traefik.http.routers.umami.tls.certresolver=myresolver"
      - "traefik.http.services.umami.loadbalancer.server.port=3000"
      - "traefik.docker.network=web"
      - "traefik.http.middlewares.umami-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.middlewares.umami-headers.headers.customrequestheaders.X-Real-IP={{.Request.RemoteAddr}}"
      - "traefik.http.routers.umami.middlewares=umami-headers@docker"
    networks:
      - web
      - default  # Keep the default network for communication with the DB
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: umami
    volumes:
      - /home/ubuntu/umami/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
    networks:
      - default
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

volumes:
  umami-db-data:

networks:
  web:
    external: true  # This connects to the external network used by Traefik
  default:  # This is the internal network for umami and its database

Note

Make sure to replace your_domain.com with your actual domain name and your_secret_key with a random string. You can generate a random string using the following command: openssl rand -hex 32

Finally, I started the Umami container:

docker compose up -d

Since I set up health checks for both the Umami and PostgreSQL containers, I can check if they are healthy by running:

docker compose ps

Which should show something like this:

NAME            IMAGE                                            COMMAND                  SERVICE   CREATED         STATUS                   PORTS
umami-db-1      postgres:15-alpine                               "docker-entrypoint.s…"   db        9 minutes ago   Up 9 minutes (healthy)   5432/tcp
umami-umami-1   ghcr.io/umami-software/umami:postgresql-latest   "docker-entrypoint.s…"   umami     9 minutes ago   Up 9 minutes (healthy)   3000/tcp

Access Umami

Once Umami is up and running, it should be accessible at https://your_domain.com. You can log in with the default credentials:

Warning

These credentials are only for the initial login and should be changed immediately.

Next steps

Next, I plan to set up recurring backups for the umami-db-data directory and acme.json file — a topic for another post.

I’m now tracking my blog’s traffic with zero monthly costs — mission accomplished! If this guide helped you implement your own Umami and Traefik setup, or if you have suggestions to improve it, let’s chat on X/Twitter or BlueSky.

Thanks for reading, and happy tracking!


Previous Post
How to Build a Node.js Lambda Layer with AWS CDK