VPS DeploymentIntermediate 13 min Lesson 27 of 30

Monitoring Your Server

Know before your users do: uptime checks, resource monitoring, application metrics and alerts that are worth waking up for.

VPS Deployment · Lesson 27 of 30
0/30 done(0%)

What is it? #

Monitoring on a single VPS has three layers: is the site up, is the machine healthy, and is the application behaving.

The minimum useful setup is an external uptime check and a disk space alert. Those two catch a large share of real incidents and take fifteen minutes to configure.

Beyond that, resource monitoring shows trends — memory creeping up over a week, disk filling gradually — which lets you act before something breaks.

Alerts must be worth acting on. An alert that fires regularly and is always ignored trains everyone to ignore the one that matters.

Think of it like this #

Smoke alarms and a dashboard. The alarm wakes you for a fire; the dashboard tells you the boiler pressure has been drifting for a fortnight.

You need both, and the alarm must not go off every time someone makes toast.

Simple example #

An uptime check from outside the server every minute, alerts for disk above 85%, memory pressure and certificate expiry, plus a dashboard showing CPU, memory, disk and request rate.

Code #

TEXT
Layer 1: external uptime checks (do this first)

An external service requests your health endpoint every minute from
outside the server. It catches what internal monitoring cannot: the
machine being down, the network failing, DNS breaking, TLS expiring.

Free options: UptimeRobot, Better Stack, Healthchecks.io for cron jobs.
Check a real endpoint that exercises the database, not a static page.
PYTHON
# A health endpoint worth checking
@app.get("/health")
def health():
    checks = {"database": False, "cache": False}
    try:
        db.execute("SELECT 1")
        checks["database"] = True
    except Exception:
        pass
    try:
        cache.ping()
        checks["cache"] = True
    except Exception:
        pass

    healthy = checks["database"]          # the cache is optional, the DB is not
    return ({"status": "ok" if healthy else "degraded", "checks": checks},
            200 if healthy else 503)
BASH
#!/usr/bin/env bash
# Layer 2: resource checks, run every 5 minutes from cron
THRESHOLD=85

DISK=$(df / --output=pcent | tail -1 | tr -dc '0-9')
[ "<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>D</mi><mi>I</mi><mi>S</mi><mi>K</mi><mi mathvariant="normal">&quot;</mi><mo>−</mo><mi>g</mi><mi>t</mi><mi mathvariant="normal">&quot;</mi></mrow><annotation encoding="application/x-tex">DISK&quot; -gt &quot;</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord mathnormal" style="margin-right:0.0785em;">I</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal" style="margin-right:0.0715em;">K</span><span class="mord">&quot;</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">t</span><span class="mord">&quot;</span></span></span></span>THRESHOLD" ] && alert "disk at ${DISK}%"

MEM=<span class="katex-error" title="ParseError: KaTeX parse error: Expected &#x27;}&#x27;, got &#x27;EOF&#x27; at end of input: …{printf &quot;%.0f&quot;," style="color:#cc0000">(free | awk &#x27;/Mem:/ {printf &quot;%.0f&quot;,</span>3/$2 * 100}')
[ "<span class="katex-error" title="ParseError: KaTeX parse error: Expected &#x27;EOF&#x27;, got &#x27;&amp;&#x27; at position 15: MEM&quot; -gt 90 ] &amp;̲&amp; alert &quot;memory…" style="color:#cc0000">MEM&quot; -gt 90 ] &amp;&amp; alert &quot;memory at</span>{MEM}%"

LOAD=$(cut -d' ' -f1 /proc/loadavg)
CORES=$(nproc)
awk -v l="<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>L</mi><mi>O</mi><mi>A</mi><mi>D</mi><mi mathvariant="normal">&quot;</mi><mo>−</mo><mi>v</mi><mi>c</mi><mo>=</mo><mi mathvariant="normal">&quot;</mi></mrow><annotation encoding="application/x-tex">LOAD&quot; -v c=&quot;</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">L</span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord">&quot;</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.4306em;"></span><span class="mord mathnormal" style="margin-right:0.0359em;">v</span><span class="mord mathnormal">c</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">=</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.6944em;"></span><span class="mord">&quot;</span></span></span></span>CORES" 'BEGIN { exit !(l > c * 2) }' && alert "load <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>L</mi><mi>O</mi><mi>A</mi><mi>D</mi><mi>o</mi><mi>n</mi></mrow><annotation encoding="application/x-tex">LOAD on</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.6833em;"></span><span class="mord mathnormal">L</span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord mathnormal">o</span><span class="mord mathnormal">n</span></span></span></span>CORES cores"

systemctl is-active --quiet myapp || alert "myapp is not running"
systemctl is-active --quiet nginx || alert "nginx is not running"

alert() { curl -fsS -m 10 "https://alerts.example.com/notify?msg=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>e</mi><mi>c</mi><mi>h</mi><mi>o</mi><mi mathvariant="normal">&quot;</mi></mrow><annotation encoding="application/x-tex">(echo &quot;</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">h</span><span class="mord mathnormal">o</span><span class="mord">&quot;</span></span></span></span>1" | jq -sRr @uri)" || true; }
BASH
# Layer 3: a dashboard, for trends rather than alerts
# Netdata: one command, immediate per-second metrics, good for a single VPS
bash <(curl -SsL https://my-netdata.io/kickstart.sh)
# Bind it to localhost and reach it through an SSH tunnel rather than exposing it:
# ssh -L 19999:localhost:19999 app-prod
TEXT
Alerts worth having, and their thresholds

site down (external)        immediately — this is the one that wakes people
disk above 85%              hours of warning before it fills
memory above 90% sustained  before the OOM killer intervenes
service not running         within a minute
certificate under 14 days   weeks of warning
backup ping missing         within a day
error rate above 2%         sustained for 5 minutes

Alerts NOT worth having: brief CPU spikes, single failed requests,
anything that resolves itself before anyone can look.

How it works #

External checks are first because they see what the server cannot report about itself. A machine that is down cannot send an alert saying it is down.

The health endpoint should exercise real dependencies. A static 200 tells you the web server is running and nothing about whether the application can serve a request. Distinguishing essential dependencies from optional ones — the database from the cache — avoids failing over something that only degrades the service.

The resource script uses simple thresholds. Disk at 85% gives hours of warning; waiting until 95% often does not, because logs fill quickly during an incident.

Load is compared with the core count, since a load of 4 is fine on four cores and severe on one.

Service checks with systemctl is-active catch a service that failed to restart after hitting its start limit, which is exactly the situation the restart limit is designed to surface.

Netdata gives a detailed dashboard from a single command, which suits one server. Binding it to localhost and reaching it over an SSH tunnel avoids exposing an unauthenticated dashboard to the internet — a common mistake.

The alert list is deliberately short. Every alert should represent something a human must act on; the rest belongs on a dashboard.

Real-world use #

The two highest-value monitors are an external uptime check and a disk space alert. Between them they catch a large proportion of real incidents on small deployments.

Alert fatigue is the failure mode to guard against. When most alerts are noise, the important one is missed, and the fix is deleting alerts rather than adding more.

As systems grow, Prometheus with Grafana or a hosted service replaces ad hoc scripts, and application metrics join the infrastructure ones — the monitoring lesson in the system design track covers that.

Notification routing matters. An alert that arrives only by email at 3am is not an alert. Route the urgent ones to something that makes a noise.

Every alert should have a runbook, even a three-line one. Whoever is woken should not have to work out the response from first principles.

Common mistakes #

  • Monitoring only from the server, so it cannot report its own outage.
  • A health endpoint that returns 200 unconditionally.
  • Disk alerts set at 95%, leaving no time to react.
  • Exposing a monitoring dashboard publicly without authentication.
  • So many alerts that people ignore all of them.

Practice #

Set up an external uptime check against a health endpoint that verifies the database. Add a cron script alerting on disk, memory and service status, and install a dashboard bound to localhost. Then fill the disk deliberately on a test machine and confirm the alert fires before anything breaks.

Quick quiz

  1. 1. Why is an external uptime check the first thing to set up?

  2. 2. What should a health endpoint do?

  3. 3. Why alert on disk at 85% rather than 95%?

  4. 4. Why compare load average with the core count?

  5. 5. What is the main risk of too many alerts?

Summary

  • Start with an external uptime check and a disk space alert.
  • Make the health endpoint verify real dependencies.
  • Alert on disk, memory, service status, certificate expiry and missing backups.
  • Keep dashboards private, behind a tunnel or authentication.
  • Fewer, actionable alerts beat many that are ignored.