Umami is a personal website analytics tool that supports self-deployment. Using their hosted service, you can track up to three sites. I’ve also tried Plausible, but it seemed a bit heavy for my needs - system load increased significantly after using it.
Deployment
-
Create
docker-compose.ymland modify according to your situation. Here I integrated Cloudflared’s network for public internet access:services: umami: container_name: umami image: ghcr.io/umami-software/umami:postgresql-latest environment: DATABASE_URL: postgresql://<db_username>:<db_password>@umami-db:5432/umami DATABASE_TYPE: postgresql APP_SECRET: depends_on: umami-db: condition: service_healthy init: true restart: unless-stopped networks: - umami - cloudflared umami-db: container_name: umami-db image: postgres:15-alpine environment: POSTGRES_DB: umami POSTGRES_USER: POSTGRES_PASSWORD: volumes: - ./data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 5 networks: - umami networks: cloudflared: external: true umami: name: umami driver: bridge -
You can generate the secret using OpenSSL:
openssl rand -base64 32 -
Start the containers:
docker compose up -d -
Add the domain in your reverse proxy. Umami automatically listens on port 3000:
Subdomain: data Domain: your_domain Type: HTTP URL: umami:3000 -
Default username is
admin, default password isumami. You can change these directly through the web interface. -
After adding a website, go to Settings → Tracking Code to get the tracking code. My personal blog uses the Hugo theme, so I paste the tracking code into
layouts/partials/head.html.
The final result looks great! Unfortunately, previous analytics data wasn’t recorded. You can preview the dashboard here: Umami
Embedding into Blog Frontend
This requires a three-tier proxy architecture: the frontend script embeds code in script.js, which calls the umami-api interface, and the API service uses Umami’s token to call Umami and return data to the frontend, which then displays it. I wrote a small service for this: GitHub - Kyxie/umami-api.
-
Create a Team in Umami and move your blog to that Team.
-
Create a new user with limited permissions (e.g., view-only) and add them to the newly created Team. It’s best not to use a high-privilege user, as token leakage could be dangerous.
-
Use Postman or curl to send a request. For username, use the credentials of the new user:
curl -X POST https://umami_url/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"username", "password":"password"}' -
You’ll receive a token in the response body.
-
Create a new hostname in Cloudflared. We need umami-api accessible from the public internet:
Subdomain: umami-api domain: your_domain type: HTTP URL: umami-api:3001 -
Create a
.envfile:UMAMI_URL=[your_umami_url] UMAMI_TOKEN=[your_token] UMAMI_WEBSITE_ID=[your_website_id] CORS_ALLOW_ORIGIN=[your_blog_url]- UMAMI_URL is the Umami URL. Since the API and Umami are in the same container network, you can use the container name (http://umami:3000)
- UMAMI_TOKEN is the token you just generated
- UMAMI_WEBSITE_ID is the website’s ID in Umami
- CORS_ALLOW_ORIGIN is the URL allowed to access the service (e.g., your blog). Multiple URLs can be separated by commas.
-
Create
docker-compose.yml:umami-api: image: kyxie/umami-api:latest container_name: umami-api env_file: .env restart: unless-stopped networks: - umami - cloudflaredAdd this section to your umami stack and include it in the cloudflared container network.
-
Start the containers:
docker compose up -d