Umami Analytics: Simple, Fast, Privacy-Focused Website Analytics
Deploy Umami Analytics for lightweight, self-hosted web analytics. Fast setup, real-time data, and GDPR compliant tracking.
Introduction
Umami (Japanese for “pleasant taste”) delivers exactly what its name suggests: analytics that are simple, clean, and satisfying to use. Born from frustration with bloated analytics platforms, Umami offers a refreshingly minimal approach without sacrificing essential features.
With over 20,000 GitHub stars and powering analytics for thousands of websites, Umami has become the go-to choice for developers who want straightforward analytics without complexity or privacy concerns.
What You’ll Learn:
- Deploy Umami with Docker in under 20 minutes
- Install tracking on any website or app
- Create real-time dashboards with custom metrics
- Set up event tracking for user interactions
- Share analytics with team members or clients
- Configure multi-website tracking from one installation
Prerequisites:
- Docker and Docker Compose installed (v20.10+ recommended)
- Basic command-line knowledge
- 500MB+ free disk space (1-2GB for multiple sites)
- (Optional) Domain name for SSL access
Why Umami Analytics?
The Umami Philosophy
Umami was built on three core principles:
- Simplicity: No clutter, just the metrics that matter
- Speed: Blazing fast interface and sub-1KB tracking script
- Privacy: No cookies, no personal data, fully compliant
Key Advantages
Developer-Friendly:
- Clean, modern interface built with React and Next.js
- Simple API for custom integrations
- One-line installation for most frameworks
- Real-time updates without page refresh
Lightweight Performance:
- Tracking script under 2KB (compressed)
- PostgreSQL or MySQL backend (your choice)
- Minimal server requirements (runs on Raspberry Pi)
- Fast dashboard loads even with millions of events
Privacy-First:
- Cookie-free tracking (no consent banners needed)
- GDPR, CCPA, and PECR compliant
- IP addresses hashed and anonymized
- No cross-site tracking or fingerprinting
Multi-Tenant Ready:
- Track unlimited websites from one installation
- Team collaboration with role-based access
- Share individual dashboards publicly or privately
- Client reporting with white-label options
Feature Comparison
| Feature | Umami | Plausible | Matomo | Google Analytics |
|---|---|---|---|---|
| Script Size | 2KB | 1KB | 22KB | 45KB+ |
| Setup Time | 15 min | 30 min | 45 min | 10 min |
| Real-time | Yes | Yes | Yes | Delayed |
| Multi-site | Unlimited | Unlimited | Unlimited | Limited |
| Team Features | Yes | Limited | Yes | Yes |
| Event Tracking | Built-in | Built-in | Advanced | Complex |
| API Access | Full REST | Full REST | Partial | Limited |
| Self-hosted | Yes | Yes | Yes | No |
Understanding Umami Architecture
System Components
Website → Tracking Script (2KB) → Umami API → Database (PostgreSQL/MySQL)
↓
Dashboard (Next.js) Stack:
- Frontend: Next.js (React) with real-time updates
- Backend: Node.js API with efficient data processing
- Database: PostgreSQL (recommended) or MySQL
- Tracker: Vanilla JavaScript, framework-agnostic
What Gets Tracked:
- Page URLs and referrers
- Browser, OS, and device type
- Country from IP (IP not stored)
- Screen resolution categories
- Custom events you define
Privacy Features:
- IP addresses salted, hashed, and never stored
- Daily salts ensure no long-term visitor identification
- No cookies or persistent storage
- No third-party requests
Installation: Docker Compose (Recommended)
Step 1: Create Project Directory
mkdir -p ~/umami
cd ~/umami Step 2: Create docker-compose.yml
Production-ready configuration:
version: '3'
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: replace-me-with-random-string
# Optional: Enable cloud storage for backups
# CLOUD_MODE: 1
# CLOUD_URL: https://cloud.umami.is
depends_on:
db:
condition: service_healthy
restart: always
networks:
- umami-network
db:
image: postgres:15-alpine
container_name: umami_db
environment:
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
networks:
- umami-network
volumes:
umami-db-data:
networks:
umami-network:
driver: bridge For MySQL instead:
services:
umami:
image: ghcr.io/umami-software/umami:mysql-latest
environment:
DATABASE_URL: mysql://umami:umami@db:3306/umami
DATABASE_TYPE: mysql
APP_SECRET: replace-me-with-random-string
db:
image: mysql:8
environment:
MYSQL_DATABASE: umami
MYSQL_USER: umami
MYSQL_PASSWORD: umami
MYSQL_ROOT_PASSWORD: root-password Step 3: Generate APP_SECRET
# Generate secure random string
openssl rand -hex 32 Copy the output and replace replace-me-with-random-string in docker-compose.yml
Step 4: Start Umami
# Start all services
docker compose up -d
# Check status
docker compose ps
# Watch initialization logs
docker compose logs -f umami Expected output:
umami | Loaded env from .env
umami | ready - started server on 0.0.0.0:3000, url: http://localhost:3000
umami | Database connection established. Step 5: Access Dashboard
Navigate to: http://YOUR_SERVER_IP:3000
Default credentials:
- Username:
admin - Password:
umami
IMPORTANT: Change the default password immediately!
- Log in with default credentials
- Click profile icon (top right)
- Select “Profile”
- Click “Change Password”
- Enter new secure password
- Save changes
Alternative Installation: MySQL Backend
If you prefer MySQL over PostgreSQL:
version: '3'
services:
umami:
image: ghcr.io/umami-software/umami:mysql-latest
container_name: umami
ports:
- "3000:3000"
environment:
DATABASE_URL: mysql://umami:umami@db:3306/umami
DATABASE_TYPE: mysql
APP_SECRET: your-generated-secret-here
depends_on:
- db
restart: always
db:
image: mysql:8
container_name: umami_mysql
environment:
MYSQL_ROOT_PASSWORD: your-root-password
MYSQL_DATABASE: umami
MYSQL_USER: umami
MYSQL_PASSWORD: umami
volumes:
- umami-mysql-data:/var/lib/mysql
restart: always
volumes:
umami-mysql-data: Adding Your First Website
Step 1: Create Website in Umami
- Log into Umami dashboard
- Click “Settings” in sidebar
- Select “Websites” tab
- Click ”+ Add website”
- Fill in details:
- Name: My Website
- Domain: example.com (without https://)
- Enable Share URL: Toggle if you want public sharing
- Click “Save”
Step 2: Get Tracking Code
- Click the website you just created
- Click “Edit” button
- Select “Tracking code” tab
- Copy the code snippet
Example tracking code:
<script defer src="http://YOUR_SERVER:3000/script.js" data-website-id="abc123def-4567-890-abc-def123456789"></script> Step 3: Install Tracking Code
HTML websites: Add before closing </head> tag:
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script defer src="https://analytics.yourdomain.com/script.js" data-website-id="abc123def-4567-890-abc-def123456789"></script>
</head>
<body>
<!-- Your content -->
</body>
</html> React/Next.js: Add to pages/_app.js or app/layout.js:
import Script from 'next/script'
export default function App({ Component, pageProps }) {
return (
<>
<Script
defer
src="https://analytics.yourdomain.com/script.js"
data-website-id="abc123def-4567-890-abc-def123456789"
/>
<Component {...pageProps} />
</>
)
} Vue.js: Add to public/index.html or use vue-meta:
// In main.js or App.vue
export default {
mounted() {
const script = document.createElement('script');
script.defer = true;
script.src = 'https://analytics.yourdomain.com/script.js';
script.setAttribute('data-website-id', 'abc123def-4567-890-abc-def123456789');
document.head.appendChild(script);
}
} WordPress: Use “Insert Headers and Footers” plugin or add to theme’s header.php:
<script defer src="https://analytics.yourdomain.com/script.js" data-website-id="abc123def-4567-890-abc-def123456789"></script> Ghost: Settings → Code Injection → Site Header
Gatsby: Install gatsby-plugin-umami:
npm install gatsby-plugin-umami // gatsby-config.js
module.exports = {
plugins: [
{
resolve: 'gatsby-plugin-umami',
options: {
websiteId: 'abc123def-4567-890-abc-def123456789',
srcUrl: 'https://analytics.yourdomain.com/script.js',
},
},
],
} Step 4: Verify Installation
Method 1: Real-time Dashboard
- Visit your website
- Open Umami dashboard
- Click “Realtime” in sidebar
- Should see your visit immediately
Method 2: Browser DevTools
- Visit your website
- Open DevTools (F12) → Network tab
- Search for “script.js”
- Should see 200 OK response
- Look for
/api/sendPOST request (the tracking beacon)
Method 3: Console Test In browser console on your website:
// Check if umami object exists
console.log(window.umami);
// Manually trigger test event
umami.track('test-event'); Custom Event Tracking
Track specific user actions beyond pageviews.
Basic Event Tracking
Button clicks:
<button onclick="umami.track('signup-button')">Sign Up</button> Form submissions:
<form onsubmit="umami.track('newsletter-signup'); return true;">
<input type="email" name="email" required>
<button type="submit">Subscribe</button>
</form> Link clicks:
<a href="/download.pdf" onclick="umami.track('download', { file: 'guide.pdf' })">
Download Guide
</a> Event Tracking with Properties
Track additional context with events:
// E-commerce purchase
umami.track('purchase', {
product: 'Premium Plan',
price: 49.99,
currency: 'USD'
});
// Video playback
umami.track('video-play', {
title: 'Product Demo',
duration: 180,
quality: '1080p'
});
// Feature usage
umami.track('feature-used', {
feature: 'export',
format: 'csv'
}); Tracking with React
import { useEffect } from 'react';
function PricingPage() {
// Track page view with custom data
useEffect(() => {
umami.track('pricing-page-view', {
source: 'nav-menu'
});
}, []);
const handlePurchase = (plan) => {
umami.track('purchase-started', {
plan: plan,
price: getPlanPrice(plan)
});
// Continue with purchase flow
};
return (
<button onClick={() => handlePurchase('pro')}>
Buy Pro Plan
</button>
);
} Tracking with Vue
export default {
methods: {
trackDownload(filename) {
window.umami.track('file-download', {
filename: filename,
timestamp: new Date().toISOString()
});
}
}
} Viewing Custom Events
- Go to your website’s dashboard
- Click “Events” tab
- See all tracked events with counts
- Click on event name to see event properties
- Filter by date range or event type
Team Collaboration
User Roles
Umami supports three user roles:
| Role | Permissions |
|---|---|
| Admin | Full system access, manage all users and websites |
| User | Access assigned websites, cannot create new sites |
| View Only | Read-only access to assigned websites |
Adding Team Members
- Log in as admin
- Go to “Settings” → “Users”
- Click ”+ Add user”
- Fill in details:
- Username: team-member
- Password: Generate secure password
- Role: Select appropriate role
- Click “Save”
- Assign websites to user:
- Go to “Websites” tab
- Click website → “Edit”
- Select “Team” tab
- Add user to team
- Choose access level (View/Edit)
Sharing Dashboards
Option 1: Share URL (Public)
- Go to website settings
- Enable “Enable Share URL”
- Copy the generated share URL
- Share with anyone (no login required)
Option 2: Team Access (Private)
- Add team members as users
- Assign them to specific websites
- They log in with their credentials
- See only their assigned websites
Share URL example:
https://analytics.yourdomain.com/share/abc123def/my-website Anyone with this URL can view analytics (read-only).
Reverse Proxy Configuration
Nginx Configuration
# /etc/nginx/sites-available/umami
server {
listen 443 ssl http2;
server_name analytics.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/analytics.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/analytics.yourdomain.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
}
}
# HTTP redirect
server {
listen 80;
server_name analytics.yourdomain.com;
return 301 https://$server_name$request_uri;
} Enable and reload:
sudo ln -s /etc/nginx/sites-available/umami /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx Caddy Configuration
# /etc/caddy/Caddyfile
analytics.yourdomain.com {
reverse_proxy localhost:3000
} Reload:
sudo systemctl reload caddy Traefik Configuration
Add labels to docker-compose.yml:
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.umami.rule=Host(`analytics.yourdomain.com`)"
- "traefik.http.routers.umami.entrypoints=websecure"
- "traefik.http.routers.umami.tls.certresolver=letsencrypt"
- "traefik.http.services.umami.loadbalancer.server.port=3000"
networks:
- traefik-proxy
- umami-network
networks:
traefik-proxy:
external: true
umami-network:
driver: bridge Advanced Features
Environment Variables
Complete list of configuration options for docker-compose.yml:
environment:
# Required
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: your-secret-key
# Optional
PORT: 3000 # Change default port
HOSTNAME: 0.0.0.0 # Bind address
DISABLE_LOGIN: 0 # Set to 1 to disable login (public mode)
DISABLE_UPDATES: 0 # Disable update checks
FORCE_SSL: 0 # Force HTTPS (set to 1 behind proxy)
LOG_QUERY: 0 # Log database queries (debug)
TRACKER_SCRIPT_NAME: script.js # Rename tracker script
COLLECT_API_ENDPOINT: /api/send # Rename collection endpoint
REMOVE_TRAILING_SLASH: 0 # Remove trailing slashes from URLs
CLIENT_IP_HEADER: X-Forwarded-For # Header for client IP (behind proxy) Custom Tracker Script Name
To avoid ad blockers, rename the tracking script:
environment:
TRACKER_SCRIPT_NAME: analytics.js
COLLECT_API_ENDPOINT: /api/collect Update tracking code:
<script defer src="https://analytics.yourdomain.com/analytics.js" data-website-id="..."></script> Proxying Through Your Domain
Serve Umami tracker from your main website domain:
Nginx configuration:
# On your main website server
location /stats/ {
proxy_pass https://analytics.yourdomain.com/;
proxy_set_header Host $host;
} Update tracking code:
<script defer src="/stats/script.js" data-website-id="..."></script> API Access
Umami provides a full REST API for automation.
Get API token:
- Settings → Profile → Generate API Key
- Copy the token
Example API calls:
# Get website stats
curl -X GET "https://analytics.yourdomain.com/api/websites/YOUR_WEBSITE_ID/stats" -H "Authorization: Bearer YOUR_API_TOKEN" -H "Content-Type: application/json"
# Get realtime data
curl -X GET "https://analytics.yourdomain.com/api/websites/YOUR_WEBSITE_ID/realtime" -H "Authorization: Bearer YOUR_API_TOKEN"
# Get events
curl -X GET "https://analytics.yourdomain.com/api/websites/YOUR_WEBSITE_ID/events?startAt=1640000000000&endAt=1650000000000" -H "Authorization: Bearer YOUR_API_TOKEN" Python example:
import requests
API_URL = "https://analytics.yourdomain.com/api"
API_TOKEN = "your-api-token"
WEBSITE_ID = "your-website-id"
headers = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
# Get website statistics
response = requests.get(
f"{API_URL}/websites/{WEBSITE_ID}/stats",
headers=headers,
params={
"startAt": 1640000000000,
"endAt": 1650000000000
}
)
stats = response.json()
print(f"Pageviews: {stats['pageviews']['value']}")
print(f"Visitors: {stats['visitors']['value']}") GDPR Compliance
Why Umami Is GDPR Compliant
No Personal Data:
- IP addresses hashed with daily-rotated salt
- No cookies or persistent identifiers
- No cross-site tracking
- No user profiles or behavioral data
Data Processing:
- All data aggregate and statistical
- Cannot identify individuals
- Salt rotation prevents long-term tracking
- Complies with “Privacy by Design”
Consent Not Required: Umami doesn’t require cookie consent because:
- No cookies used
- No personal data collected
- Processing based on legitimate interest (Article 6(1)(f) GDPR)
Privacy Policy Language
Add to your privacy policy:
Website Analytics
We use Umami Analytics, a privacy-friendly analytics tool, to understand website
usage patterns. Umami does not use cookies and does not collect personal data.
Information collected (all anonymized):
- Page URLs visited
- Referring website
- Browser and device type
- Country (derived from IP, which is immediately hashed and discarded)
All data is aggregated and cannot be used to identify individual visitors.
Analytics data is hosted on our own servers and never shared with third parties.
For more information: https://umami.is/docs/privacy Data Deletion
Delete analytics for a specific website:
- Settings → Websites
- Click website → Edit
- Click “Delete” button
- Confirm deletion
Note: This deletes all analytics data permanently.
Troubleshooting Common Issues
Issue 1: Tracking Not Working
Diagnosis:
# Check Umami logs
docker compose logs umami
# Test tracking endpoint
curl -X POST https://analytics.yourdomain.com/api/send -H "Content-Type: application/json" -d '{"type":"pageview","payload":{"website":"YOUR_WEBSITE_ID","url":"/test"}}' Solutions:
- Verify website ID matches in tracking code
- Check CORS settings if tracking from different domain
- Ensure script loads (check Network tab in DevTools)
- Disable browser ad blockers for testing
- Verify CLIENT_IP_HEADER if behind proxy
Issue 2: Database Connection Failed
Symptoms: Umami won’t start, logs show database errors
Solutions:
# Check database container
docker compose logs db
# Verify database is healthy
docker exec umami_db pg_isready -U umami
# Restart everything
docker compose down
docker compose up -d
# Check DATABASE_URL format
# Correct: postgresql://user:pass@host:5432/dbname Issue 3: High Memory Usage
Symptoms: Container using excessive RAM
Solution: Limit container resources in docker-compose.yml:
services:
umami:
deploy:
resources:
limits:
memory: 512M
db:
deploy:
resources:
limits:
memory: 1G Issue 4: Slow Dashboard Loading
For high-traffic sites:
- Optimize PostgreSQL:
db:
command: postgres -c shared_buffers=256MB -c max_connections=200 - Add database indexes:
docker exec -it umami_db psql -U umami -d umami
# Add custom indexes for better performance
CREATE INDEX IF NOT EXISTS idx_event_created_at ON event(created_at);
CREATE INDEX IF NOT EXISTS idx_event_website_id ON event(website_id); - Consider database on separate server for production
Issue 5: Cannot Login After Password Change
Reset admin password:
# Access database
docker exec -it umami_db psql -U umami -d umami
# Reset password to 'umami'
UPDATE account SET password = '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa' WHERE username = 'admin'; Then log in with username admin and password umami, then change it again.
Backup and Restore
Automated Backup Script
#!/bin/bash
# backup-umami.sh
BACKUP_DIR="/backups/umami"
DATE=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"
# Backup PostgreSQL database
docker exec umami_db pg_dump -U umami umami | gzip > "$BACKUP_DIR/umami-$DATE.sql.gz"
# Backup docker-compose config
cp ~/umami/docker-compose.yml "$BACKUP_DIR/docker-compose-$DATE.yml"
# Remove backups older than 30 days
find "$BACKUP_DIR" -type f -mtime +30 -delete
echo "Backup completed: $BACKUP_DIR/umami-$DATE.sql.gz" Automate with cron:
chmod +x backup-umami.sh
# Run daily at 2 AM
crontab -e
# Add: 0 2 * * * /root/backup-umami.sh >> /var/log/umami-backup.log 2>&1 Restore from Backup
# Stop Umami
docker compose stop umami
# Restore database
gunzip -c /backups/umami/umami-YYYYMMDD-HHMMSS.sql.gz | docker exec -i umami_db psql -U umami -d umami
# Restart Umami
docker compose start umami Migration from Google Analytics
Export GA Data
- In Google Analytics, go to Admin
- Select Property → Property Settings
- Export your historical data (if needed)
Import Historical Data
Umami doesn’t support direct GA import, but you can:
- Continue running GA alongside Umami for comparison period
- Use GA API to fetch historical data and import via Umami API
- Start fresh with Umami (recommended for most cases)
Update Tracking Codes
Replace GA tracking with Umami:
Remove:
<!-- Remove Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script> Add:
<!-- Add Umami -->
<script defer src="https://analytics.yourdomain.com/script.js" data-website-id="YOUR_WEBSITE_ID"></script> Performance Optimization
For High-Traffic Sites
1. Database optimization:
db:
environment:
POSTGRES_SHARED_BUFFERS: 512MB
POSTGRES_EFFECTIVE_CACHE_SIZE: 2GB
POSTGRES_MAX_CONNECTIONS: 200 2. Enable connection pooling:
Use PgBouncer for connection pooling:
services:
pgbouncer:
image: pgbouncer/pgbouncer
environment:
DATABASES_HOST: db
DATABASES_PORT: 5432
DATABASES_USER: umami
DATABASES_PASSWORD: umami
DATABASES_DBNAME: umami
PGBOUNCER_POOL_MODE: transaction
PGBOUNCER_MAX_CLIENT_CONN: 1000
PGBOUNCER_DEFAULT_POOL_SIZE: 25
depends_on:
- db
umami:
environment:
DATABASE_URL: postgresql://umami:umami@pgbouncer:6432/umami 3. Monitor resource usage:
docker stats umami umami_db Conclusion
You now have a production-ready Umami analytics system that provides:
- Fast, real-time analytics without compromising privacy
- Simple, intuitive dashboards anyone can understand
- Multi-website tracking from a single installation
- GDPR compliance without cookie consent complexity
- Full API access for custom integrations
Next Steps:
- Add tracking to all your websites
- Set up custom event tracking for key actions
- Create team accounts for collaborators
- Configure automated backups
- Explore the API for custom dashboards
Remember: Umami’s strength is its simplicity. Start with basic page tracking, add custom events as needed, and enjoy analytics that just work.
Additional Resources
Official:
Community:
Integrations:
Last Updated: January 2025 Version Covered: Umami v2.10+