Me
Analytics Azure

Medama on Ubuntu 24.04 on Azure User Guide

| Product: Medama on Ubuntu 24.04 LTS on Azure

Overview

Medama is a privacy focused, cookieless, self hosted web analytics server — a lightweight, single binary alternative to hosted analytics services. It gives you fast visitor insights (page views, unique visitors, referrers, top pages, countries, browsers, operating systems and devices) from a clean dashboard, and serves a tiny tracking script that sets no cookies and collects no personal data. Your analytics data is stored locally on your own server and never leaves infrastructure you control, so there are no consent banners to manage and nothing shared with a third party.

The Medama server is a single self-contained Go binary that serves both the REST API and the bundled dashboard web app, using an embedded SQLite metadata store and an embedded DuckDB analytics store — no external database or runtime interpreter is required. It runs behind nginx as a reverse proxy on port 80. Because Medama has no listen address option of its own, an nftables guard confines its port 8080 to the loopback interface, so nginx on port 80 is the only public surface.

On the first boot of every deployed VM, a one-shot service rotates the administrator password to a value unique to that VM, records the login in /root/medama-credentials.txt with mode 0600, and only then starts nginx. Port 80 never answers before that rotation has finished, and the upstream default admin / CHANGE_ME_ON_FIRST_LOGIN login never works on a deployed VM. Two VMs deployed from the same image never share credentials.

Medama sign in

What is included:

  • Medama 0.6.2 server (single self-contained Go binary, bundles the dashboard web app, embedded SQLite + DuckDB) at /usr/local/bin/medama
  • nginx reverse proxy on :80 in front of the Medama server on loopback :8080
  • An nftables guard that confines Medama's port 8080 to the loopback interface
  • Per-VM administrator password generated at first boot, in a root-only file — no working default login ships in the image
  • medama.service, nginx.service and a one-shot medama-firstboot.service as systemd units
  • A fully patched Ubuntu 24.04 LTS security baseline at capture time, with unattended security updates enabled
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a comfortable starting point — Medama is very lightweight and runs happily on small instances. NSG inbound: allow 22/tcp from your management network and 80/tcp (plus 443/tcp once you enable HTTPS) from the networks your dashboard users and tracked websites will reach Medama on. The tracking endpoint must be reachable from your visitors' browsers, so port 80/443 should be open to the public if you are collecting analytics from a public website.

Step 1 — Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Medama by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + createCreate. First boot initialisation takes a few seconds after the VM starts.

Step 2 — Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name medama \
  --image <marketplace-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name medama --port 80 --priority 1010

Step 3 — Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 — Verify the Medama stack

Confirm the services are active and that the dashboard answers through nginx:

systemctl is-active medama nginx
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/

Both service lines read active and the dashboard returns 200.

Medama itself listens on port 8080. Because it has no loopback bind option, an nftables guard drops any connection to port 8080 on the VM's routable address, so only nginx on port 80 is reachable from outside. You can confirm the guard from the VM:

IP=$(hostname -I | awk '{print $1}')
curl -s -o /dev/null -w 'port 8080 on %{url_effective}: %{http_code}\n' -m 4 "http://$IP:8080/" || echo "port 8080 refused on $IP"
curl -s -o /dev/null -w 'nginx :80: %{http_code}\n' http://127.0.0.1/

The request to port 8080 on the routable IP is refused (000 / no response), while nginx on port 80 answers 200.

Service status and nftables guard

Step 5 — Retrieve your administrator login

The administrator login generated on the first boot of your VM is stored in a root-only file. It lists the Medama URL, the administrator username (admin) and the password:

sudo cat /root/medama-credentials.txt

Store the password in your password manager and treat the file as sensitive — it is mode 0600, readable only by root:

ls -l /root/medama-credentials.txt

You can change the password at any time from the dashboard (top-right Settings → Account).

Version and per-VM credentials

Step 6 — Verify authentication

Medama authenticates dashboard requests with a session cookie. An unauthenticated request to the dashboard API is refused; logging in with the per-VM password sets a session that then reads back your user; and the upstream default admin / CHANGE_ME_ON_FIRST_LOGIN is rejected:

PASS=$(sudo grep '^medama.admin.pass=' /root/medama-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'no session:   %{http_code}\n' http://127.0.0.1/api/user
CJ=$(mktemp)
curl -s -o /dev/null -w 'login:        %{http_code}\n' -c "$CJ" -X POST http://127.0.0.1/api/auth/login \
  -H 'Content-Type: application/json' -d "{\"username\":\"admin\",\"password\":\"$PASS\"}"
echo -n 'with session: '; curl -s -b "$CJ" http://127.0.0.1/api/user | jq -r '.username'
curl -s -o /dev/null -w 'default pass: %{http_code}\n' -X POST http://127.0.0.1/api/auth/login \
  -H 'Content-Type: application/json' -d '{"username":"admin","password":"CHANGE_ME_ON_FIRST_LOGIN"}'
rm -f "$CJ"

The unauthenticated call reports 401, the login reports 200, the authenticated call prints admin, and the upstream default reports 401 — proving the shipped image has no working default credential.

Authentication round-trip

Step 7 — First sign-in to the dashboard

Open a web browser and navigate to http://<vm-public-ip>/. Sign in with the username admin and the password from /root/medama-credentials.txt. The dashboard opens on My Websites, from which you register the sites you want to track and open each site's analytics.

Medama sign in

Step 8 — Register a website

On the My Websites page choose Add Website and enter the hostname of the site you want to track (for example www.example.com). Medama attributes incoming pageviews to a registered website by the hostname in the page URL, so the hostname must match the site the tracking script runs on.

My Websites — registered sites and their visitors

Step 9 — Install the cookieless tracking snippet

Open Settings → Tracker to copy the tracking snippet. Paste it into the <head> of every page of your website. The script is tiny, sets no cookies and collects no personal data:

<script defer src="http://<vm-public-ip>/script.js"></script>

Replace <vm-public-ip> with your VM's public IP or, better, the DNS name you serve Medama on (see Enabling HTTPS below). From the Tracker page you can also enable optional click tracking and page-view event tracking.

Tracker settings and the tracking snippet to copy

Step 10 — View your analytics

Once the snippet is live, visits to your site appear in Medama in real time. Open a website from My Websites to see its dashboard: unique visitors, page views, average time spent and bounce rate at the top, a visitors-over-time graph, and breakdowns by page, referrer, country, browser, operating system and device. Use the period selector (top right) to switch between today, the last few days, and longer ranges.

Website analytics dashboard with visitors graph and breakdowns

You can confirm end to end ingestion from the VM by registering a test website over the API, sending a simulated browser pageview to the public ingest endpoint, and reading the count back through the authenticated stats API:

PASS=$(sudo grep '^medama.admin.pass=' /root/medama-credentials.txt | cut -d= -f2-)
CJ=$(mktemp); SITE="test.example.com"
curl -s -c "$CJ" -X POST http://127.0.0.1/api/auth/login -H 'Content-Type: application/json' \
  -d "{\"username\":\"admin\",\"password\":\"$PASS\"}" >/dev/null
curl -s -o /dev/null -b "$CJ" -X POST http://127.0.0.1/api/websites -H 'Content-Type: application/json' \
  -d "{\"hostname\":\"$SITE\"}"
UA='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
curl -s -o /dev/null -H "User-Agent: $UA" -H "X-Forwarded-For: 203.0.113.45" "http://127.0.0.1/api/event/ping?u=http://$SITE/"
curl -s -o /dev/null -X POST http://127.0.0.1/api/event/hit -H 'Content-Type: application/json' \
  -H "User-Agent: $UA" -H "X-Forwarded-For: 203.0.113.45" \
  -d "{\"b\":\"$(openssl rand -hex 8)\",\"e\":\"load\",\"u\":\"http://$SITE/\",\"r\":\"https://www.google.com/\",\"p\":true,\"q\":true,\"t\":\"UTC\"}"
ST=$(date -u -d '-1 day' '+%Y-%m-%dT%H:%M:%SZ'); EN=$(date -u -d '+1 hour' '+%Y-%m-%dT%H:%M:%SZ')
echo -n 'recorded page views: '
curl -s -b "$CJ" "http://127.0.0.1/api/website/$SITE/summary?start=$ST&end=$EN&interval=hour" | jq -r '.current.pageviews'
rm -f "$CJ"

The final line prints 1 (or more), confirming the pageview was ingested through the public endpoint and read back through the authenticated dashboard API. Note that Medama, by design, ignores traffic that looks like a bot or comes from a non-genuine source, so this test shapes the request like a real browser visit.

Security baseline

The image is captured with the Ubuntu 24.04 security baseline fully applied (including phased updates) and ships with unattended security upgrades enabled, so your VM keeps patching itself after deployment. The shipped image contains no database and no user account: the default administrator is re-seeded on your VM's first boot on the loopback interface only and immediately rotated to a per-VM password before nginx exposes port 80, which is gated on the first-boot marker.

sudo nft list table inet medama_guard | grep 'dport 8080'
grep -h ConditionPathExists /etc/systemd/system/nginx.service.d/*.conf
grep -h Unattended-Upgrade /etc/apt/apt.conf.d/20auto-upgrades

Security posture

Enabling HTTPS

For production use, serve Medama over HTTPS so that logins and tracking are encrypted in transit, and so the tracking snippet loads securely on HTTPS sites. Point a DNS A record at the VM's public IP address, then install a certificate with Certbot (replace the domain):

sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.example.com

After enabling HTTPS, open 443/tcp in the NSG and restrict or close inbound port 80. Update the tracking snippet on your website to load https://your-domain.example.com/script.js.

Backup and maintenance

All Medama state lives under /var/lib/medama — the SQLite metadata database (me_meta.db, holding your user, settings and registered websites) and the DuckDB analytics database (me_analytics.db, holding your pageview events). The simplest logical backup is to copy that directory while the service is stopped, or snapshot the OS disk:

sudo systemctl stop medama
sudo tar czf /var/backups/medama-$(date +%F).tar.gz -C /var/lib medama
sudo systemctl start medama

Apply operating system security updates with sudo apt update && sudo apt upgrade; the services restart cleanly. Restart the server at any time with sudo systemctl restart medama.

Licence and source

Medama's server and dashboard are free software licensed under the Apache License 2.0; the small client-side tracking script served at /script.js is licensed under the MIT License. The upstream project and source are at the Medama repository. This image ships the official Medama 0.6.2 release binary unmodified — cloudimg supplies the surrounding Ubuntu image, the nginx reverse proxy, the nftables guard and first-boot configuration only.

Support

This image is backed by 24/7 cloudimg support. Contact us by email and chat for deployment help, website and tracker configuration, TLS termination and analytics questions.

All product and company names are trademarks or registered trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them.