Skip to content

3. Umami on the VM

Analytics for the site, with the visitor data on your own disk.

3.0 Why

A published site raises an immediate question: is anyone reading it. The usual answer hands every visitor to an advertising company, which is why the consent banner exists. Umami is the self-hosted alternative: it counts page views without cookies and without personal data, and the database is on your machine.

Self-hosting analytics has a second, practical benefit. Blockers filter requests to the well-known analytics hostnames by list, so a share of your traffic never reports. A script served from your own hostname is not on those lists.

This chapter runs Umami and its Postgres database on the guest, on loopback, and stops there. It is not published yet, because publishing it correctly is a more interesting decision than it first looks, and chapter 4 is about that decision.

Ubuntu guest      umami container ──► postgres container
                        ▲                    (named volume: the data)
                        │  127.0.0.1:3000 (loopback only)
                  curl on the guest

nothing published yet

Where you run this: everything on the Ubuntu guest, except the Umami login, which is a browser on your laptop reaching the guest through an SSH tunnel.

3.1 The job

You run Umami and Postgres with Docker Compose, set a real application secret, log in, change the default password, and register your site to get a tracking ID.

Done when

  • docker compose -f ~/umami/docker-compose.yml ps shows umami and db running and healthy.
  • curl http://127.0.0.1:3000/api/heartbeat answers, and ss shows 127.0.0.1:3000.
  • APP_SECRET in ~/umami/.env is a value you generated.
  • You have logged in and changed the default admin password.
  • Umami lists your site, and you have copied its website ID.
  • Port 3000 is not forwarded on the router.

Need first: 2. Publish the site, so there is a public site worth measuring. Leave it and hello-world running. The guest wants 4 GB of memory once Postgres is in the mix.

Not this chapter: an Edgible app for Umami, the tracking snippet in your pages, or uptime monitoring. Chapter 4 does the first two.

3.2 Run Umami and Postgres

Umami needs a database, so this is two containers rather than one. On the guest:

mkdir -p ~/umami
cat > ~/umami/docker-compose.yml <<'YAML'
services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
      APP_SECRET: ${APP_SECRET}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
  db:
    image: postgres:15-alpine
    container_name: umami-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - umami-db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  umami-db-data:
YAML

Both secrets come from a .env file beside it, generated rather than chosen:

cat > ~/umami/.env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 16)
APP_SECRET=$(openssl rand -base64 32)
EOF
chmod 600 ~/umami/.env

APP_SECRET signs the session tokens. The upstream example file ships a placeholder, and leaving it means anyone who knows that placeholder, which is everyone, can mint a session for your dashboard. It is one command to avoid, and changing it later logs everyone out.

Start it:

cd ~/umami && docker compose up -d
docker compose ps

The first start runs the database migrations, so umami may sit unhealthy for 30 seconds or so before it settles. docker compose logs -f umami shows the progress.

Smoke test. On the guest:

curl -s http://127.0.0.1:3000/api/heartbeat
ss -ltnp | grep 3000

The heartbeat answers, and the port is on 127.0.0.1:3000. As with the site, the 127.0.0.1: prefix in the compose file is what keeps it off the rest of your network, and it matters more here: this is an admin interface with a default password on it until the next step.

3.3 Log in and change the password

Umami ships with admin / umami, so this is the first thing to do, before it is reachable from anywhere but this machine.

Umami is a web interface and the guest is Ubuntu Server, with no browser on it. Carry the port to a machine that has one, over the host 2222 to guest 22 forward from Start here, 1.5. From your laptop:

ssh -p 2222 -L 3000:127.0.0.1:3000 ubuntu@127.0.0.1

Leave that session open and, on the laptop, open http://127.0.0.1:3000. The tunnel carries it to the guest. Close the session when you are done, and the port goes with it.

If your guest does have a desktop, opening http://127.0.0.1:3000 on it works just as well and needs no tunnel.

  1. Sign in as admin with password umami.
  2. Open Settings, then Profile, then Change password.
  3. Set something you have stored in a password manager.

That the login page was never reachable from outside this machine is not a reason to skip this. Chapter 4 publishes this process, and it is easier to change the password now than to remember to do it in the window between publishing and being found.

3.4 Add your site and copy the tracking ID

  1. In Umami, open Settings, then Websites, then Add website.
  2. Name it whatever you like. For Domain, use the hostname from chapter 2: site.<your-org>.edgible.com.
  3. Save, then open the site's Edit or Tracking code view.

You want two things from that screen: the website ID, a UUID, and the shape of the snippet, which looks like this:

<script defer src="https://<umami-host>/script.js" data-website-id="<your-website-id>"></script>

Copy the website ID somewhere. Do not paste the snippet into your site yet: the src on that screen points at whatever host you are viewing Umami on, which right now is 127.0.0.1, and a visitor's browser cannot fetch that. Chapter 4 publishes a hostname that works and then adds the snippet.

Verify

  • docker compose -f ~/umami/docker-compose.yml ps shows umami and db running and healthy.
  • curl http://127.0.0.1:3000/api/heartbeat answers, and ss -ltnp | grep 3000 shows 127.0.0.1:3000.
  • grep APP_SECRET ~/umami/.env shows the value you generated, not an empty or default one.
  • You can sign in with your new password, and admin / umami no longer works.
  • Umami lists your site, and you have its website ID.
  • Port 3000 is not forwarded on the router.

Next

4. Publish Umami puts this on two hostnames with two different auth modes, which is the only way the tracking script and the dashboard can both be correct. Series: README.

Last updated