Technology

VPS Monitoring with Prometheus, Grafana and Uptime Kuma

VPS Monitoring: Metrics, Availability and Alerts

A slow site can be difficult to diagnose without monitoring. A manual check may show ordinary CPU and memory usage while requests are still queuing. Historical metrics help you see what changed and narrow down where to investigate.

This guide sets up VPS monitoring with Prometheus for metrics, Grafana for dashboards and alerts, and Uptime Kuma for checks from an independent location. The command examples assume a Linux amd64 host using Debian or Ubuntu, systemd, and sudo access. Use the linked official installation instructions for Grafana and Docker, then configure the connections, dashboards and checks below. Verify supported releases and required network access before deployment.

Roles of Prometheus, Grafana and Uptime Kuma

Prometheus and Grafana provide system and application metrics, including CPU, memory, disks, network activity and application behavior. Uptime Kuma, when hosted outside the monitored infrastructure, checks whether websites and APIs respond and whether TLS certificates are valid. Metrics and external checks provide complementary evidence during an incident.

Prometheus scrapes metrics from exporters, such as Node Exporter on a VPS, and stores time-series data. With the relevant metrics, graphs can reveal IOwait spikes or memory growth, though those patterns alone do not prove a memory leak. For more on the exporter and alerting stack, see the Prometheus, Grafana and Node Exporter guide.

Grafana queries these metrics to display panels and evaluate alerts. Use panels for CPU utilization, available memory, disk activity and network throughput. Configure contact points and notification policies for the channels your team monitors, then choose thresholds and pending periods that distinguish brief spikes from sustained pressure.

This guide uses Grafana-managed alert rules: Grafana queries Prometheus, evaluates conditions and routes notifications through its contact points or policies. A separate Prometheus Alertmanager deployment is not required for this path. An alternative is to evaluate rules in Prometheus and send firing alerts to Alertmanager; that option requires its own rule loading, Alertmanager connection and receiver/routing configuration. Choose one primary metric-alerting path to avoid duplicate notifications. Uptime Kuma manages its own external-check notifications.

Uptime Kuma checks sites and services from its deployment location, measures response times and tests TCP ports. Push monitors can check whether a scheduled job reports successful completion within an expected interval. Its status pages can display selected monitors to a team or clients.

Combine monitoring with capacity planning for variable traffic. Metrics help investigate resource pressure, external checks detect availability problems, and alerts direct responders to the affected service. For a store preparing for a promotion, see the guide to sizing vCPU, RAM and IOPS for WooCommerce.

Inspect disk activity and IOwait alongside CPU usage when investigating storage-related delays. The NVMe VPS hosting guide explains how storage type and workload affect performance.

Install and Configure Prometheus

How Prometheus Collects Metrics

Prometheus works by scraping endpoints that expose metrics in plain text. Your VPS will run Node Exporter to expose system metrics. Prometheus itself can live on the same server for a small setup, or on a separate monitoring box if you want to scale later. Choose the location according to the failure isolation and resource capacity you need.

Install Node Exporter on the VPS you want to monitor

It exposes CPU loads, memory, disks, filesystems, and network metrics; systemd status requires enabling its optional systemd collector. The downloads below target Linux amd64 and pin older versions, not the latest releases. Before starting Node Exporter, restrict inbound port 9100 to the Prometheus server, or block remote access for local-only scraping. On Debian/Ubuntu:

# Create a user
sudo useradd --no-create-home --shell /usr/sbin/nologin node_exporter

# Pinned example: Node Exporter 1.7.0 (Linux amd64), not a latest-release selector
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz

tar -xzf node_exporter-1.7.0.linux-amd64.tar.gz
sudo cp node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin/

# Systemd service
sudo tee /etc/systemd/system/node_exporter.service >/dev/null << 'EOF'
[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter

# Default port is 9100
# Confirm it works
curl http://127.0.0.1:9100/metrics | head -n 5

After starting Node Exporter, confirm the Prometheus host can reach port 9100 and an untrusted host cannot. For local-only scraping, remote access should remain blocked. If metric traffic crosses an untrusted network, use TLS or a VPN; restricting source IPs does not encrypt the traffic.

Install Prometheus

You can run Prometheus on the same machine while testing, or spin up a small VPS just for monitoring. For a single VPS, same-box monitoring is convenient, but it goes down with that VPS. A separate monitoring host provides better failure isolation. The configuration below assumes Node Exporter is on the Prometheus host: if it is on another VPS, replace 127.0.0.1:9100 with that VPS’s reachable private Node Exporter address. Before starting Prometheus below, restrict inbound port 9090 to trusted administrative and Grafana hosts: the example listens on all IPv4 interfaces without authentication.

# Create user and directories
sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus
sudo mkdir -p /etc/prometheus /var/lib/prometheus

# Pinned example: Prometheus 2.53.1 (Linux amd64); check current supported releases
wget https://github.com/prometheus/prometheus/releases/download/v2.53.1/prometheus-2.53.1.linux-amd64.tar.gz

tar -xzf prometheus-2.53.1.linux-amd64.tar.gz
sudo cp prometheus-2.53.1.linux-amd64/prometheus /usr/local/bin/
sudo cp prometheus-2.53.1.linux-amd64/promtool /usr/local/bin/
sudo cp -r prometheus-2.53.1.linux-amd64/consoles /etc/prometheus/
sudo cp -r prometheus-2.53.1.linux-amd64/console_libraries /etc/prometheus/

# Basic config
sudo tee /etc/prometheus/prometheus.yml >/dev/null << 'EOF'
global:
  scrape_interval: 15s
  evaluation_interval: 15s
scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['127.0.0.1:9100']
EOF

sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus

# Systemd service
sudo tee /etc/systemd/system/prometheus.service >/dev/null << 'EOF'
[Unit]
Description=Prometheus
Wants=network-online.target
After=network-online.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/var/lib/prometheus --storage.tsdb.retention.time=15d --web.listen-address=0.0.0.0:9090

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now prometheus

# Visit http://<server-ip>:9090 to confirm

A note on retention: 15 days is the default when neither time nor size retention is configured; the example sets it explicitly. Disk usage also depends on series count and scrape frequency, so monitor free space and tune retention to fit. Remote storage does not automatically change local retention.

Scrape multiple VPS nodes

To add more servers, install Node Exporter on each, allow scraping only from Prometheus, and replace the existing scrape_configs section with one of the examples below. Substitute reachable private IPs for the sample addresses. After saving, validate the configuration with promtool check config /etc/prometheus/prometheus.yml, then restart Prometheus to apply it.

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['10.0.0.11:9100', '10.0.0.12:9100', '10.0.0.13:9100']

To organize targets by role, add labels:

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['10.0.0.11:9100']
        labels:
          role: 'web'
      - targets: ['10.0.0.12:9100']
        labels:
          role: 'db'

Use consistent labels such as role and environment so dashboards and alert rules can group or filter targets.

Optional: HTTP checks from the inside with Blackbox Exporter

Blackbox Exporter can probe HTTP, TCP or ICMP endpoints from its deployment location. The following pinned Linux amd64 download is only an archive download, not a complete installation:

wget https://github.com/prometheus/blackbox_exporter/releases/download/v0.24.0/blackbox_exporter-0.24.0.linux-amd64.tar.gz

The next block is only a Prometheus scrape-job fragment: it assumes Blackbox Exporter is already installed, running at 127.0.0.1:9115 from Prometheus’s perspective, and has an http_2xx module loaded. Prepare that service using the versioned Blackbox Exporter instructions; check release suitability and restrict access to trusted callers. Append this job to the existing scrape_configs list, keep the Node Exporter job, and replace the sample target URLs.

# Append this item under your existing scrape_configs list.
  - job_name: 'blackbox'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://example.com
          - https://api.example.com/health
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: 127.0.0.1:9115

Check the exporter’s /probe output for each intended target and module, including probe_success. Validate the merged Prometheus configuration with promtool check config /etc/prometheus/prometheus.yml, apply it through the existing service workflow and inspect each target’s distinct instance label. A reachable exporter does not by itself prove the probed website is healthy. For the general model, see the Prometheus overview.

Configure Grafana Dashboards and Alerts

Install Grafana and Connect Prometheus

Install Grafana before adding the data source. On the Debian or Ubuntu host, follow the official Grafana APT installation instructions for a supported package. For that package on systemd, reload unit definitions with sudo systemctl daemon-reload, start and enable it with sudo systemctl enable --now grafana-server, then check sudo systemctl status grafana-server. The official Grafana service instructions cover other installation methods. Restrict dashboard access to trusted users and complete secure administrator setup before remote use. In Grafana, add a Prometheus data source using the URL reachable from the Grafana host, such as http://YOUR_PROM_SERVER:9090. Save and test the connection, then confirm a query such as up returns the expected targets.

Start with CPU utilization per instance; the irate query below uses the last two samples within a five-minute lookback, not a five-minute average or system load. Then add memory usage based on MemAvailable, which estimates memory available without swapping rather than simply subtracting cache and buffers. Disk IO time and IOwait follow, and finally network throughput. Choose units and thresholds appropriate to each panel; reserve warning colors for conditions that need attention.

CPU, Memory, Disk and Network Queries

Use these PromQL queries as starting points, then select the relevant instances and devices:

CPU usage (per instance):

100 - (avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

Memory used (percentage based on MemAvailable):

(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100

Disk IO time (per device):

avg by (instance, device) (irate(node_disk_io_time_seconds_total[5m]) * 100)

Network receive/transmit (bytes per second): These queries sum all reported interfaces, including loopback and virtual interfaces. Traffic traversing multiple interfaces may be counted more than once; filter to the relevant interfaces when measuring external throughput.

sum by (instance) (irate(node_network_receive_bytes_total[5m]))
sum by (instance) (irate(node_network_transmit_bytes_total[5m]))

Compare these panels over the same time range. Disk IO time may rise during backups while CPU remains low, or memory usage may grow between service restarts. Correlate those patterns with workload changes and logs before assigning a cause.

Set Alert Thresholds and Pending Periods

Use alert severity and pending duration to match operational needs. For example, five-minute average CPU above 90% throughout a ten-minute pending period could be a warning; a higher sustained threshold might justify a separate critical rule. These are starting examples, not universal cutoffs. A rule also needs an evaluation interval, labels, a notification route and a plan for missing data or query errors. Brief utilization spikes and sustained pressure should not be treated identically.

The following is a conceptual checklist, not importable Grafana YAML. In a Grafana-managed rule, select the Prometheus data source and use the unfiltered numeric query as A. For a range query, add a Reduce expression using Last, then a Threshold expression above 90; make that threshold the alert condition. Set an evaluation group and a ten-minute pending period. Preview the query and condition, checking that healthy instances return a value and resolve normally. Configure No Data and Error handling deliberately, and route their separate alerts along with the CPU rule. Set the keep-firing period deliberately: a rule can remain in Recovering after its threshold is no longer breached. For the starter example, use a zero keep-firing period, then test firing and resolution with a disposable rule. The UI and provisioning formats depend on the Grafana version; this checklist follows the current official Grafana-managed rule documentation.

Conceptual design checklist — configure these in Grafana, do not import this block.
Query A: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
Reduce B: Last value of A, per instance
Condition C: B is above 90
Evaluation interval: 1 minute (example)
Pending period: 10 minutes (example)
Keep firing for: 0 minutes (starter example)
Label: severity = warning
Summary: High CPU on {{ $labels.instance }}
No Data behavior: Set No Data state
Query error behavior: Set Error state
Notifications: configure and test the intended contact point or policy

A separate critical rule can use a different threshold and duration. For memory, disk and I/O, define appropriate units and conditions instead of copying CPU thresholds unchanged. Ensure notification routing also covers DatasourceNoData and DatasourceError. A dead man’s switch is a heartbeat sent regularly to an independent receiver, which alerts when it stops arriving. An alert evaluated only by Prometheus cannot report that Prometheus itself has stopped.

Use the Grafana documentation for version-specific alert configuration, contact points, mute times and silences.

Organize the Dashboard

Group panels by host, storage and networking, with an overview that highlights the services requiring attention. Keep labels, units and time ranges consistent so responders can compare related metrics.

Add application metrics when available, such as queue depth next to CPU usage or cache hit rate next to disk IO. This helps distinguish application bottlenecks from host resource pressure. For PHP-FPM, OPcache, queues, Horizon and Redis, see the Laravel production tuning guide.

Configure Uptime Kuma for External Checks

Choose an Independent Monitoring Location

Uptime Kuma checks whether a service is reachable from its own location. Run it on a separate VPS, ideally outside the monitored service’s network or provider failure domain. DNS or firewall failures affecting that probe’s path can then be detected, though one location does not represent every user’s experience. A successful HTTP check of a login or checkout endpoint does not prove the complete login or payment workflow works. Use separate synthetic workflow tests, with a test environment for payments.

Install Docker and Start Uptime Kuma

The following example uses Docker Engine on the monitoring host. Install the supported packages for that host using the official Docker Engine installation instructions for Debian; Ubuntu users should select the Ubuntu instructions from Docker’s installation navigation. Verify the daemon is running and that sudo docker run hello-world succeeds before continuing. Docker normally starts at boot on Debian and Ubuntu; verify that configuration, or enable docker.service and containerd.service as described in Docker’s systemd startup instructions. The example uses sudo rather than assuming Docker-group membership. Before launch, restrict port 3001 using controls that apply to Docker-published ports; a host firewall rule alone may not provide the expected restriction.

sudo docker run -d \
  --name uptime-kuma \
  --restart unless-stopped \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  louislam/uptime-kuma:2

The command publishes port 3001 on all host interfaces and stores application data in the local uptime-kuma volume at /app/data. The :2 tag selects a major release line, not an immutable version; review release and backup requirements before upgrades. The unless-stopped restart policy restarts a successfully started container after an exit or daemon restart, but a manually stopped container stays stopped until you start it again. This policy does not itself enable the Docker daemon at boot. Use a trusted private connection for initial setup at http://YOUR_MONITORING_VPS:3001 and HTTPS for remote web access. Set the admin account, then add HTTP(s) checks for the website, admin panel and critical APIs. Check MySQL or Redis over a private network; do not expose them publicly for monitoring. A cron-driven keyword endpoint must also signal stale results: unchanged “healthy” text does not prove the job ran recently.

Check sudo docker ps --filter name=uptime-kuma and sudo docker logs --tail 50 uptime-kuma, then verify the web interface and monitor history. During a planned maintenance window, with console access and a recovery plan available, reboot the monitoring host and confirm that Docker and Uptime Kuma return, saved monitors remain and checks resume. Use an independent heartbeat or external check to detect failure of the monitoring host itself; Kuma cannot notify while it is down.

Use Uptime Kuma’s Push monitor when a job must check in on time. Your backup job can hit a URL after it completes. If Uptime Kuma doesn’t receive the push within the interval you set, it flags it. A missing push can reveal a job that failed before reaching its completion signal; send that signal only after successful completion.

Configure and Test Notifications

Configure a notification channel that responders monitor, and assign urgency according to the affected service. Test the contact point, then simulate failure and recovery on a disposable test monitor to confirm both messages arrive through the intended route. Repeat this check after the planned reboot test. Schedule maintenance windows so expected downtime does not trigger avoidable alerts.

Choose Monitors for a Status Page

Choose which monitors appear on a public status page and use plain descriptions such as “Europe API degraded.” Include the affected service and relevant updates without exposing internal-only monitoring details.

The Uptime Kuma repository documents current installation options, release information and supported configuration.

Verify the Monitoring Setup

Prometheus supplies metric history, Grafana displays it and evaluates alerts, and independently hosted Uptime Kuma checks availability. Together they help detect changes and provide evidence for incident investigation.

Before relying on the setup, verify exporter targets, Grafana queries, alert recovery, notification routing and monitoring-host reboot recovery. Start with CPU, memory, disk and network panels plus alerts for sustained CPU pressure, low disk space and downtime. Add IOwait, queue depth and HTTP probe metrics as the workload requires.

Monitoring also supports incident diagnosis alongside preventive controls. For a related overview of Cloudflare, ModSecurity and Fail2ban, see the guide to layered WAF and bot protection.

Frequently Asked Questions

Not necessarily. A small setup can keep Prometheus and Grafana on the monitored VPS, but monitoring then shares that VPS’s failure risk. A separate monitoring host improves failure isolation. Host Uptime Kuma outside the monitored infrastructure for an outside view.

Start with sustained high CPU usage, disk space nearing full and downtime. Choose thresholds and pending durations for your workload, test notification routing, and configure missing-data and query-error handling. An independent heartbeat receiver can also alert when the monitoring system stops checking in.

They serve different purposes. Uptime Kuma checks reachability and response time from its deployment location; one location does not represent every user. Prometheus collects metrics and Grafana displays them and evaluates alerts, helping investigate performance changes. Use both for complementary views.
↑