Back to tutorials
Tutorial

Uptime Monitoring Tutorial (2026): External Checks + On-Server Health Endpoints for VPS & Dedicated Hosting

Uptime monitoring tutorial for 2026: set external checks, health endpoints, and alerting for VPS or dedicated servers without noisy logs.

By Anurag Singh
Updated on Sep 01, 2026
Category: Tutorial
Share article
Uptime Monitoring Tutorial (2026): External Checks + On-Server Health Endpoints for VPS & Dedicated Hosting

Your users don’t care that “the server is up” if checkout spins forever or PHP-FPM is stuck. A practical uptime monitoring tutorial for 2026 needs two layers. Use external checks that behave like a real visitor. Add a small on-server health endpoint that tells you which layer is failing.

This guide shows a monitoring setup that fits WordPress, WooCommerce, and custom apps on a VPS or dedicated server. You’ll build clean signals, add fast isolation checks, and avoid a classic mistake. Don’t alert on noise when customers can still use the site.

What you’ll build (and what you won’t)

  • External uptime checks for HTTPS, redirects, and a “real page” that executes WordPress/PHP.
  • An internal health endpoint that reports web + PHP-FPM status without leaking sensitive data.
  • Actionable alerts that fire only when the problem is user-visible.
  • Optional: mail alert deliverability sanity so notifications don’t disappear into spam.

You won’t need Kubernetes, service meshes, or a full observability platform. This stays firmly in day-to-day hosting operations.

Prerequisites and a safe test environment

Examples use Ubuntu Server 24.04 LTS with Nginx + PHP-FPM. The same pattern works on Debian 12 and most common VPS images. You’ll need root or sudo access.

If you want a clean place to follow along, start on a small VPS and scale later. A HostMyCode VPS is enough for the tutorial. You can move to managed ops once the workflow is proven.

Before changing anything, confirm your baseline:

uname -a
nginx -v
php-fpm8.3 -v || php-fpm8.2 -v
systemctl status nginx --no-pager

Step 1: Decide what “up” means for your site

Most false alarms come from fuzzy definitions. Stick to three checks. Give each one a single job:

  1. Network/SSL check: TCP/443 is reachable and the TLS handshake completes.
  2. Web stack check: a fast URL served by Nginx (static or cached) returns HTTP 200.
  3. Application check: a dynamic URL (uncached) proves the PHP + database path works.

For WordPress, the application check should hit PHP without dragging in expensive templates or heavy queries.

If you don’t already have a lightweight dynamic URL, you’ll create one in Step 3.

Step 2: Build an external check that behaves like a user

Run external checks from outside your network. Any hosted uptime service works.

Validate the request/response logic locally first. The curl examples below map cleanly to most monitoring tools.

2.1 Check HTTPS + redirects

This catches broken certificates and redirect loops. Run it from your laptop or a separate server:

curl -fsSIL --max-time 10 https://example.com/ | sed -n '1,10p'
  • -I requests headers only (fast).
  • -L follows redirects.
  • -f fails on 4xx/5xx so the check is strict.

If this tends to break around renewal time, keep an SSL runbook handy. HostMyCode already has a companion guide: SSL renewal troubleshooting tutorial.

2.2 Check a real page (GET, not HEAD)

Some stacks behave differently for HEAD requests. Use GET instead. Record timing:

curl -fsS --max-time 15 -o /dev/null \
  -w 'code=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n' \
  https://example.com/

Set thresholds that match your site, not someone else’s benchmark.

For a typical tuned WordPress install on a VPS, a sustained ttfb above ~1.5s from multiple regions is usually real pain. A single spike isn’t.

Step 3: Add a private on-server health endpoint (Nginx + PHP-FPM)

External checks tell you the site is failing. The health endpoint tells you which layer is failing.

This lets you triage faster, without jumping into SSH immediately during an incident.

You’re aiming for a URL like https://example.com/.well-known/health that:

  • Returns 200 OK quickly when Nginx and PHP-FPM are healthy.
  • Returns 503 if PHP-FPM is down or overloaded (so monitors mark it as “down”).
  • Is accessible only to your monitoring provider IPs and your office/VPN.

3.1 Create a tiny PHP health script

Put this outside your WordPress docroot. That keeps themes and plugins from casually touching it. Example:

sudo mkdir -p /var/www/health
sudo tee /var/www/health/health.php >/dev/null <<'PHP'
<?php
// Minimal health endpoint: checks PHP runtime and (optionally) local services.
header('Content-Type: application/json');

$out = [
  'ok' => true,
  'ts' => gmdate('c'),
  'php' => PHP_VERSION,
];

// Optional: quick check that PHP can open a local TCP socket (php-fpm is already running if this executes).
// Add service checks carefully; keep timeouts tiny.

echo json_encode($out, JSON_UNESCAPED_SLASHES);
PHP

Lock permissions:

sudo chown -R root:root /var/www/health
sudo chmod -R 755 /var/www/health

3.2 Wire it into Nginx with IP allowlists

Edit your site’s Nginx server block. Common paths:

  • /etc/nginx/sites-available/example.com (Debian/Ubuntu)
  • Or an include under /etc/nginx/conf.d/

Add a location block:

location = /.well-known/health {
    # Allow only your monitoring vendor IP ranges and your own IP.
    # Replace these with real IPs/CIDRs.
    allow 203.0.113.10;
    allow 198.51.100.0/24;
    deny all;

    root /var/www/health;
    include snippets/fastcgi-php.conf;

    # If you don't have snippets/fastcgi-php.conf, use the basics:
    # fastcgi_param SCRIPT_FILENAME $document_root/health.php;

    fastcgi_param SCRIPT_FILENAME $document_root/health.php;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;

    fastcgi_connect_timeout 2s;
    fastcgi_read_timeout 2s;

    # If PHP-FPM is unhealthy, you want a hard fail.
    fastcgi_intercept_errors on;
}

Test and reload:

sudo nginx -t
sudo systemctl reload nginx

Verify from an allowed IP:

curl -fsS https://example.com/.well-known/health

3.3 Add an Nginx-only fast check (no PHP)

This is your “web server is alive” probe. It separates Nginx issues from PHP/app issues.

Add:

location = /.well-known/ping {
    allow 203.0.113.10;
    allow 198.51.100.0/24;
    deny all;

    add_header Content-Type text/plain;
    return 200 "ok\n";
}

Your alerts can now point you in the right direction.

If /ping is fine but /health fails, look at PHP-FPM, an upstream socket issue, or the application layer.

Step 4: Make PHP-FPM failures visible (status page + service guardrails)

PHP-FPM can show as “running” and still drop requests. Common causes include max children limits, slow scripts, or stuck pools.

Two small changes make these failures easier to spot. Enable an FPM status endpoint. Add sane timeouts.

4.1 Enable PHP-FPM status (local only)

Edit your pool config, typically:

  • /etc/php/8.3/fpm/pool.d/www.conf

Enable status and ping:

sudo sed -i 's~^;pm.status_path =.*~pm.status_path = /fpm-status~' /etc/php/8.3/fpm/pool.d/www.conf
sudo sed -i 's~^;ping.path =.*~ping.path = /fpm-ping~' /etc/php/8.3/fpm/pool.d/www.conf
sudo systemctl restart php8.3-fpm

Expose it through Nginx but restrict to localhost (or your VPN IP):

location = /fpm-status {
    allow 127.0.0.1;
    deny all;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

location = /fpm-ping {
    allow 127.0.0.1;
    deny all;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Check locally:

curl -fsS http://127.0.0.1/fpm-ping
curl -fsS http://127.0.0.1/fpm-status | head

4.2 Quick guardrail: timeouts that fail fast

If Nginx waits forever on upstreams, your monitor reports timeouts. You also lose the useful “why.”

In most cases, it’s better to fail quickly with a 5xx. Then you can alert, triage, and recover.

In Nginx, keep upstream timeouts reasonable for normal pages (tune for your workload):

fastcgi_connect_timeout 5s;
fastcgi_send_timeout 30s;
fastcgi_read_timeout 30s;

If you’re seeing CPU spikes and slow WordPress admin screens, “longer timeouts” won’t save you. Caching and PHP-FPM tuning usually will.

Pair this with performance work; HostMyCode’s VPS performance optimization tutorial is a solid follow-up.

Step 5: Add a minimal on-server checker for cron-based alerting

Hosted uptime services are useful. Still, keep a basic local script as a backstop.

If your monitoring vendor is blocked, misconfigured, or having its own outage, you still get a signal from the server itself.

5.1 Create a healthcheck script

sudo tee /usr/local/bin/healthcheck.sh >/dev/null <<'SH'
#!/usr/bin/env bash
set -euo pipefail

URL_PING="https://example.com/.well-known/ping"
URL_HEALTH="https://example.com/.well-known/health"

# Use a short timeout. If it can't answer fast, treat it as down.
PING_CODE=$(curl -k -sS -o /dev/null -w "%{http_code}" --max-time 5 "$URL_PING" || echo "000")
HEALTH_CODE=$(curl -k -sS -o /dev/null -w "%{http_code}" --max-time 8 "$URL_HEALTH" || echo "000")

if [[ "$PING_CODE" != "200" ]]; then
  echo "CRIT: Nginx ping failed (code=$PING_CODE) on $(hostname)" 
  exit 2
fi

if [[ "$HEALTH_CODE" != "200" ]]; then
  echo "CRIT: App health failed (code=$HEALTH_CODE) on $(hostname)" 
  exit 2
fi

echo "OK: ping=200 health=200"
SH
sudo chmod +x /usr/local/bin/healthcheck.sh

5.2 Run it from cron and email output

Install a local mailer if you don’t have one (choose one):

sudo apt update
sudo apt install -y mailutils

Add a cron entry to run every 5 minutes:

sudo crontab -e
*/5 * * * * /usr/local/bin/healthcheck.sh | mail -s "Healthcheck $(hostname)" you@example.com

If emails don’t arrive, fix deliverability before you rely on alerting. Use: Email deliverability troubleshooting tutorial.

Step 6: Keep monitoring traffic from becoming an attack vector

Health endpoints are small, but they’re still endpoints. Treat them like any other surface area.

Lock them down. Limit abuse.

6.1 Rate-limit the health endpoints (Nginx)

In /etc/nginx/nginx.conf (http block):

limit_req_zone $binary_remote_addr zone=health_zone:10m rate=5r/s;

Then in the health locations:

limit_req zone=health_zone burst=10 nodelay;

6.2 Enforce a firewall baseline

If your VPS hosts public sites, don’t leave admin ports open to the world. If you’re using iptables, use a hosting-safe ruleset that won’t lock you out mid-change. This guide is practical: IPTables firewall configuration tutorial.

Step 7: Alerting rules that don’t waste your time

Good alerting is intentionally boring. Here’s a policy that works well for most hosting operators:

  • Page down (critical): external check fails from 2 regions for 2 consecutive runs.
  • Degraded (warning): 95th percentile TTFB over 1.5–2.5s for 10 minutes (tune to your audience).
  • SSL expiry (warning): certificate expires in 14 days.
  • Health endpoint down but ping up (critical): usually PHP-FPM, upstream socket, or app deadlock.

During an incident, context matters as much as symptoms. Tie alerts to your change history.

Track plugin updates, theme edits, PHP upgrades, deploys, and firewall changes.

Step 8: Incident checklist (5 minutes to triage)

When your monitor fires, run this sequence. You’ll narrow the problem quickly.

This also helps you avoid random “try restarting everything” moves.

  1. Check ping vs health: does /.well-known/ping work but /.well-known/health fail?
  2. Confirm DNS isn’t the issue: resolve from a public resolver.
    dig +short A example.com @1.1.1.1
    
  3. Check Nginx:
    sudo systemctl status nginx --no-pager
    sudo tail -n 80 /var/log/nginx/error.log
    
  4. Check PHP-FPM:
    sudo systemctl status php8.3-fpm --no-pager
    sudo journalctl -u php8.3-fpm -n 120 --no-pager
    
  5. Check disk pressure:
    df -h
    sudo du -xhd1 /var | sort -h | tail
    

If you’re migrating between servers, validate TTL and cutover sequencing as well.

A clean cutover prevents “phantom outages” caused by traffic still hitting the old IP. Use: DNS TTL reduction tutorial.

Common pitfalls (and how to avoid them)

  • Monitoring only the homepage: caching can mask PHP failures. Monitor one dynamic path too.
  • Health endpoint is public: it will get probed. Restrict by IP and rate-limit it.
  • Alerting on single-region failures: transient routing problems happen. Require multi-region confirmation.
  • Timeouts set too high: you get “unknown” instead of a clear 5xx. Fail fast.
  • No notification reliability testing: if email alerting is broken, you’ll find out during an outage.

Summary: a hosting-grade monitoring stack you can trust

External checks measure what customers actually experience. Private health endpoints tell you which layer failed.

Put them together and you get a clear signal. You also avoid turning monitoring into another full-time job.

If you want this running on infrastructure built for real workloads, start with a HostMyCode VPS for full control. Or use managed VPS hosting if you’d rather spend your time on the site than on server upkeep.

If your uptime alerts keep blaming “server issues” but the root cause stays fuzzy, put the site on a VPS with predictable resources and clean routing. HostMyCode offers HostMyCode VPS plans for hands-on admins, plus managed VPS hosting if you want help with OS updates, web stack stability, and monitoring-ready defaults.

FAQ

Should my uptime check use HEAD or GET?

Use GET for at least one check. HEAD can skip parts of your stack. It can also miss cache or PHP-specific failures.

Is it safe to expose a health endpoint on the public internet?

It can be, but only if you restrict it by IP/CIDR, rate-limit it, and keep the response minimal. Don’t leak versions, usernames, or internal hostnames.

How often should I run checks?

For most sites, every 60 seconds externally is a good balance. For local cron checks, every 5 minutes is usually fine as a fallback.

What’s the best first signal for WordPress outages?

Monitor a dynamic URL that hits PHP-FPM (your health endpoint or a lightweight WP path). Pair it with a static ping endpoint so you can separate “web server” failures from “app” failures.

My site is up, but monitoring reports slow responses. What do I do first?

Check TTFB from multiple regions, then look for PHP-FPM saturation (max children) and error logs. If it’s resource pressure, tuning Nginx/PHP-FPM and adding object cache often fixes it.

Uptime Monitoring Tutorial (2026): External Checks + On-Server Health Endpoints for VPS & Dedicated Hosting | HostMyCode