Observability Azure

Kuvasz on Ubuntu 24.04 on Azure User Guide

| Product: Kuvasz on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of Kuvasz on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. Kuvasz is an open source, self-hosted uptime and SSL monitoring service. It checks your websites, APIs and services over HTTP, ICMP ping and push heartbeats, at intervals as short as five seconds, and records every state change as an incident you can look back over. Alongside uptime it watches the SSL certificate behind each HTTPS monitor and warns you before it expires. Alerts go out per monitor through email, Slack, Discord, Telegram, PagerDuty, Microsoft Teams or a custom webhook, maintenance windows suppress alerts during planned work, and public or private status pages let you share the picture with customers or a team.

The cloudimg image ships the free and open source, AGPL-3.0 licensed Kuvasz application, run the officially supported way as the upstream container pinned by image digest, with PostgreSQL 16 as its database. Both images are captured into the VM, so your instance starts in seconds. Nothing sensitive ships in the image: there is no database, no monitor and no account inside it. On first boot, before the public port is ever opened, the instance generates its own administrator password, its own REST API key and its own PostgreSQL password, writes them to a root-only file, and proves they authenticate before nginx is allowed to start. Backed by 24/7 cloudimg support.

Kuvasz is a trademark of its respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by the Kuvasz project. It ships the free and open source self-hosted software, unmodified.

The docker, kuvasz-firstboot, kuvasz, kuvasz-postboot and nginx services all reporting active, and both compose containers up - the Kuvasz application published only to 127.0.0.1:8080 and PostgreSQL on the internal container network with no host port at all

What is included:

  • Kuvasz v4.1.0 (the AGPL-3.0 licensed uptime and SSL monitoring service), pinned by image digest
  • PostgreSQL 16, created empty on first boot and stored on a persistent volume, reachable only from the application container
  • Docker Engine running both containers on a private network, with the application published to loopback only
  • Host nginx as the single public listener on port 80, with header buffers sized for authenticated responses
  • kuvasz.service, kuvasz-firstboot.service and kuvasz-postboot.service as systemd units, enabled and active on boot
  • A per-VM administrator password, a per-VM REST API key and a per-VM PostgreSQL password, generated on first boot and never baked into the image
  • The public port held shut until those credentials have been generated and proven to authenticate
  • A mounted YAML configuration file ready for monitors, notification channels and SMTP as code
  • Fixed memory ceilings on both containers and an explicit JVM heap limit, so the stack fits the recommended VM size with no swap
  • No default login, no shipped secret, no third-party token and an empty database on first boot
  • Ubuntu 24.04 LTS base with latest security patches applied at build time
  • Azure Linux Agent for seamless cloud integration and SSH key injection
  • 24/7 cloudimg support with guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region
  • Subscription to the Kuvasz listing on Azure Marketplace

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM). Kuvasz is a JVM service and it runs alongside PostgreSQL, so this image pins both: the application container is capped at 1 GB with a 512 MB Java heap, and PostgreSQL is capped at 768 MB with a small buffer profile. That leaves well over 1.9 GB for the operating system, so the whole stack fits comfortably and no swap is used or shipped. For hundreds of monitors, very short check intervals, or long retention windows, move to Standard_D2s_v5 or larger. NSG inbound: allow 22/tcp from your management network and 80/tcp for the web interface from the networks that use it.

Step 1: Deploy from the Azure Portal

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Kuvasz 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 + create and Create.

Step 2: Deploy from the Azure CLI

RG="kuvasz-prod"; LOCATION="eastus"; VM_NAME="kuvasz-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/kuvasz-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name kuvasz-vnet --address-prefix 10.100.0.0/16 --subnet-name kuvasz-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name kuvasz-nsg
az network nsg rule create -g "$RG" --nsg-name kuvasz-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name kuvasz-nsg --name allow-http --priority 110 \
  --destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name kuvasz-vnet --subnet kuvasz-subnet --nsg kuvasz-nsg --public-ip-sku Standard

Replace <sub-id> and <version> with your subscription id and the image version shown on the listing, and <your-mgmt-cidr> with the network you administer from.

Step 3: First boot and per-instance credentials

On the first boot of each VM, Kuvasz generates its own per-instance secrets before the web port is opened. Nothing is baked into the image. Give the instance a minute after first boot, then SSH in and read the credentials file (it is 0600 root:root):

sudo stat -c '%a %U:%G %n' /root/kuvasz-credentials.txt /opt/kuvasz/.env
sudo cat /root/kuvasz-credentials.txt

Both the credentials file and the environment file confirmed as 600 root:root, and the per instance credentials file showing the Kuvasz administrator login, the REST API key and the PostgreSQL password each generated randomly for this VM and masked here, together with the resolved web address

The file contains everything unique to this instance:

  • KUVASZ_USERNAME / KUVASZ_PASSWORD — the administrator login you use in the web interface.
  • KUVASZ_API_KEY — the REST API key, sent as an X-API-KEY header or as a bearer token.
  • KUVASZ_URL — the web address of this instance, resolved from its public IP on first boot.
  • POSTGRES_PASSWORD — the database role password. PostgreSQL is not published to any host port, so this is only used inside the private container network.

Confirm the services and containers are healthy:

sudo systemctl is-active docker kuvasz-firstboot kuvasz kuvasz-postboot nginx
active
active
active
active
active
sudo docker compose -f /opt/kuvasz/docker-compose.yml ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}'
SERVICE   STATUS                  PORTS
kuvasz    Up 12 minutes           127.0.0.1:8080->8080/tcp
db        Up 13 minutes (healthy) 5432/tcp

The application container publishes to loopback only and PostgreSQL has no host port at all; host nginx on port 80 is the single public door.

Step 4: The security model

Kuvasz reads its administrator username, administrator password and REST API key from its environment when it starts, so those values decide who can use the instance. Upstream documents example values, and the project runs a public demo with a published login; neither ever exists on your VM. kuvasz-firstboot.service generates a random administrator password, a random REST API key and a random PostgreSQL password for this VM alone, and writes them to a root-only file. The application cannot start before that has happened, because Docker Compose has nothing to substitute into the container until the file exists, and a start-up guard independently refuses to launch on an empty, too short, or documented-example secret.

The public listener is held shut for the whole of that window. Host nginx is the only thing bound to a public address, and it is gated on a bootstrap marker that kuvasz-postboot.service creates only after it has proven that both the new API key and the new administrator password really authenticate. Only then does port 80 open.

The health endpoint is deliberately anonymous so that load balancers and orchestrators can probe it. Everything else is closed.

The security posture: nginx the only 0.0.0.0:80 listener with Kuvasz on 127.0.0.1:8080 and no PostgreSQL host port, an anonymous API request refused 401, a wrong API key refused 401, an anonymous dashboard request redirected to the login page, the MCP endpoint refused 401 and only the health endpoint answering 200

ss -tln | grep -E ':(80|8080|5432) ' | awk '{print $1, $4}'
LISTEN 0.0.0.0:80
LISTEN 127.0.0.1:8080
LISTEN [::]:80

Nothing but the health check answers without a credential:

curl -s -o /dev/null -w 'anonymous  GET /api/v2/http-monitors  -> %{http_code}\n' http://127.0.0.1/api/v2/http-monitors
curl -s -o /dev/null -w 'anonymous  GET /                      -> %{http_code}\n' http://127.0.0.1/
curl -s -o /dev/null -w 'anonymous  GET /mcp                   -> %{http_code}\n' http://127.0.0.1/mcp
curl -s -o /dev/null -w 'anonymous  GET /api/v2/health         -> %{http_code}\n' http://127.0.0.1/api/v2/health
anonymous  GET /api/v2/http-monitors  -> 401
anonymous  GET /                      -> 303
anonymous  GET /mcp                   -> 401
anonymous  GET /api/v2/health         -> 200

The 303 on / is the redirect to the login page. The MCP server endpoint is unreachable because no MCP key is shipped; supply one yourself if you want to drive Kuvasz from an AI assistant.

Step 5: Verify the end-to-end round-trip

The image ships a prover that exercises the whole security model through the public front door. It confirms the service is healthy, that the dashboard and the REST API are both closed to anonymous callers, that a wrong API key and a wrong password are refused, and that the per-instance credentials really do open both.

sudo /usr/local/sbin/kuvasz-roundtrip.sh

The credential round-trip prover reporting OK: health UP, the anonymous dashboard redirected 303 to the login page, the anonymous API refused 401, a wrong API key refused 401, the per instance API key accepted with the settings endpoint reporting version 4.1.0, a wrong password issuing no session, and the real login returning a JWT session cookie that opens both the dashboard and the API

A wrong password is worth a note. Kuvasz answers 303 to both a good and a bad login, redirecting either to the dashboard or back to /login?error=true, so a status code alone proves nothing. The prover therefore checks whether a session cookie was actually issued, which is the only real evidence.

Step 6: Sign in to the web interface

Open http://<vm-ip>/ in your browser and sign in with the KUVASZ_USERNAME and KUVASZ_PASSWORD from the credentials file.

The Kuvasz sign in page asking for a username and password, for the administrator account generated on this VM during first boot

The Dashboard is the home view. It rolls every monitor up into a single picture: how many are up, down, paused or in maintenance, the uptime ratio and total downtime over the last seven days, how many incidents there were, and the same summary again for SSL certificate state. Anything unhealthy is listed underneath so a problem is the first thing you see.

The Kuvasz dashboard with four HTTP monitors all reporting UP, none down, paused or in maintenance, zero incidents and zero affected monitors over the last seven days, a 100.00 percent uptime ratio and no downtime, with the SSL summary section beginning below

Monitors then HTTP & SSL lists every website and API you are watching, with its current uptime verdict, the SSL padlock, when the state last changed and when the next check is due. The controls on each row clone, pause or delete a monitor.

The Kuvasz HTTP and SSL monitor list showing four monitors, Azure Portal, GitHub API, Kuvasz documentation and cloudimg website, each reporting UP with a green SSL padlock, when the status last changed and a countdown to the next uptime check

Selecting a monitor opens its own page: how long it has currently been up, when it was last checked and when it is next due, its incident count and uptime ratio, and a latency breakdown with the average, minimum, maximum and the 90th, 95th and 99th percentiles over the last 24 hours. Configure edits everything about the check, including the request method, expected status codes, an expected keyword, custom headers and the failure count that has to be reached before you are alerted.

The detail page for the cloudimg website monitor showing it currently up for 8 minutes 54 seconds, last checked 53 seconds ago, next check in 7 seconds, zero incidents, a 100.00 percent uptime ratio and latency percentiles of 39 ms average, 7 ms minimum, 280 ms maximum, 67 ms at P90, 174 ms at P95 and 259 ms at P99

Step 7: Add monitors from the REST API

Everything the web interface does is also available over the REST API, authenticated with the KUVASZ_API_KEY from the credentials file. On the VM itself, read the key straight out of that file rather than pasting it around:

API_KEY="$(sudo grep -E '^KUVASZ_API_KEY=' /root/kuvasz-credentials.txt | cut -d= -f2-)"
curl -s -H "X-API-KEY: $API_KEY" http://127.0.0.1/api/v2/settings \
  | jq -e '{version: .app.version, authEnabled: .authentication.enabled, retentionDays: .app.eventDataRetentionDays} | select(.version == "4.1.0")'
{
  "version": "4.1.0",
  "authEnabled": true,
  "retentionDays": 365
}

Create a monitor. A 201 means it was accepted and the first check is already scheduled:

API_KEY="$(sudo grep -E '^KUVASZ_API_KEY=' /root/kuvasz-credentials.txt | cut -d= -f2-)"
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1/api/v2/http-monitors \
  -H "X-API-KEY: $API_KEY" -H 'Content-Type: application/json' \
  -d '{"name":"cloudimg website","url":"https://www.cloudimg.co.uk","uptimeCheckInterval":60,"enabled":true,"sslCheckEnabled":true,"latencyHistoryEnabled":true}')"
echo "create monitor -> HTTP $CODE"
[ "$CODE" = "201" ]

List what is being watched, and its current verdict. The first uptime check runs within a few seconds of creation, so wait for it rather than reading the row straight away:

API_KEY="$(sudo grep -E '^KUVASZ_API_KEY=' /root/kuvasz-credentials.txt | cut -d= -f2-)"
for i in $(seq 1 9); do
  READY="$(curl -s -H "X-API-KEY: $API_KEY" http://127.0.0.1/api/v2/http-monitors | jq '[.[] | select(.uptimeStatus != null)] | length')"
  [ "$READY" -gt 0 ] && break
  sleep 10
done
curl -s -H "X-API-KEY: $API_KEY" http://127.0.0.1/api/v2/http-monitors \
  | jq -r '.[] | [.id, .name, (.uptimeStatus // "pending"), (.sslStatus // "pending")] | @tsv'
1   cloudimg website    UP  pending

The uptime verdict lands within about twenty seconds of creating the monitor. The SSL column is filled in by a separate certificate check on its own, slower cadence, so it reads pending for two to three minutes longer before turning VALID.

From a machine other than the VM itself, replace http://127.0.0.1 with the address in KUVASZ_URL. The key can also be sent as Authorization: Bearer $API_KEY if that suits your client better.

Notification channels and monitors as code

Kuvasz reads a YAML configuration file, mounted into the container at /config/kuvasz.yml and stored on the VM at /opt/kuvasz/kuvasz.yml. It ships empty of configuration, deliberately: a cloudimg image never carries a notification token, an SMTP password or a monitor you did not ask for. It is the place to declare notification channels, SMTP and monitors as code, so an entire monitoring estate can live in version control.

sudo cat /opt/kuvasz/kuvasz.yml

Edit it with your preferred editor — sudo nano /opt/kuvasz/kuvasz.yml — then restart the service so Kuvasz reloads it:

sudo systemctl restart kuvasz

A global Slack channel plus a monitor defined as code looks like this:

integrations:
  slack:
    - name: ops_channel
      webhook-url: 'https://hooks.slack.com/services/XXX/YYY/ZZZ'
      global: true

monitors:
  - name: my-website
    url: 'https://example.com'
    uptimeCheckInterval: 60
    enabled: true
    sslCheckEnabled: true

Setting global: true applies that channel to every monitor. Leaving it out lets you attach the channel to individual monitors instead, from Integrations in the web interface or through the API. Email notifications need an smtp-config block, and Kuvasz will refuse to start if that block is present but incomplete, so add all of its keys together. The full reference is in the Kuvasz documentation.

Status pages and maintenance windows are managed from the web interface. A status page publishes the state of the monitors you choose, either publicly or behind your own access control; a maintenance window pauses checks on a schedule, one off or recurring, so planned work never raises a false alert.

Prometheus and OpenTelemetry

Kuvasz can export uptime, latency and SSL metrics. Both exporters are off by default in this image. Turn the Prometheus exporter on by adding the environment variables to /opt/kuvasz/.env and restarting, after which the metrics are served from the application's own endpoint and can be scraped through the same nginx front door. The available switches, including the per metric toggles and the OpenTelemetry collector settings, are listed in the configuration reference.

Using your own domain and HTTPS

This image serves plain HTTP on port 80 so it works the moment the VM boots. For anything reachable from the internet, put your own certificate in front of it. Point a hostname at the VM and either terminate TLS on an Azure Application Gateway or load balancer in front of the instance, or add a certificate to the host nginx configuration at /etc/nginx/sites-available/kuvasz.conf and open 443/tcp on the network security group.

The address recorded in KUVASZ_URL is resolved from the VM's public IP on first boot and is used for the credentials note and the message of the day. It does not gate any request, so reaching the instance by a DNS name works without further changes; update the value if you want the on-VM notes to match.

Managing the service

sudo systemctl status kuvasz --no-pager

To restart the whole stack after a configuration change:

sudo systemctl restart kuvasz

Data lives in a Docker volume, so it survives a restart of the service and a reboot of the VM. The upstream release notes and the complete corresponding source for the version shipped here are recorded on the VM at /usr/share/doc/kuvasz/, alongside the AGPL-3.0 licence text. Kuvasz is served over a network, so section 13 of that licence applies: the image ships a written offer naming the exact upstream tag whose source is published, and the application itself is shipped unmodified.

sudo cat /usr/share/doc/kuvasz/README.cloudimg

Support

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA. Contact support@cloudimg.co.uk for assistance with this image.