In this tutorial, you will install Prometheus, Node Exporter, and Grafana on Ubuntu 24.04, keep all three web interfaces bound to localhost, connect Grafana to Prometheus, and verify live CPU, memory, disk, and network metrics through an SSH tunnel.
Prometheus is a time-series monitoring system that scrapes and stores metrics, Node Exporter exposes Linux host metrics, and Grafana turns those metrics into dashboards. Raff Technologies supports 3,000+ customers and 15,000+ VMs, and a Raff Linux VM provides the persistent Ubuntu environment, NVMe storage, firewall controls, and 3 Gbps unmetered VM traffic needed for a self-hosted monitoring stack.
The original workflow was tested on a Raff Ubuntu 24.04 VM with 2 vCPU, 4 GB RAM, and 80 GB NVMe storage. This revision pins Prometheus 3.5.5 LTS and Node Exporter 1.12.1, the current LTS and exporter releases used for this guide as of August 14, 2026. Grafana is installed from its official stable APT repository so normal package updates can deliver supported stable releases.
Security model: Prometheus explicitly recommends against exposing its monitoring endpoints directly to the public internet. This tutorial therefore binds Prometheus to
127.0.0.1:9090, Node Exporter to127.0.0.1:9100, and Grafana to127.0.0.1:3000. You reach Grafana through an SSH tunnel instead of opening monitoring ports publicly.
Prerequisites:
- A Raff Ubuntu 24.04 VM with 2 vCPU and 4 GB RAM for this tutorial baseline
- SSH access with a non-root sudo user
- Working SSH key authentication recommended
- Enough free disk space for Prometheus time-series data and Grafana configuration
Step 1 — Prepare Ubuntu 24.04
Update packages and install the utilities used throughout the tutorial:
sudo apt update sudo apt upgrade -y sudo apt install -y curl wget tar gnupg ca-certificates ufw jq
Verify the operating system and available resources:
lsb_release -ds nproc free -h df -h /
Expected baseline includes Ubuntu 24.04 LTS, at least 2 vCPU, and about 4 GB RAM for the reference setup.
If /var/run/reboot-required exists after the upgrade, reboot during a safe window before continuing.
Verification is complete when Ubuntu 24.04 is current and the VM has enough CPU, memory, and disk space for the monitoring workload.
Step 2 — Create dedicated Prometheus and Node Exporter users
Create non-login system accounts:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin prometheus 2>/dev/null || true sudo useradd --system --no-create-home --shell /usr/sbin/nologin node_exporter 2>/dev/null || true
Create Prometheus configuration and data directories:
sudo install -d -o prometheus -g prometheus /etc/prometheus sudo install -d -o prometheus -g prometheus /var/lib/prometheus
Verify the accounts and directories:
getent passwd prometheus getent passwd node_exporter ls -ld /etc/prometheus /var/lib/prometheus
Verification is complete when both service accounts exist and the Prometheus directories are owned by prometheus.
Step 3 — Install Prometheus 3.5.5 LTS and verify its checksum
Prometheus currently publishes 3.5.5 in its LTS line. Download the Linux AMD64 archive:
cd /tmp PROMETHEUS_VERSION="3.5.5" curl -fLO "https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz"
Verify the SHA-256 checksum published on the official Prometheus download page:
echo "64d0beab873272b861a91df41668bc852c7e2e5b23f75c16059fb15b5630c577 prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz" | sha256sum -c -
Expected output:
prometheus-3.5.5.linux-amd64.tar.gz: OK
Extract and install the binaries plus console assets:
tar -xzf "prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz" cd "prometheus-${PROMETHEUS_VERSION}.linux-amd64" sudo install -m 0755 prometheus promtool /usr/local/bin/ sudo cp -a consoles console_libraries /etc/prometheus/ sudo chown -R prometheus:prometheus /etc/prometheus/consoles /etc/prometheus/console_libraries
Verify the installed version:
prometheus --version | head -n 1 promtool --version | head -n 1
The version output should report 3.5.5.
Verification is complete when the checksum passes and both Prometheus binaries report version 3.5.5.
Step 4 — Configure Prometheus to scrape itself and Node Exporter
Create /etc/prometheus/prometheus.yml:
sudo tee /etc/prometheus/prometheus.yml > /dev/null <<'EOF' global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: "prometheus" static_configs: - targets: ["127.0.0.1:9090"] - job_name: "node_exporter" static_configs: - targets: ["127.0.0.1:9100"] EOF
Set ownership and validate the YAML:
sudo chown prometheus:prometheus /etc/prometheus/prometheus.yml promtool check config /etc/prometheus/prometheus.yml
Expected output includes a successful configuration check.
Prometheus' current documentation uses the same scrape model: a YAML file defines jobs and targets, while scrape_interval controls collection frequency.
Verification is complete when promtool check config succeeds and both localhost targets appear in the configuration.
Step 5 — Run Prometheus under systemd on localhost only
Create the systemd unit:
sudo tee /etc/systemd/system/prometheus.service > /dev/null <<'EOF' [Unit] Description=Prometheus Monitoring 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 \ --web.listen-address=127.0.0.1:9090 \ --web.console.templates=/etc/prometheus/consoles \ --web.console.libraries=/etc/prometheus/console_libraries Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target EOF
Start and enable Prometheus:
sudo systemctl daemon-reload sudo systemctl enable --now prometheus
Verify the service, readiness endpoint, and listening address:
systemctl is-active prometheus curl -fsS http://127.0.0.1:9090/-/ready ss -lntp | grep ':9090'
Expected state includes active, a successful readiness response, and 127.0.0.1:9090 rather than 0.0.0.0:9090.
Prometheus defaults to listening on all interfaces unless --web.listen-address is set, so the explicit loopback bind is intentional.
Verification is complete when Prometheus is active, ready, and accessible only on localhost port 9090.
Step 6 — Install Node Exporter 1.12.1 and verify its checksum
Download the current Node Exporter release:
cd /tmp NODE_EXPORTER_VERSION="1.12.1" curl -fLO "https://github.com/prometheus/node_exporter/releases/download/v${NODE_EXPORTER_VERSION}/node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64.tar.gz"
Verify the official SHA-256 checksum:
echo "b51d8a76aa2a9156a55d501aca6276fae09e262259a5e4e831d2c2222f084e63 node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64.tar.gz" | sha256sum -c -
Expected output:
node_exporter-1.12.1.linux-amd64.tar.gz: OK
Extract and install the binary:
tar -xzf "node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64.tar.gz" sudo install -m 0755 "node_exporter-${NODE_EXPORTER_VERSION}.linux-amd64/node_exporter" /usr/local/bin/node_exporter
Verify the version:
node_exporter --version | head -n 1
The output should report 1.12.1.
Verification is complete when the checksum passes and Node Exporter reports version 1.12.1.
Step 7 — Run Node Exporter under systemd on localhost only
Create the Node Exporter unit:
sudo tee /etc/systemd/system/node_exporter.service > /dev/null <<'EOF' [Unit] Description=Prometheus 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 --web.listen-address=127.0.0.1:9100 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target EOF
Start and enable the service:
sudo systemctl daemon-reload sudo systemctl enable --now node_exporter
Verify metrics and the bind address:
systemctl is-active node_exporter curl -fsS http://127.0.0.1:9100/metrics | head ss -lntp | grep ':9100'
Expected state includes active, Prometheus-format metric lines, and 127.0.0.1:9100.
Verification is complete when Node Exporter is active and its metrics endpoint is available only on localhost.
Step 8 — Verify Prometheus is scraping both targets
Wait at least one scrape interval, then query the target API:
sleep 20 curl -fsS http://127.0.0.1:9090/api/v1/targets | \ jq -r '.data.activeTargets[] | [.labels.job, .health] | @tsv'
Expected result:
prometheus up node_exporter up
Verify actual host metrics are stored:
curl -fsSG http://127.0.0.1:9090/api/v1/query \ --data-urlencode 'query=node_uname_info' | jq -r '.status'
Expected output:
success
Verification is complete when both scrape targets are up and Prometheus can query Node Exporter metrics.
Step 9 — Install Grafana from the official stable APT repository
Grafana's current Ubuntu documentation recommends its signed APT repository for installations that should receive package updates.
Add the repository key and stable source:
sudo mkdir -p /etc/apt/keyrings sudo wget -q -O /etc/apt/keyrings/grafana.asc https://apt.grafana.com/gpg-full.key sudo chmod 644 /etc/apt/keyrings/grafana.asc echo "deb [signed-by=/etc/apt/keyrings/grafana.asc] https://apt.grafana.com stable main" | \ sudo tee /etc/apt/sources.list.d/grafana.list
Install Grafana OSS:
sudo apt update sudo apt install -y grafana
Bind Grafana to loopback instead of all network interfaces:
sudo sed -i 's/^;\?http_addr =.*/http_addr = 127.0.0.1/' /etc/grafana/grafana.ini
Start and enable Grafana:
sudo systemctl enable --now grafana-server
Verify the service and bind address:
systemctl is-active grafana-server curl -I http://127.0.0.1:3000/login ss -lntp | grep ':3000'
Expected state includes an HTTP response and 127.0.0.1:3000.
Grafana documents http_addr as the setting that controls which network interface the server binds to; an empty value binds to all interfaces.
Verification is complete when Grafana is active and listening only on localhost port 3000.
Step 10 — Keep monitoring ports closed and open an SSH tunnel to Grafana
Allow SSH before enabling UFW:
sudo ufw allow OpenSSH sudo ufw --force enable sudo ufw status numbered
Do not add public allow rules for ports 3000, 9090, or 9100.
From your local computer, create an SSH tunnel to Grafana:
ssh -L 3000:127.0.0.1:3000 your-user@your-server-ip
Keep that SSH session open and browse to:
http://127.0.0.1:3000
Grafana's default first-login credentials are:
Username: admin Password: admin
Change the administrator password when Grafana prompts you.

Verify from the VM that the monitoring ports are loopback-only:
ss -lntp | grep -E ':3000|:9090|:9100'
All three listeners should show 127.0.0.1.
Verification is complete when Grafana opens through the SSH tunnel and none of the monitoring ports is publicly bound.
Step 11 — Connect Grafana to Prometheus
In Grafana, open:
Connections → Data sources → Add data source → Prometheus
Set the Prometheus server URL to:
http://127.0.0.1:9090
Click Save & test.
Expected visible result indicates Grafana successfully queried the Prometheus API.

Verify Prometheus directly from the VM:
curl -fsSG http://127.0.0.1:9090/api/v1/query \ --data-urlencode 'query=up' | jq -r '.status'
Expected output:
success
Verification is complete when Grafana accepts the Prometheus data source and the local Prometheus API query succeeds.
Step 12 — Import a Node Exporter dashboard
In Grafana, open:
Dashboards → New → Import
For the existing Raff-tested path, use Grafana.com dashboard ID:
1860
Select the Prometheus data source and import the dashboard.

If a third-party dashboard changes or becomes unavailable, you can still verify the stack by creating a simple panel using a PromQL query such as:
rate(node_cpu_seconds_total{mode!="idle"}[5m])
Verification is complete when Grafana imports the dashboard or a manually created panel returns Node Exporter data from Prometheus.
Step 13 — Verify Prometheus and Grafana monitoring end to end
Open the Node Exporter dashboard through the SSH tunnel, set the time range to Last 15 minutes, and refresh it.

Run the final server checks:
systemctl is-active prometheus systemctl is-active node_exporter systemctl is-active grafana-server curl -fsS http://127.0.0.1:9090/-/ready curl -fsS http://127.0.0.1:9100/metrics >/dev/null curl -fsS http://127.0.0.1:3000/api/health | jq curl -fsS http://127.0.0.1:9090/api/v1/targets | \ jq -r '.data.activeTargets[] | [.labels.job, .health] | @tsv' ss -lntp | grep -E ':3000|:9090|:9100'
End-to-end verification is complete when:
- Prometheus, Node Exporter, and Grafana are active
- Prometheus and Node Exporter targets both report
up - Grafana's health endpoint responds
- The dashboard displays live host metrics
- Ports
3000,9090, and9100are bound only to127.0.0.1 - Grafana is reachable from your workstation through the SSH tunnel
