Prometheus collects and stores time-series metrics; Grafana turns them into dashboards and alerts. Together they are the standard way to answer "what was this server doing at 3am" without guessing. This guide installs both on a single Ubuntu or Debian VPS, wires up host metrics, secures the two web interfaces behind Nginx, and sets a retention policy that will not fill your disk.
Prerequisites
- Ubuntu 22.04/24.04 or Debian 11/12 VPS
- Root or sudo access
- At least 2 GB RAM. Prometheus is memory-hungry in proportion to the number of active series it holds; 1 GB works for one host but leaves no headroom once you add targets.
- A domain name pointed at the server if you want HTTPS on the dashboards
How the Pieces Fit Together
Three separate programs, and mixing them up is the most common source of confusion:
- node_exporter reads CPU, memory, disk and network from the kernel and publishes them as plain text on port 9100. It stores nothing.
- Prometheus scrapes that endpoint on a timer and stores the results in its own database on port 9090.
- Grafana queries Prometheus and draws the graphs on port 3000. It stores no metrics of its own.
Metrics flow one way: node_exporter → Prometheus → Grafana. Prometheus pulls; nothing is pushed to it.
Step 1 — Create Service Users
Neither daemon should run as root, and neither needs a login shell or a home directory:
sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus
sudo useradd --no-create-home --shell /usr/sbin/nologin node_exporter
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /var/lib/prometheus
Step 2 — Install node_exporter
Check the current release version on the Prometheus download page, then adjust the version below:
cd /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
sudo cp node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter
Use linux-arm64 instead if uname -m reports aarch64.
sudo nano /etc/systemd/system/node_exporter.service
[Unit]
Description=Prometheus Node Exporter
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=always
[Install]
WantedBy=multi-user.target
Binding to 127.0.0.1 rather than all interfaces matters: node_exporter has no authentication of any kind, and on a public address it hands your entire system profile to anyone who asks. Keep it on loopback while Prometheus runs on the same host.
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
curl -s localhost:9100/metrics | head
Step 3 — Install Prometheus
cd /tmp
wget https://github.com/prometheus/prometheus/releases/download/v2.53.0/prometheus-2.53.0.linux-amd64.tar.gz
tar xzf prometheus-2.53.0.linux-amd64.tar.gz
cd prometheus-2.53.0.linux-amd64
sudo cp prometheus promtool /usr/local/bin/
sudo cp -r consoles console_libraries /etc/prometheus/
sudo chown -R prometheus:prometheus /etc/prometheus /usr/local/bin/prometheus
Step 4 — Configure Scraping
sudo nano /etc/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['127.0.0.1:9090']
- job_name: 'node'
static_configs:
- targets: ['127.0.0.1:9100']
labels:
instance: 'web-01'
Set instance to something you will recognise. Once you monitor a second server, "127.0.0.1:9100" on every dashboard is useless.
Validate before restarting — Prometheus refuses to start on a bad config, and a typo at 2am is a silent outage of your monitoring:
promtool check config /etc/prometheus/prometheus.yml
Step 5 — Run Prometheus with a Retention Limit
sudo nano /etc/systemd/system/prometheus.service
[Unit]
Description=Prometheus
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=30d \
--storage.tsdb.retention.size=5GB \
--web.listen-address=127.0.0.1:9090
Restart=always
[Install]
WantedBy=multi-user.target
Both retention flags are here on purpose. Time-based retention alone will still fill a small disk if you add targets, and a full disk stops Prometheus writing and takes your monitoring down at exactly the moment you need it. Whichever limit is reached first wins.
sudo systemctl daemon-reload
sudo systemctl enable --now prometheus
sudo systemctl status prometheus
Step 6 — Install Grafana
sudo apt install -y apt-transport-https software-properties-common
sudo mkdir -p /etc/apt/keyrings
wget -q -O - https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt update
sudo apt install -y grafana
sudo systemctl enable --now grafana-server
Grafana listens on port 3000 and ships with the login admin/admin. Change it on first sign-in, before the instance is reachable from outside.
Step 7 — Put Both Behind Nginx and Close the Ports
Prometheus has no login screen at all. Anyone who can reach port 9090 can read every metric and query the whole database, so it should never be open to the internet. Reach it through a reverse proxy with authentication, or over an SSH tunnel.
sudo apt install -y nginx apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd monitor
server {
listen 80;
server_name monitor.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /prometheus/ {
auth_basic "Metrics";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://127.0.0.1:9090/;
}
}
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d monitor.example.com
If you would rather expose nothing at all, skip the proxy and tunnel instead:
ssh -L 3000:127.0.0.1:3000 -L 9090:127.0.0.1:9090 user@your-server
Step 8 — Connect Grafana to Prometheus
In Grafana, go to Connections → Data sources → Add data source → Prometheus, and set the URL to http://127.0.0.1:9090. Save & test should report success. Then Dashboards → New → Import and enter dashboard ID 1860 (Node Exporter Full), selecting your Prometheus data source when prompted. That gives you CPU, memory, disk, filesystem and network panels without building anything by hand.
Queries Worth Knowing
Grafana dashboards are PromQL underneath. These four cover most of what you will actually be asked:
# CPU used, as a percentage
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
# Memory used, as a percentage
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# Root filesystem free, as a percentage
100 * node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}
# Is the target up? 1 means yes
up{job="node"}
rate() is required on counters such as node_cpu_seconds_total, which only ever increase. Graphing the raw counter draws a line going up and to the right forever and tells you nothing.
Adding a Second Server
On the new host, install node_exporter as above, but bind it to the private network address rather than loopback so the Prometheus server can reach it, and allow only that source:
sudo ufw allow from 10.0.0.5 to any port 9100 proto tcp
Then add it to prometheus.yml under the same job and reload:
sudo systemctl reload prometheus
Reload re-reads the config without dropping the stored data. A restart is not needed for a config change.
Common Errors
Grafana shows "No data" but the data source tests fine — check the time range in the top-right corner first; a range that predates the install is empty by definition. Then confirm the target is actually being scraped at Status → Targets in the Prometheus UI.
Target shows DOWN with "connection refused" — node_exporter is not running, or it is bound to 127.0.0.1 on a different machine than Prometheus. curl the exporter's /metrics endpoint from the Prometheus host to confirm which.
Prometheus will not start after a config edit — run promtool check config, then journalctl -u prometheus -n 50. YAML indentation is nearly always the cause.
Disk filling up — both retention flags must be present, and they only apply to new data after a restart. Check current usage with du -sh /var/lib/prometheus/.
Grafana behind a proxy shows a blank page or broken assets — set root_url in /etc/grafana/grafana.ini to the public URL and restart grafana-server.
Best Practices
- Keep Prometheus and node_exporter on loopback and reach them through a proxy or SSH tunnel — neither has authentication of its own.
- Set both retention flags on day one, not after the disk fills.
- Give every target a meaningful
instancelabel before you have more than one. - Use
systemctl reloadfor config changes and keep the stored history. - Alert on
up == 0as well as on thresholds — a target that stopped reporting looks identical to a healthy one on a graph.
FAQ
How much disk will this use?
Roughly 1—2 GB per month for a single host at a 15-second scrape interval with the default node_exporter collectors. It scales with the number of series, so a second server roughly doubles it.
Can I run this on a 1 GB VPS?
For one monitored host, yes. Grafana and Prometheus together idle at around 300—400 MB, which leaves little room for anything else on the box — and none for growth.
Do I need Alertmanager?
Not to start. Grafana can send alerts on its own, which is enough for a handful of servers. Alertmanager earns its place when you need routing, grouping and silences.
Continue Reading
- How to Check VPS Resource Usage (CPU, RAM & Disk)
- How to Manage Services with systemd and systemctl
- How to Configure Nginx as a Reverse Proxy
Browse more articles in Performance & Monitoring.