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:
- Uptime Kuma (self-hosted) — monitors all your internal services and external URLs from your VPS.
- UptimeRobot free tier — pings your public endpoints from outside your network every 5 minutes at no cost.
- 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:
- Open Settings → Notifications → Add Notification.
- Choose Telegram.
- Enter your bot token and chat ID.
- Click Test — you should receive a test message immediately.
- 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
.envfile 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.