How Umami Works
Umami is a self-hosted analytics platform built with Next.js. It collects data through a JavaScript tracker (script.js, roughly 2KB gzipped) that sends POST requests to a /api/send endpoint. Each request contains the page URL, referrer, screen dimensions, browser language, and a hashed session identifier.
Umami does not set cookies. Visitor uniqueness is calculated by hashing the IP address, User-Agent, hostname, and a rotating salt together. The hash changes daily, so Umami cannot recognize returning visitors across days. No personally identifiable information is stored in the database.
The data model consists of:
- Sessions -- Identified by the daily hash. Contains browser, OS, device type, country, and language.
- Pageviews -- URL path, referrer URL, and timestamp tied to a session.
- Events -- Named custom events with optional JSON data properties, tied to a session.
All data is stored in PostgreSQL (recommended) or MySQL. Each tracked site is identified by a website ID (UUID), which is embedded in the tracking script.
Self-Hosted Deployment
Docker Compose
Umami provides an official Docker image. Create a docker-compose.yml:
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://umami:${DB_PASS}@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: ${APP_SECRET}
# Optional: disable telemetry
DISABLE_TELEMETRY: 1
depends_on:
db:
condition: service_healthy
restart: always
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${DB_PASS}
volumes:
- umami_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 5s
timeout: 5s
retries: 5
restart: always
volumes:
umami_db:
Start with:
export DB_PASS=$(openssl rand -hex 16)
export APP_SECRET=$(openssl rand -hex 32)
docker compose up -d
Default login: admin / umami. Change the password immediately after first login.
Environment Variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL or MySQL connection string |
DATABASE_TYPE |
Yes | postgresql or mysql |
APP_SECRET |
Yes | Secret for hashing, minimum 32 characters |
DISABLE_TELEMETRY |
No | Set to 1 to disable anonymous telemetry |
TRACKER_SCRIPT_NAME |
No | Custom filename for tracker (default: script.js) |
COLLECT_API_ENDPOINT |
No | Custom collection endpoint path (default: /api/send) |
DISABLE_BOT_CHECK |
No | Set to 1 to allow bot traffic |
DISABLE_UPDATES |
No | Set to 1 to skip update checks |
CORS_MAX_AGE |
No | CORS preflight cache duration in seconds |
Reverse Proxy
Place behind Nginx or Caddy for TLS termination:
# Caddy
analytics.example.com {
reverse_proxy localhost:3000
}
# Nginx
server {
listen 443 ssl;
server_name analytics.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
MySQL Backend
Replace the image tag and database URL:
umami:
image: ghcr.io/umami-software/umami:mysql-latest
environment:
DATABASE_URL: mysql://umami:${DB_PASS}@db:3306/umami
DATABASE_TYPE: mysql
Upgrading
docker compose pull
docker compose up -d
Umami applies database migrations automatically on startup.
Installing the Tracking Script
After creating a website in the Umami dashboard, you receive a website ID (UUID). Add the tracking script to your pages:
<script defer
src="https://analytics.example.com/script.js"
data-website-id="b59e9c65-ae32-4baa-87cf-1a6e508e3f45">
</script>
Script Attributes
| Attribute | Description |
|---|---|
data-website-id |
Required. UUID of the website in Umami |
data-auto-track |
Set to false to disable automatic pageview tracking |
data-host-url |
Override the collection endpoint URL |
data-domains |
Comma-separated list of domains to track (ignore others) |
data-tag |
Tag for filtering in the dashboard |
Excluding Your Own Visits
Add data-domains to restrict tracking to production domains:
<script defer
src="https://analytics.example.com/script.js"
data-website-id="YOUR_ID"
data-domains="example.com,www.example.com">
</script>
Visits from localhost or staging domains are ignored.
Custom Script Filename
Rename the tracker to avoid ad-blocker detection. Set the environment variable:
TRACKER_SCRIPT_NAME=stats.js
Then reference the new filename:
<script defer src="https://analytics.example.com/stats.js"
data-website-id="YOUR_ID"></script>
Event Tracking
umami.track() API
Umami exposes a global umami.track() function for custom events:
// Track a named event
umami.track('signup-click');
// Track an event with properties
umami.track('purchase', {
product: 'Pro Plan',
price: 49.99,
currency: 'USD'
});
// Track a custom pageview (virtual page)
umami.track({ url: '/virtual/thank-you', title: 'Thank You' });
// Override page data on an event
umami.track('form-submit', {
url: '/contact',
title: 'Contact Form'
});
Event names are case-sensitive. Properties are stored as JSON and can be queried via the API.
Data Attributes (No-Code Events)
Track clicks without writing JavaScript using HTML data attributes:
<button data-umami-event="signup-click">Sign Up</button>
<!-- With properties -->
<a href="/pricing"
data-umami-event="pricing-click"
data-umami-event-plan="enterprise">
View Enterprise Plan
</a>
Any element with a data-umami-event attribute triggers an event on click. Additional data-umami-event-* attributes become event properties.
SPA Tracking
Umami automatically detects pushState and replaceState calls. For frameworks using the History API (React Router, Next.js, Vue Router), pageviews are tracked on route changes without configuration.
For hash-based routing, Umami tracks changes to location.hash by default.
Disabling Auto-Tracking
If you want full manual control:
<script defer
src="https://analytics.example.com/script.js"
data-website-id="YOUR_ID"
data-auto-track="false">
</script>
<script>
// Manually track pageviews and events
umami.track(); // Track current page
umami.track('custom-event');
</script>
REST API
Umami provides a REST API for reading analytics data and managing resources. Authentication uses a bearer token obtained from the login endpoint.
Authentication
# Get auth token
TOKEN=$(curl -s -X POST https://analytics.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"YOUR_PASSWORD"}' | jq -r '.token')
Website Stats
# Get aggregate stats for a website
curl "https://analytics.example.com/api/websites/WEBSITE_ID/stats?\
startAt=1704067200000&\
endAt=1706745600000" \
-H "Authorization: Bearer $TOKEN"
Response:
{
"pageviews": { "value": 45230, "prev": 38102 },
"visitors": { "value": 12543, "prev": 10891 },
"visits": { "value": 18320, "prev": 15400 },
"bounces": { "value": 7800, "prev": 6900 },
"totaltime": { "value": 2847000, "prev": 2340000 }
}
Timestamps are Unix milliseconds.
Pageview Data
# Pageviews over time
curl "https://analytics.example.com/api/websites/WEBSITE_ID/pageviews?\
startAt=1704067200000&\
endAt=1706745600000&\
unit=day" \
-H "Authorization: Bearer $TOKEN"
Metrics Breakdown
# Top pages
curl "https://analytics.example.com/api/websites/WEBSITE_ID/metrics?\
startAt=1704067200000&\
endAt=1706745600000&\
type=url" \
-H "Authorization: Bearer $TOKEN"
# Top referrers
curl "https://analytics.example.com/api/websites/WEBSITE_ID/metrics?\
startAt=1704067200000&\
endAt=1706745600000&\
type=referrer" \
-H "Authorization: Bearer $TOKEN"
Valid type values: url, referrer, browser, os, device, country, event.
Event Data
# Get custom events
curl "https://analytics.example.com/api/websites/WEBSITE_ID/events?\
startAt=1704067200000&\
endAt=1706745600000&\
unit=day" \
-H "Authorization: Bearer $TOKEN"
# Event name breakdown
curl "https://analytics.example.com/api/websites/WEBSITE_ID/metrics?\
startAt=1704067200000&\
endAt=1706745600000&\
type=event" \
-H "Authorization: Bearer $TOKEN"
Managing Websites
# List all websites
curl "https://analytics.example.com/api/websites" \
-H "Authorization: Bearer $TOKEN"
# Create a website
curl -X POST "https://analytics.example.com/api/websites" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"My Site","domain":"example.com"}'
# Get share URL token
curl "https://analytics.example.com/api/websites/WEBSITE_ID" \
-H "Authorization: Bearer $TOKEN"
Share URLs
Umami generates public share URLs that display a read-only dashboard without authentication. Enable sharing in the website settings. The share URL format:
https://analytics.example.com/share/SHARE_TOKEN/Dashboard
Common Issues
Tracker script blocked by ad blockers:
Rename the script file using TRACKER_SCRIPT_NAME=stats.js and the endpoint using COLLECT_API_ENDPOINT=/api/collect. Update the <script> tag accordingly.
Events not appearing in dashboard:
Event names in umami.track('name') must match exactly what appears in the dashboard filter. Check for typos and case sensitivity. Events sent before the script loads are dropped.
Visitor counts seem low: Without cookies, Umami resets visitor identity daily. The same person visiting Monday and Tuesday counts as two visitors. This is by design. Compare unique visitors at weekly or monthly granularity for more stable numbers.
Database growing large:
For high-traffic sites, Umami stores one row per pageview and event. On PostgreSQL, run periodic VACUUM ANALYZE and consider partitioning the website_event table by date. Set up automated cleanup with:
DELETE FROM website_event
WHERE created_at < NOW() - INTERVAL '365 days';
umami.track() not defined:
The script loads with defer. If you call umami.track() in an inline script before the tracker loads, wrap it in a load handler:
window.addEventListener('load', () => {
umami.track('page-loaded');
});
Docker container fails to start:
Check that DATABASE_URL includes the correct credentials and that the database container is healthy. Umami runs Prisma migrations on startup -- if the database is unreachable, the container exits immediately.
Platform-Specific Considerations
PostgreSQL vs MySQL:
PostgreSQL is recommended for better JSON query performance on event properties. MySQL support uses a separate Docker image (mysql-latest tag). Schema and migrations differ between the two -- you cannot switch databases after initial setup without a data migration.
Referrer Tracking Without Cookies:
Umami captures the document.referrer value on each pageview. Since there are no cookies, referrer attribution is per-pageview rather than per-session. If a visitor arrives from Google and then navigates to three pages, only the first pageview has a referrer value.
Team and Role Management: Umami supports three roles: Admin (full access), User (view all sites), and View-Only (specific sites). Create teams in the dashboard and assign websites to team members. API tokens are scoped to the authenticated user's permissions.
Horizontal Scaling: Run multiple Umami containers behind a load balancer. All instances must share the same PostgreSQL database. There is no inter-process state -- the app is stateless. For high-write scenarios, ensure PostgreSQL has connection pooling (PgBouncer) configured.
Server-Side Event Collection: Send events without the JavaScript tracker by POSTing directly to the collection endpoint:
curl -X POST https://analytics.example.com/api/send \
-H "Content-Type: application/json" \
-H "User-Agent: Mozilla/5.0..." \
-d '{
"payload": {
"hostname": "example.com",
"language": "en-US",
"referrer": "",
"screen": "1920x1080",
"title": "Order Complete",
"url": "/order/complete",
"website": "WEBSITE_ID",
"name": "purchase"
},
"type": "event"
}'
The User-Agent header is used for device detection. The hostname must match the website domain configured in Umami.