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
- A VM with Ubuntu (tested on 24.04)
- A domain name
- Docker and Docker Compose installed
- If Docker is in rootless mode
- you need to allow non-root users to bind to port 80:
sudo sh -c 'echo "net.ipv4.ip_unprivileged_port_start=80" >> /etc/sysctl.conf' sudo sysctl -p
- and also set the port driver to
slirp4netns
to send the correct IP address in the request:
mkdir -p ~/.config/systemd/user/docker.service.d/ && \ echo -e '[Service]\nEnvironment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns"' > ~/.config/systemd/user/docker.service.d/override.conf systemctl --user daemon-reload systemctl --user restart docker
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.
- username
admin
- password
umami
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!