Deployment guide

Medama

Overview

Medama is a privacy-focused, cookieless, self-hosted web analytics server. It is a lightweight, single-binary alternative to hosted analytics services. This image gives you fast visitor insights (page views, unique visitors, referrers, top pages, countries, browsers, operating systems and devices) from a clean dashboard. It also 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 that you control. There are no consent banners to manage, and nothing is 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. It uses an embedded SQLite metadata store and an embedded DuckDB analytics store, so no external database or runtime interpreter is required. It runs behind nginx as a reverse proxy on port 80. Medama has no listen address option of its own, so an nftables guard confines its port 8080 to the loopback interface. 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. It 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.

What you get:

  • Medama 0.6.2 server (single self-contained Go binary that bundles the dashboard web app, with embedded SQLite and 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
  • A 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 SecureAnyCloud support

Prerequisites

  • An active Azure subscription, an SSH key pair, and a VNet and 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 rules: allow 22/tcp from your management network. Allow 80/tcp (plus 443/tcp once you enable HTTPS) from the networks that your dashboard users and tracked websites reach Medama on.
  • The tracking endpoint must be reachable from your visitors' browsers. Open port 80/443 to the public if you collect 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 SecureAnyCloud, 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 choose Review + create, and then Create. First-boot initialisation takes a few seconds after the VM starts.

Step 2 - Deploy from the Azure CLI

# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:medama-ubuntu-24-04:default:latest
az vm create \
  --resource-group <your-rg> \
  --name medama \
  --image sac1647283583153:medama-ubuntu-24-04:default:latest \
  --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 that 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. It has no loopback bind option, so an nftables guard drops any connection to port 8080 on the VM's routable address. Only nginx on port 80 is reachable from outside. 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), and nginx on port 80 answers 200.

Step 5 - Retrieve your administrator login

The login for the admin account is generated on the first boot of your VM and stored in a root-only file. The file 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, then Account).

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. 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. This shows that the shipped image has no working default credential.

Step 7 - First sign-in to the dashboard

Open a web browser and go 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 there you register the sites that you want to track and open each site's analytics.

Step 8 - Register a website

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

Step 9 - Install the cookieless tracking snippet

Open Settings, then Tracker, and 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 that you serve Medama on (see Enabling HTTPS below). On the Tracker page you can also enable optional click tracking and page-view event tracking.

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. It shows 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.

You can confirm end-to-end ingestion from the VM. Register a test website over the API, send a simulated browser pageview to the public ingest endpoint, and read 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). This confirms that the pageview was ingested through the public endpoint and read back through the authenticated dashboard API. By design, Medama 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). It 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 it is immediately rotated to a per-VM password before nginx exposes port 80. Port 80 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

Enabling HTTPS

For production use, serve Medama over HTTPS. This encrypts logins and tracking in transit, and lets the tracking snippet load 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. It holds the SQLite metadata database (me_meta.db, with your user, settings and registered websites) and the DuckDB analytics database (me_analytics.db, with your pageview events). The simplest logical backup is to copy that directory while the service is stopped, or to 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. SecureAnyCloud supplies only the surrounding Ubuntu image, the nginx reverse proxy, the nftables guard and the first-boot configuration.

Support

This image is backed by 24/7 SecureAnyCloud support. Contact us by email or chat for help with deployment, 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.

Deploy on Azure

Launch Medama on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.

View this image

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on Azure