How to Monitor Server Uptime Cheaply in 2025

by David Park
How to Monitor Server Uptime Cheaply in 2025

You will end up with a layered uptime monitoring stack: a self-hosted Uptime Kuma instance on your existing VPS sending alerts to a free Telegram bot, a free UptimeRobot account as an external check, and a lightweight cron script that pages you if the VPS itself goes dark. Total recurring cost: $0 in new spend if you already have a VPS.

Prerequisites

  • Ubuntu 24.04 VPS (1 vCPU / 1 GB RAM minimum — a $4/month Hetzner CAX11 works fine)
  • Docker 26+ and Docker Compose v2 installed
  • A domain or subdomain pointed at the VPS (optional but recommended for HTTPS)
  • A free UptimeRobot account at uptimerobot.com
  • A Telegram account and a bot token from @BotFather
  • Root or sudo access

1. Why Layer Multiple Monitors

A single monitor has a single point of failure. If the tool lives on the same server it watches, a kernel panic silences both. The approach here uses three independent layers:

  1. Uptime Kuma (self-hosted) — monitors all your internal services and external URLs from your VPS.
  2. UptimeRobot free tier — pings your public endpoints from outside your network every 5 minutes at no cost.
  3. Cron heartbeat script — runs on a second machine (or a free Oracle Cloud Free Tier instance) and alerts you if the primary VPS stops responding.

This covers the failure modes that matter for indie projects without adding a new monthly bill.


2. Install Uptime Kuma with Docker Compose

Uptime Kuma is a self-hosted monitoring UI that supports HTTP, TCP, ping, DNS, and more. It runs comfortably inside 150 MB of RAM.

Step 1. Create the project directory.

mkdir -p /opt/uptime-kuma && cd /opt/uptime-kuma

Step 2. Write the Compose file.

cat > docker-compose.yml <<'EOF'
services:
  uptime-kuma:
    image: louislam/uptime-kuma:1.23.13
    container_name: uptime-kuma
    restart: unless-stopped
    ports:
      - "127.0.0.1:3001:3001"
    volumes:
      - ./data:/app/data
EOF

Pinning to 1.23.13 avoids surprise breakage on latest. Check the Uptime Kuma releases page and update the tag when you upgrade intentionally.

Step 3. Start the container.

docker compose up -d

Expected output:

[+] Running 2/2
 ✔ Network uptime-kuma_default  Created
 ✔ Container uptime-kuma        Started

Step 4. Confirm the container is healthy.

docker ps --filter name=uptime-kuma --format "table {{.Names}}\t{{.Status}}"

Expected output:

NAMES           STATUS
uptime-kuma     Up 12 seconds

3. Reverse-Proxy Uptime Kuma with Nginx

Expose the dashboard over HTTPS so you can reach it from anywhere without opening port 3001 publicly.

Step 5. Install Nginx and Certbot.

apt update && apt install -y nginx certbot python3-certbot-nginx

Step 6. Write the Nginx server block. Replace status.example.com with your subdomain.

cat > /etc/nginx/sites-available/uptime-kuma <<'EOF'
server {
    listen 80;
    server_name status.example.com;

    location / {
        proxy_pass         http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
EOF

Step 7. Enable the site and reload Nginx.

ln -s /etc/nginx/sites-available/uptime-kuma /etc/nginx/sites-enabled/ && \
nginx -t && \
systemctl reload nginx

Step 8. Issue a TLS certificate.

certbot --nginx -d status.example.com --non-interactive --agree-tos -m you@example.com

Certbot rewrites the Nginx config automatically and schedules renewal via the certbot.timer systemd unit.


4. Connect Telegram Alerts to Uptime Kuma

Telegram notifications are free and arrive in under two seconds. You need a bot token and your personal chat ID.

Step 9. Get your Telegram chat ID. Send any message to your bot, then run:

curl -s "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates" | \
  python3 -c "import sys,json; data=json.load(sys.stdin); print(data['result'][0]['message']['chat']['id'])"

Note the integer printed — that is your <CHAT_ID>.

Step 10. In the Uptime Kuma web UI at https://status.example.com:

  1. Open Settings → Notifications → Add Notification.
  2. Choose Telegram.
  3. Enter your bot token and chat ID.
  4. Click Test — you should receive a test message immediately.
  5. Save and attach the notification to each monitor you create.

5. Add External Monitoring with UptimeRobot (Free Tier)

UptimeRobot's free plan gives you 50 monitors checked every 5 minutes from external locations. This is the layer that catches problems your self-hosted Kuma cannot report on — because Kuma itself is down.

Step 11. Log in to uptimerobot.com and click Add New Monitor.

  • Monitor Type: HTTP(s)
  • Friendly Name: Your service name
  • URL: https://yourdomain.com
  • Monitoring Interval: 5 minutes
  • Alert Contacts: Add your email or a Telegram contact via the integrations tab

Repeat for each public-facing URL. 50 monitors covers most indie stacks.

Step 12. Grab your UptimeRobot API key from My Settings → API Settings → Main API Key. Store it — you will use it in the next section.

export UPTIMEROBOT_API_KEY="your-api-key-here"

6. Add a Cron Heartbeat Script for the VPS Itself

If the VPS kernel panics or the network card fails, Uptime Kuma goes silent. Run this script on a second machine — an Oracle Cloud Free Tier ARM instance costs $0/month and works perfectly here.

Step 13. On the second machine, create the heartbeat script.

cat > /usr/local/bin/check-primary-vps.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

HOST="your-primary-vps-ip"
PORT=22
TELEGRAM_TOKEN="your-bot-token"
CHAT_ID="your-chat-id"
TIMEOUT=10

if ! nc -z -w "${TIMEOUT}" "${HOST}" "${PORT}" 2>/dev/null; then
  curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
    --data-urlencode "chat_id=${CHAT_ID}" \
    --data-urlencode "text=ALERT: Primary VPS ${HOST} port ${PORT} unreachable at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi
EOF
chmod +x /usr/local/bin/check-primary-vps.sh

Replace your-primary-vps-ip, your-bot-token, and your-chat-id before saving.

Step 14. Install netcat-openbsd if not present.

apt install -y netcat-openbsd

Step 15. Schedule the script every 3 minutes via cron.

(crontab -l 2>/dev/null; echo "*/3 * * * * /usr/local/bin/check-primary-vps.sh >> /var/log/check-primary-vps.log 2>&1") | crontab -

Verify the entry was added:

crontab -l

Expected output includes:

*/3 * * * * /usr/local/bin/check-primary-vps.sh >> /var/log/check-primary-vps.log 2>&1

7. Verify It Works

Run each check in order before considering the setup complete.

Check 1 — Uptime Kuma is reachable:

curl -o /dev/null -s -w "%{http_code}" https://status.example.com

Expected output: 200

Check 2 — Telegram alert fires from Kuma: In the Uptime Kuma UI, open any monitor, click Edit, then Test Notification. Confirm the message arrives in Telegram within 5 seconds.

Check 3 — Heartbeat script sends an alert when port is closed: Temporarily change the port in the script to a closed port (e.g., 9999), run it manually, and confirm the Telegram alert arrives.

/usr/local/bin/check-primary-vps.sh

Revert the port to 22 after confirming.

Check 4 — UptimeRobot sees your endpoint: In the UptimeRobot dashboard, the monitor status should show Up with a green indicator within 5 minutes of creation.

Check 5 — TLS certificate auto-renews:

certbot renew --dry-run

Expected output ends with: Congratulations, all simulated renewals succeeded:


8. Troubleshooting

Uptime Kuma container exits immediately. Check logs: docker logs uptime-kuma. A common cause is a permission error on ./data. Fix with chown -R 1000:1000 /opt/uptime-kuma/data.

Nginx returns 502 Bad Gateway. Confirm Kuma is listening: ss -tlnp | grep 3001. If nothing appears, the container stopped — run docker compose up -d from /opt/uptime-kuma.

Certbot fails with "Could not bind to IPv4 or IPv6". Nginx is holding port 80. Stop it first: systemctl stop nginx, run certbot, then systemctl start nginx. Certbot's --nginx plugin should handle this automatically, but a stale PID file can interfere.

Telegram messages not arriving. Verify the bot token with: curl -s https://api.telegram.org/bot<TOKEN>/getMe. If the response shows "ok":false, the token is invalid — regenerate it in @BotFather.

Heartbeat script runs but sends no alert even when VPS is down. Check that netcat-openbsd is installed (nc -h 2>&1 | head -1). The BSD variant supports -w timeout; the traditional variant does not.

UptimeRobot shows monitor as paused. Free accounts pause monitors if you do not log in for 60 days. Set a calendar reminder to log in monthly, or upgrade to the $7/month Solo plan to remove the restriction.


Next Steps

  • Add a status page in Uptime Kuma (Settings → Status Page) so users can check service health without contacting you.
  • Configure maintenance windows in UptimeRobot to suppress false alerts during planned reboots.
  • Store the Telegram bot token and chat ID in a .env file and reference them in both Docker Compose and the heartbeat script to avoid scattering secrets across configs.
  • Explore Grafana + Prometheus if you need historical latency graphs — both run free on the same Hetzner box with another 200 MB of RAM headroom.

The stack described here lets you monitor server uptime cheaply without compromising on alert speed or reliability. Three independent layers, zero new monthly cost, and every alert lands in your pocket within seconds of a failure.