Deployment guide

Gatus

Overview

Gatus is an open source health dashboard and status page. You describe the services you want to watch in one YAML file. Each endpoint has a URL (HTTP, HTTPS, TCP, ICMP, DNS, SSH, STARTTLS, WebSocket or gRPC), a check interval and a list of conditions such as [STATUS] == 200, [RESPONSE_TIME] < 300, [BODY].status == UP or [CERTIFICATE_EXPIRATION] > 720h. Gatus runs the checks, keeps the history, and shows a live dashboard with uptime and response-time charts, a JSON API and status badges. It can alert you through Slack, Microsoft Teams, email, PagerDuty, Discord, Telegram, ntfy and many other providers.

The SecureAnyCloud image runs Gatus 5.36.0, compiled by SecureAnyCloud from the official 5.36.0 source with Go 1.27.3 and patched golang.org/x/net, golang.org/x/crypto and gRPC modules. Gatus publishes no release binary of its own, only container images. The 5.36.0 source does not build with the current Go release until gRPC is updated.

The build is reproducible, and its record ships with the image. The base is Ubuntu 24.04 LTS, patched at build time, and unattended security upgrades stay on for your VM.

What you get:

  • Gatus 5.36.0 (gatus.service) running as the unprivileged gatus user, listening on 127.0.0.1:8080 only
  • nginx publishing the dashboard over HTTPS on port 443 with a certificate created on your VM's first boot, behind HTTP Basic Auth with a password unique to your VM; port 80 redirects to HTTPS
  • A starter configuration at /etc/gatus/config.yaml that monitors this VM only (its HTTPS front end and certificate expiry, Gatus itself, and SSH), so the dashboard is green on first boot. Nothing on the internet is probed until you add it.
  • gatus-set-hostname, a helper to serve the dashboard under your own DNS name
  • The build record (BUILD-INFO.txt), go.mod, go.sum, the govulncheck results and the Apache 2.0 licence in /usr/share/doc/gatus/
  • 24/7 SecureAnyCloud support

Security posture

  • Password on everything. Gatus has no login by default, and its own optional login leaves the badge, uptime and history routes open. nginx therefore protects every path (the dashboard, the JSON API, the badges and Gatus' health route) with HTTP Basic Auth for the user admin. The password is generated on each VM's first boot, stored only as a bcrypt hash in /etc/nginx/.gatus.htpasswd, and written in plain text to /root/gatus-credentials.txt (mode 0600, root only). No password or hash ships in the image.
  • Fails closed. nginx refuses to start until first boot has created the certificate and the password, so the dashboard is never reachable without a login.
  • Loopback only. Gatus listens on 127.0.0.1 only. nginx on 443 is the only way in.
  • HTTPS on first boot. nginx serves the dashboard with a TLS key and self-signed certificate created on your VM (TLS 1.2 and 1.3 only). Port 80 only redirects to HTTPS.
  • Fixed host name. The dashboard is served under your VM's public IP address, read from the Azure Instance Metadata Service at first boot (or a name you set), never from the request. Requests for any other host name are dropped, and cross-site form posts and WebSocket connections are refused.
  • Real client addresses. nginx passes the connecting address to Gatus and ignores any X-Forwarded-For header a client sends.
  • No metadata access for the checks. Gatus fetches whatever URLs you configure, so gatus.service and nginx.service are blocked from reaching the Azure Instance Metadata Service (169.254.169.254) and the Azure wire server (168.63.129.16). A check pointed at either fails instead of leaking it. Name resolution keeps working through Ubuntu's local resolver.
  • Least privilege. gatus.service runs with no Linux capabilities, a read-only system (ProtectSystem=strict) except /var/lib/gatus, and its own private /tmp.
  • No surprise traffic. The starter configuration checks only the VM itself, and the Prometheus /metrics endpoint is off.
  • Per-VM identity. SSH host keys and the machine ID are generated fresh on every VM.

Known advisory in a bundled library

govulncheck reports one advisory against the binary, GO-2026-5932: the golang.org/x/crypto/openpgp package is deprecated and described by the Go team as unsafe by design, and it has no fixed version. It is compiled in only because the GitHub library behind Gatus' GitHub-issue alerting provider imports it for signing commits, which Gatus never does. Every call path govulncheck finds reaches it only through package initialisation, and the build fails if that ever changes. Every other finding was fixed by the module updates. The full output ships in /usr/share/doc/gatus/govulncheck-binary.txt and govulncheck-source.txt.

Prerequisites

You need an active Azure subscription, an SSH key, and a virtual network with a subnet.

The recommended VM size is Standard_B2s. Gatus is a single small Go process with an embedded SQLite database. The VM needs outbound access to the services you want to monitor.

1

Deploy from the Azure Portal

Search the Azure Marketplace for Gatus, choose the plan and create the VM.

In the networking step, attach a network security group. Allow inbound TCP 22 (SSH) from your management address. Allow TCP 443 (the dashboard) and TCP 80 (redirect only) from the people who need the status page.

2

Deploy from the Azure CLI

RG="gatus-prod"; LOCATION="eastus"; VM_NAME="gatus-01" IMAGE_URN="sac1647283583153:gatus-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION" az network vnet create -g "$RG" --name gt-vnet --address-prefix 10.106.0.0/16 --subnet-name gt-subnet --subnet-prefix 10.106.1.0/24 az network nsg create -g "$RG" --name gt-nsg az network nsg rule create -g "$RG" --nsg-name gt-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 gt-nsg --name allow-web --priority 110 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 80 443 --access Allow --protocol Tcp
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:gatus-ubuntu-24-04:default:latest
az vm create -g "$RG" --name "$VM_NAME" --image "$IMAGE_URN" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name gt-vnet --subnet gt-subnet --nsg gt-nsg --public-ip-sku Standard
3

Connect and read your login

Connect as the admin user you chose (here azureuser):

ssh azureuser@<vm-ip>

First boot finishes within a minute of the VM starting. It reads the VM's public IP address from the Azure Instance Metadata Service, creates the HTTPS certificate, generates the password and starts nginx. Then Gatus starts its checks. Your login is in a root-only file:

sudo cat /root/gatus-credentials.txt
# Gatus - generated on first boot by gatus-firstboot.service
# These credentials are unique to this VM. Store them somewhere safe. GATUS_URL=https://20.121.0.141/ GATUS_USERNAME=admin GATUS_PASSWORD=<GATUS_PASSWORD>
#
# Open GATUS_URL in a browser (self-signed certificate minted on this VM's first boot; port 80 # redirects to https) and sign in with the username and password above. The same credentials protect # the JSON API (/api/v1/endpoints/statuses) and the status badges. # Change the name it is served as:
sudo gatus-set-hostname <your-dns-name>
#
# The default endpoints monitor this VM only. Add your own services to /etc/gatus/config.yaml
# (Gatus reloads it automatically). Restrict 80/443 in the network security group to the people who need the page.
# Full deployment guide: https://secureanycloud.com/stacks/gatus/

GATUS_URL holds your VM's public IP address and GATUS_PASSWORD your VM's own password.

4

Confirm the services are running

Check that first boot, Gatus and nginx are active, that Gatus runs as the unprivileged gatus user, and that the open health endpoint answers:

systemctl is-active gatus-firstboot.service gatus.service nginx.service ps -o user= -C gatus test "$(curl -sk https://localhost/healthz)" = ok && echo "healthz ok"
active active active gatus healthz ok

nginx serves /healthz without a login, so you can use it for an Azure Load Balancer or Application Gateway health probe over HTTPS. Everything else needs the password.

List the ports the VM listens on. These are the only ones:

sudo ss -Hltnu | awk '{print $1, $5}' | sort -u
tcp 0.0.0.0:22 tcp 0.0.0.0:443 tcp 0.0.0.0:80 tcp 127.0.0.1:8080 tcp 127.0.0.53%lo:53 tcp 127.0.0.54:53 tcp [::]:22 tcp [::]:443 tcp [::]:80 udp 10.0.0.10%eth0:68 udp 127.0.0.1:323 udp 127.0.0.53%lo:53 udp 127.0.0.54:53 udp [::1]:323

Gatus (8080) listens on loopback only, and nginx publishes it on 443. The loopback port 53 entries are Ubuntu's local stub resolver (systemd-resolved), 323 is chrony's local control socket and 68 is the DHCP client.

The binary is the reproducible SecureAnyCloud build of the 5.36.0 source. Its record ships with the image. Check it:

sha256sum /usr/local/bin/gatus grep -E '^(source|toolchain |binary|govulncheck)' /usr/share/doc/gatus/BUILD-INFO.txt
07676fb59df06f69deb9d1c155c21eff7da6d0bb4e32585ed82d06cc7f982a66 /usr/local/bin/gatus source https://github.com/TwiN/gatus/archive/refs/tags/v5.36.0.tar.gz source sha256 b5543af591e602281406049ee2f822a6529a8f14be0cd54df5a31c210520159a (tag commit ed1107b41a30e22047eecfb6dbc3be5e39829d5a) toolchain go1.27.3 linux/amd64, official tarball https://go.dev/dl/go1.27.3.linux-amd64.tar.gz toolchain sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40 binary sha256 07676fb59df06f69deb9d1c155c21eff7da6d0bb4e32585ed82d06cc7f982a66 (reproducible) govulncheck v1.9.0 -mode=binary and source mode: only GO-2026-5932 (DB updated: 2026-10-21 00:27:29 +0000 UTC)
5

Sign in to the dashboard

Browse to the GATUS_URL from Step 3, for example https://20.121.0.141/. The certificate is self-signed, so your browser warns once. Accept it, or install your own certificate (Step 9). Without a login, nginx answers 401 and your browser asks for a username and password:

Sign in as admin with the password from Step 3. The Gatus health dashboard lists the three starter endpoints, each with its latest result and a bar per check:

Select an endpoint to open its detail page. It shows the current status, the average and range of response times, the recent checks and a response-time chart:

Port 80 only redirects to the HTTPS address. A request for any host name that is not this VM's is dropped without an answer (000):

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost/
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: not-this-vm.example' http://127.0.0.1/ || true
301 https://20.121.0.141/ 000

You can also check the login from the command line. These requests go to nginx on the VM itself. The API refuses a request with no password or a wrong one. The VM's own password returns the endpoint statuses. The block reads the password from the credentials file and passes it to curl on standard input, never on the command line. It stops with an error if any answer is not the expected one:

H=$(sudo sed -n 's/^set $gatus_host "\(.*\)";$/\1/p' /etc/nginx/gatus-host.conf) AUTH() {
sudo sed -n 's/^GATUS_PASSWORD=\(.*\)$/user = "admin:\1"/p' /root/gatus-credentials.txt; }
U=$(curl -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}' "https://$H/api/v1/endpoints/statuses")
W=$(printf 'user = "admin:not-the-password"\n' | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}' "https://$H/api/v1/endpoints/statuses")
A=$(AUTH | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null -w '%{http_code}' "https://$H/api/v1/endpoints/statuses")
echo "unauth : $U";
echo "wrongpw : $W";
echo "authed : $A" [ "$U" = 401 ] && [ "$W" = 401 ] && [ "$A" = 200 ]
unauth : 401 wrongpw : 401 authed : 200
6

Read the results from the API

The dashboard is built on a JSON API that you can use from scripts. This block lists every endpoint with how many results Gatus holds and whether the latest check passed. It fails unless all three starter endpoints are healthy:

H=$(sudo sed -n 's/^set $gatus_host "\(.*\)";$/\1/p' /etc/nginx/gatus-host.conf) AUTH() {
sudo sed -n 's/^GATUS_PASSWORD=\(.*\)$/user = "admin:\1"/p' /root/gatus-credentials.txt; }
J=$(AUTH | curl -K - -sk --resolve "$H:443:127.0.0.1" "https://$H/api/v1/endpoints/statuses")
echo "$J" | jq -r '.[] | "\(.key) results=\(.results|length) last_success=\(.results[-1].success)"' test "$(echo "$J" | jq -r '[.[] | select(.results[-1].success == true) | .name] | sort | join(",")')" = "gatus,nginx-https,ssh"
this-instance_gatus results=5 last_success=true this-instance_nginx-https results=5 last_success=true this-instance_ssh results=5 last_success=true

Each endpoint's key is <group>_<name>. The detail of one check shows each condition and its result:

H=$(sudo sed -n 's/^set $gatus_host "\(.*\)";$/\1/p' /etc/nginx/gatus-host.conf) AUTH() {
sudo sed -n 's/^GATUS_PASSWORD=\(.*\)$/user = "admin:\1"/p' /root/gatus-credentials.txt; } AUTH | curl -K - -sk --resolve "$H:443:127.0.0.1" "https://$H/api/v1/endpoints/this-instance_nginx-https/statuses" \
  | jq -e '.results[-1] | {status, success, conditionResults}'
{ "status": 200, "success": true, "conditionResults": [ { "condition": "[STATUS] == 200", "success": true }, { "condition": "[BODY] == pat(ok*)", "success": true }, { "condition": "[CERTIFICATE_EXPIRATION] > 720h", "success": true } ] }

Status badges live under the same API, for example https://<your-host>/api/v1/endpoints/this-instance_ssh/health/badge.svg, and they also need the password.

7

Add your own endpoints

The configuration is /etc/gatus/config.yaml. Only root and the gatus service account can read it, because the alerting tokens you add will live there. This is the starter file:

sudo stat -c '%a %U:%G %n' /etc/gatus/config.yaml
sudo grep -vE '^\s*#|^$' /etc/gatus/config.yaml
640 root:gatus /etc/gatus/config.yaml storage: type: sqlite path: /var/lib/gatus/data.db web: address: 127.0.0.1 port: 8080 metrics: false ui: title: "Status | Gatus" header: "Service Health" description: "Live health and uptime of the services monitored by this instance." endpoints: - name: nginx-https group: this-instance url: "https://127.0.0.1/healthz" interval: 60s client: insecure: true conditions: - "[STATUS] == 200" - "[BODY] == pat(ok*)" - "[CERTIFICATE_EXPIRATION] > 720h" - name: gatus group: this-instance url: "http://127.0.0.1:8080/health" interval: 60s conditions: - "[STATUS] == 200" - "[BODY].status == UP" - name: ssh group: this-instance url: "tcp://127.0.0.1:22" interval: 60s conditions: - "[CONNECTED] == true"

Edit it with sudoedit (or your editor of choice through sudo) and append your services under endpoints:. This example adds a website with a response-time and certificate check, a database port, and a DNS record:

sudoedit /etc/gatus/config.yaml
- name: website group: production url: "https://www.example.com/" interval: 60s conditions: - "[STATUS] == 200" - "[RESPONSE_TIME] < 800" - "[CERTIFICATE_EXPIRATION] > 336h" - name: postgres group: production url: "tcp://10.0.1.20:5432" interval: 30s conditions: - "[CONNECTED] == true" - name: dns-www group: production url: "1.1.1.1" dns: query-name: "www.example.com" query-type: "A" conditions: - "[DNS_RCODE] == NOERROR"

Gatus checks the file for changes every 30 seconds and reloads it on its own. You do not need to restart it.

If an edit does not parse, Gatus logs the error and exits. systemd keeps restarting it every few seconds, so the dashboard is down until you correct the file. It then comes back by itself. Check the service and the log after each change:

systemctl is-active gatus
sudo journalctl -u gatus -n 20 --no-pager -o cat

To be notified when a check fails, add an alerting: block (for example Slack, email or PagerDuty) and an alerts: list on each endpoint. The Gatus alerting documentation has an example for every provider. Keep the file 0640 root:gatus.

The Gatus log shows each check as it runs:

sudo journalctl -u gatus -n 3 --no-pager -o cat | grep -E 'success=(true|false)'
2026/10/21 16:08:29 [watchdog.executeEndpoint] Monitored group=this-instance; endpoint=nginx-https; key=this-instance_nginx-https; success=true; errors=0; duration=4ms 2026/10/21 16:08:29 [watchdog.executeEndpoint] Monitored group=this-instance; endpoint=gatus; key=this-instance_gatus; success=true; errors=0; duration=1ms 2026/10/21 16:08:29 [watchdog.executeEndpoint] Monitored group=this-instance; endpoint=ssh; key=this-instance_ssh; success=true; errors=0; duration=0s
8

Monitoring private services

Gatus checks run from this VM, so Gatus can watch services that are not on the internet at all. Examples are databases, internal APIs and load balancers in the same virtual network or in peered networks. Make sure the network security groups of those services allow traffic from this VM.

ICMP (icmp://) checks work too, without giving Gatus any capability. The image allows unprivileged ICMP sockets for the gatus group only (/etc/sysctl.d/60-sac-gatus-ping.conf).

9

HTTPS certificate and host name

The certificate was created on your VM's first boot for the VM's public IP address, its private IP address and localhost. Only root can read the key and the credentials file. The password hash is readable by root and nginx's group:

sudo openssl x509 -in /etc/ssl/gatus/gatus.crt -noout -subject -dates -ext subjectAltName
sudo stat -c '%a %U:%G %n' /etc/ssl/gatus/gatus.key /etc/ssl/gatus/gatus.crt /etc/nginx/.gatus.htpasswd /root/gatus-credentials.txt
subject=CN = 20.121.0.141, O = sac notBefore=Oct 21 16:07:30 2026 GMT notAfter=Jan 23 16:07:30 2029 GMT X509v3 Subject Alternative Name: IP Address:20.121.0.141, IP Address:10.0.0.10, IP Address:127.0.0.1, DNS:localhost 600 root:root /etc/ssl/gatus/gatus.key 644 root:root /etc/ssl/gatus/gatus.crt 640 root:www-data /etc/nginx/.gatus.htpasswd 600 root:root /root/gatus-credentials.txt

To serve the dashboard under your own DNS name, point the name at this VM and run the helper. The name can be, for example, status.example.com, or the DNS name label of the VM's public IP such as gatus-01.eastus.cloudapp.azure.com. The helper records the name, creates a new self-signed certificate for it, updates nginx and rewrites the URL in the credentials file:

sudo gatus-set-hostname status.example.com

If the VM's public IP address changes (for example a dynamic address after a deallocation), run the helper again so that the certificate names the new address too.

To use a certificate from your own certificate authority, replace /etc/ssl/gatus/gatus.key (keep it 0600 root) and /etc/ssl/gatus/gatus.crt. Then run sudo systemctl reload nginx.

10

Metadata access is blocked for Gatus

gatus.service and nginx.service carry a systemd IPAddressDeny rule for the Azure Instance Metadata Service and the Azure wire server. No check can reach them, even if an endpoint's URL points at one. First boot, which reads the public IP address, runs outside both services. DNS names still resolve for your checks, because Gatus asks Ubuntu's local resolver (127.0.0.53), which forwards on its own:

systemctl show -p IPAddressDeny gatus.service nginx.service
IPAddressDeny=169.254.169.254/32 168.63.129.16/32 IPAddressDeny=169.254.169.254/32 168.63.129.16/32

A Gatus dns endpoint that names 168.63.129.16 as its server is blocked by the same rule. Use another resolver address in such checks.

Maintenance

  • Password: the per-VM password is in /root/gatus-credentials.txt and its bcrypt hash in /etc/nginx/.gatus.htpasswd. To change it, run sudo htpasswd -B /etc/nginx/.gatus.htpasswd admin, which prompts for the new password. Then run sudo systemctl reload nginx and update the credentials file. To add more viewers, run the same command with a different user name.
  • A public status page: the image keeps the whole page behind the password. If you want a page anyone can read, decide which paths to open. Add an nginx location with auth_basic off; for them in /etc/nginx/sites-available/sac-gatus, then run sudo nginx -t && sudo systemctl reload nginx. The API and the badges then show your endpoint names and history to anyone.
  • Keep Gatus on loopback: leave web.address: 127.0.0.1 in the configuration. Gatus has no password of its own, so binding it to a public address would publish your dashboard and API to anyone.
  • History: results are kept in the SQLite database /var/lib/gatus/data.db. Back up that file together with /etc/gatus/config.yaml, for example to Azure Blob Storage. Stop Gatus first, or use sqlite3 .backup.
  • Upgrades: to move to a newer Gatus release, deploy the newer image version when it is published, and copy your config.yaml across.
  • Security patches: unattended-upgrades remains enabled, so the OS keeps receiving security updates.

Security recommendations

  • Keep the network security group tight. Open 22 only to your management address, and 80/443 only to the people who need the dashboard.
  • Keep alerting tokens in the config file only. Keep its 0640 root:gatus permissions, and rotate the tokens if the VM is ever shared.
  • Use your own certificate for the dashboard if other people sign in to it. Browsers then trust it without a warning.

Support and licensing

Gatus is developed by TwiN and contributors and distributed under the Apache License 2.0. This image ships a binary that SecureAnyCloud compiled from the unmodified 5.36.0 source with Go 1.27.3 and updated Go modules. The build record and licence are in /usr/share/doc/gatus/. SecureAnyCloud provides the packaging, the hardening, first-boot HTTPS and per-VM credentials, and 24/7 support.

SecureAnyCloud is not affiliated with or endorsed by the Gatus project. Product names are marks of their owners and are used here only to identify the software.

  • Email: sales@secureanycloud.com
  • Website: secureanycloud.com
  • Support hours: 24/7, with a 24-hour response SLA

Deploy on Azure

Launch Gatus 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