Goodbye Counter.dev: Self-Hosting Umami Web Analytics on a VPS
Introduction
For a long time I'd been using Counter.dev to track my blog's visitor stats — it's simple, free, and privacy-focused. But recently I noticed the dashboard had suddenly stopped counting, and even after logging in I couldn't see any new data.
A bit of digging revealed that Counter.dev is an open-source project run by a single person on a $5/month VPS with Redis. It's had several outages over the years (back in 2023 someone even asked "Is Counter.dev down?" on Hacker News). That's the fate of the freeloader — the service can vanish at any moment.
Since I already have my own VPS, why not just self-host something more stable? After looking around for a while, I settled on Umami — open source, lightweight, with an API, 35k+ stars on GitHub, and blazing fast to deploy with Docker.
Environment
- VPS: Ubuntu 24.04 (already running Ghost CMS + Nginx)
- Docker + Docker Compose
- Domain managed through Cloudflare
Installing Docker
If your VPS doesn't have Docker yet:
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
Once it's installed, log out and back in so the group change takes effect.
Deploying Umami
Create a project directory
mkdir ~/umami && cd ~/umami
Generate a password and salt
DB_PASS=$(openssl rand -hex 16)
APP_SECRET=$(openssl rand -hex 32)
echo "DB_PASS: $DB_PASS"
echo "APP_SECRET: $APP_SECRET"
Jot down these two values — you'll need them shortly.
Create docker-compose.yml
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "3001:3000"
environment:
DATABASE_URL: postgresql://umami:<your DB_PASS>@db:5432/umami
APP_SECRET: <your APP_SECRET>
depends_on:
db:
condition: service_healthy
restart: always
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: <your 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 it up
docker compose up -d
Wait about 15 seconds for the Next.js build to finish, then confirm the service is alive:
curl -s -o /dev/null -w "%{http_code}" http://localhost:3001
# should return 200
Setting up the Nginx reverse proxy
Add an Nginx config file
sudo tee /etc/nginx/sites-available/analytics.yourdomain.com.conf << 'EOF'
server {
listen 80;
server_name analytics.yourdomain.com;
location / {
proxy_pass http://localhost:3001;
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_set_header Host $host;
}
}
EOF
Enable + test
sudo ln -s /etc/nginx/sites-available/analytics.yourdomain.com.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
DNS setup
Head to your DNS management panel (I use Cloudflare) and add an A record:
| Name | Type | Value | Proxy |
|---|---|---|---|
| analytics | A | Your VPS IP | DNS only (grey cloud) |
⚠️ Turn off the Cloudflare proxy first (grey cloud), because certbot needs a direct connection to the VPS to perform HTTP validation.
Confirm the DNS has propagated:
dig analytics.yourdomain.com +short
# should return your VPS IP
Adding SSL
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d analytics.yourdomain.com
Just say Yes to everything — certbot will automatically update the Nginx config.
Logging in to Umami
Open https://analytics.yourdomain.com
Default credentials:
- Username:
admin - Password:
umami
Change the password immediately after logging in! (Settings → Profile)
Add a website + get the tracking code
- Settings → Websites → Add website
- Enter your site's domain
- Grab the tracking script:
<script defer src="https://analytics.yourdomain.com/script.js" data-website-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></script>
Integrating with Ghost CMS
Ghost Admin → Settings → Code injection → Site Header
- Delete the old Counter.dev script (if you have one)
- Paste in Umami's tracking script
- Save
Open an incognito window and visit your site, then head back to the Umami dashboard to check whether the data came through.
Backups
Since you're self-hosting, backups are now your own responsibility. Umami's data lives in PostgreSQL, and a single command will dump it:
docker compose -f ~/umami/docker-compose.yml exec -T db pg_dump -U umami umami > ~/umami-backup-$(date +%F).sql
If you already have an automated backup script (like mine, which uses cron + rclone to sync to Google Drive), just add one more line to it.
Updating Umami
When a new version comes out, two lines will do it:
cd ~/umami
docker compose pull && docker compose up -d
Counter.dev vs Umami
| Counter.dev | Umami | |
|---|---|---|
| Cost | Free | Free (self-hosted) |
| Stability | ⚠️ Small project maintained by one person | ✅ Active open-source community, 20k+ stars |
| API | ❌ No public API | ✅ Full REST API |
| Data control | Stored on someone else's Redis | Stored on your own PostgreSQL |
| Ad blocking | Easily blocked | Self-hosted subdomain won't get blocked |
| Dashboard | Ultra-minimal | Full-featured, supports multiple sites |
Conclusion
The whole process took about 15 minutes. If you already have a VPS running other services (like Ghost CMS), spinning up one more Docker container for Umami takes almost no extra resources. Compared to relying on a free third-party service that could go down at any time, self-hosting a stable, fully controllable analytics tool is absolutely worth it.
Member discussion