> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Goodbye Counter.dev: Self-Hosting Umami Web Analytics on a VPS
- URL: https://taiwanding.com/en/self-host-umami-analytics-vps/
- Published: 2026-03-01T16:33:59.000Z
- Updated: 2026-07-15T02:50:27.000Z
- Author: Kevin Chen
- Tags: #en, #en-tool

## 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:

```bash
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

```bash
mkdir ~/umami && cd ~/umami

```

### Generate a password and salt

```bash
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

```yaml
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

```bash
docker compose up -d

```

Wait about 15 seconds for the Next.js build to finish, then confirm the service is alive:

```bash
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

```bash
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

```bash
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:

```bash
dig analytics.yourdomain.com +short
# should return your VPS IP

```

## Adding SSL

```bash
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

1. Settings → Websites → Add website
2. Enter your site's domain
3. Grab the tracking script:

```html
<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**

1. Delete the old Counter.dev script (if you have one)
2. Paste in Umami's tracking script
3. 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:

```bash
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:

```bash
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.