Caption: A private Grafana UI sits in front of Prometheus, which scrapes node_exporter so host metrics stay on your VPS.
Introduction
Grafana is the open-source dashboard and alerting layer most operators reach for when they want clear views of CPU, memory, disk, and application health. On its own, Grafana does not collect metrics — it queries data sources. Pairing it with Prometheus (scrape and store time series) and node_exporter (expose Linux host metrics) gives you a complete, self-hosted monitoring path without sending telemetry to a SaaS vendor.
Self-hosting this stack makes sense for homelabs, agencies, and small teams that already run Docker on a VPS. You keep metrics close to the workloads you care about, control who can open dashboards, version-control provisioning files, and rehearse backups before the board becomes mission-critical. The stack stays modest: three containers, a reverse proxy for HTTPS, and a few YAML files under /opt/monitoring.
This guide installs Grafana Open Source, Prometheus, and node_exporter on Ubuntu 24.04 LTS with Docker Compose. You will bind Grafana and Prometheus to localhost, terminate TLS on Nginx, provision a Prometheus data source, import a Node Exporter dashboard workflow, set a basic alert contact point, and finish with troubleshooting, backups, and upgrades. Replace every example.com hostname before production use.
Why Choose Grafana?
- Dashboards operators actually use: Combine time series, gauges, tables, and variables into views tailored to your hosts and apps.
- Prometheus as the metrics brain: Pull-based scrapes, labels, and PromQL give you a durable foundation before you expand to logs or traces.
- node_exporter for host truth: CPU, memory, filesystem, and network metrics from the Linux host without inventing custom collectors on day one.
- File-based provisioning: Data sources and dashboard providers can live in Git so rebuilds are repeatable.
- Built-in alerting: Alert rules, contact points, and notification policies ship with Grafana OSS — start simple and grow.
- Docker-friendly footprint: One Compose project, named volumes, and a reverse proxy fit a modest VPS.
- Privacy by default: Metrics stay on your network; expose only HTTPS for the UI, not scrape ports.
Grafana visualizes; Prometheus stores; exporters produce. Start with one clean scrape target and a handful of panels before adding cAdvisor, blackbox probes, or remote write.
Prerequisites
Hardware Recommendations:
- Comfortable lab: 2 vCPU and 2 GB RAM for Grafana + Prometheus + node_exporter on a lightly loaded host
- Busier VPS with longer retention: 2–4 vCPU and 4 GB RAM
- 20 GB+ SSD free for images, the Prometheus TSDB, Grafana data, and local backup staging
- Off-server storage for Compose files,
.env, and volume archives - Optional second host later if you want remote scrape targets
Software and Accounts:
- Ubuntu 24.04 LTS with sudo access
- Hostname such as
grafana.example.comwith DNSA/AAAApointing at the server - Docker Engine with the Compose v2 plugin (Docker’s apt repository; avoid snap Docker)
- Nginx and Certbot for HTTPS
curl,jq, andopensslfor health checks and secrets- A password manager for the Grafana admin password
Security Notes:
- Bind Grafana (
3000) and Prometheus (9090) to127.0.0.1; never publish scrape ports to the public internet - Keep node_exporter on the Compose network (or host network only as documented) without opening
9100in UFW - Disable public sign-up; store admin credentials offline
- Treat
.env, provisioning YAML, and backup tarballs as sensitive - Prefer pinned image tags in production once you verify a known-good release
Patch the host and open only SSH plus web ports:
sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg jq openssl ufw nginx certbot python3-certbot-nginx
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status
Installation Guide
This deployment keeps files under /opt/monitoring. Docker Compose runs Grafana, Prometheus, and node_exporter on a private network. Nginx terminates HTTPS for Grafana only.
1. Install Docker Engine
sudo apt-get update -qqy
sudo apt-get install ca-certificates curl -qqy
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -qqy
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -qqy
sudo usermod -aG docker "$USER"
newgrp docker
docker --version
docker compose version
docker run --rm hello-world
If the Docker group is inactive in your shell, log out and back in before continuing.
2. Create the Project Directory
sudo mkdir -p /opt/monitoring/{prometheus,grafana/provisioning/datasources,grafana/provisioning/dashboards,grafana/dashboards,backups}
sudo chown -R "$USER":"$USER" /opt/monitoring
chmod 700 /opt/monitoring
chmod 700 /opt/monitoring/backups
cd /opt/monitoring
Keep this tree private — .env holds the admin password and may later hold webhook URLs.
3. Generate Secrets and Create .env
cd /opt/monitoring
GRAFANA_ADMIN_PASSWORD_VALUE="$(openssl rand -base64 36 | tr -d '\n')"
cat > .env <<EOF
GF_SERVER_DOMAIN=grafana.example.com
GF_SERVER_ROOT_URL=https://grafana.example.com/
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD_VALUE}
GF_USERS_ALLOW_SIGN_UP=false
GF_AUTH_ANONYMOUS_ENABLED=false
GF_ANALYTICS_REPORTING_ENABLED=false
GF_LOG_LEVEL=info
EOF
chmod 600 .env
printf 'Initial Grafana admin password: %s\n' "${GRAFANA_ADMIN_PASSWORD_VALUE}"
Save the password in your password manager before you continue.
4. Write Prometheus Configuration
Create /opt/monitoring/prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["prometheus:9090"]
- job_name: node
static_configs:
- targets: ["node-exporter:9100"]
This scrapes Prometheus itself and the Compose service named node-exporter. Adjust job names and labels when you add more hosts later.
5. Provision the Grafana Prometheus Data Source
Create /opt/monitoring/grafana/provisioning/datasources/prometheus.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
Create /opt/monitoring/grafana/provisioning/dashboards/default.yml:
apiVersion: 1
providers:
- name: Local dashboards
orgId: 1
folder: Operations
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
You can drop exported dashboard JSON files into /opt/monitoring/grafana/dashboards/ after first login; Grafana will pick them up on the provider interval.
6. Create compose.yaml
services:
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
env_file: ./.env
ports:
- "127.0.0.1:3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
depends_on:
- prometheus
networks:
- monitoring
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 5
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=15d"
- "--web.enable-lifecycle"
ports:
- "127.0.0.1:9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
networks:
- monitoring
node-exporter:
image: quay.io/prometheus/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
command:
- "--path.rootfs=/host"
pid: host
volumes:
- "/:/host:ro,rslave"
networks:
- monitoring
# No published ports — Prometheus scrapes over the Compose network only
networks:
monitoring:
name: monitoring
volumes:
grafana_data:
name: grafana_data
prometheus_data:
name: prometheus_data
Official Grafana docs also document grafana/grafana-enterprise (free to run with optional paid features). This guide uses grafana/grafana for a clear OSS image. The node_exporter flags follow the Prometheus project’s container guidance: mount the host root and set --path.rootfs=/host so metrics reflect the host, not only the container.
Caption: Nginx publishes HTTPS for Grafana while Prometheus scrapes node_exporter on a private Docker network; scrape ports stay on localhost or internal DNS.
7. Start the Stack
cd /opt/monitoring
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=2m
Health checks:
curl -s http://127.0.0.1:3000/api/health | jq
curl -s http://127.0.0.1:9090/-/ready
curl -s http://127.0.0.1:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'
Expect Grafana health JSON, Prometheus ready, and both scrape jobs reporting up. If a target is down, confirm service names match prometheus.yml and that containers share the monitoring network.
8. Configure Nginx and HTTPS
Create /etc/nginx/sites-available/grafana.example.com:
server {
listen 80;
listen [::]:80;
server_name grafana.example.com;
location / {
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:3000;
}
}
Enable the site and request a certificate:
sudo ln -s /etc/nginx/sites-available/grafana.example.com /etc/nginx/sites-enabled/grafana.example.com
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d grafana.example.com
sudo systemctl reload nginx
Open https://grafana.example.com and sign in with the generated admin credentials. Leave Prometheus on localhost (or tunnel via SSH) unless you deliberately proxy it behind authentication.
Configuration
Key Grafana environment settings in this stack:
GF_SERVER_ROOT_URL— public HTTPS URL for links and redirectsGF_SECURITY_ADMIN_USER/GF_SECURITY_ADMIN_PASSWORD— initial admin accountGF_USERS_ALLOW_SIGN_UP=false— no anonymous account creationGF_AUTH_ANONYMOUS_ENABLED=false— dashboards stay private- Provisioning mounts under
/etc/grafana/provisioning - Persistent Grafana state in the
grafana_datavolume
Prometheus retention is set to 15d via --storage.tsdb.retention.time. Raise it only after you confirm disk headroom. Reloading config without a full restart:
curl -X POST http://127.0.0.1:9090/-/reload
Requires --web.enable-lifecycle as shown in the Compose file. After editing prometheus.yml, always validate syntax before reload:
docker compose exec prometheus promtool check config /etc/prometheus/prometheus.yml
Caption: Prometheus pulls metrics on a schedule from node_exporter; Grafana queries Prometheus over the same private network.
Usage
First Login
- Open
https://grafana.example.com - Sign in as
adminwith the password from.env - Change the admin password when prompted
- Confirm Connections → Data sources lists Prometheus (provisioned) and that Save & test succeeds
- In Prometheus UI (
http://127.0.0.1:9090via SSH tunnel), open Status → Targets and verifyprometheusandnodeareUP
Build a First Host Dashboard
In Grafana:
- Dashboards → New → New dashboard → Add visualization
- Query Prometheus, for example:
- CPU busy ratio:
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) - Memory available:
node_memory_MemAvailable_bytes - Root filesystem free:
node_filesystem_avail_bytes{mountpoint="/"}
- CPU busy ratio:
- Set units (percent, bytes) and save the dashboard under the Operations folder
- Optionally import a community Node Exporter full dashboard from grafana.com by ID after reviewing the JSON — pin the data source to your provisioned Prometheus
Alerting Smoke Test
- Alerting → Contact points → New — add email or a webhook you control
- Alerting → Alert rules → New alert rule — for example, fire when
node_filesystem_avail_bytes{mountpoint="/"}stays below a threshold for 5 minutes - Attach a notification policy that routes to your contact point
- Use Test on the contact point, then confirm a firing rule appears in Alert rules
Do not point production alerts at a personal inbox you ignore. Use a channel your on-call path actually reads.
Testing Checklist
docker compose psshowsgrafana,prometheus, andnode-exporterhealthyhttps://grafana.example.comloads over a valid certificate- Provisioned Prometheus data source tests green
- Both Prometheus targets are
UP - At least one dashboard panel shows live host metrics
- Contact point test notification arrives
- Reboot the VPS and confirm the stack returns with
restart: unless-stopped
Screenshots and Visuals
The visuals in this guide are original architecture diagrams — not scraped product UI. They show the HTTPS edge, Compose topology, scrape path, and backup/alert maintenance flow so you can reason about the stack before it becomes critical path.
Caption: Day-two ops: back up Grafana and Prometheus volumes, keep Compose and prometheus.yml in Git, and verify alert contact points after upgrades.
Troubleshooting
- Grafana 502 from Nginx: Container still starting or not bound to
127.0.0.1:3000— checkdocker compose psandss -tlnp | grep 3000 - Data source test fails: Wrong URL (use
http://prometheus:9090from inside the Compose network, notlocalhost), or Prometheus not on themonitoringnetwork - node job DOWN: Service name mismatch (
node-exportervs config), exporter crash, or missing host mounts — inspectdocker compose logs node-exporter - Empty panels / no data: Wrong metric names, time range too short after first start, or scrape interval not elapsed — verify targets and try a raw PromQL in Explore
- Permission errors on Grafana volume: Avoid arbitrary host UID remaps unless you intentionally follow Grafana bind-mount docs; named volumes are simpler on Ubuntu
- Prometheus disk growth: Lower
--storage.tsdb.retention.time, drop high-cardinality jobs, and monitor volume size under/var/lib/docker - Forgot admin password: Prefer Grafana CLI reset inside the container after a volume backup — do not delete
grafana_dataas a shortcut - Certbot / Nginx failures: Confirm DNS points at the server and port
80is reachable for HTTP-01 challenges - node_exporter shows container-ish metrics: Confirm
--path.rootfs=/hostand the/:/host:ro,rslavemount match official container guidance
Scaling, Securing, and Next Steps
Back up both volumes plus config before upgrades:
cd /opt/monitoring
docker compose stop
sudo mkdir -p /var/backups/monitoring
docker run --rm \
-v grafana_data:/data:ro \
-v /var/backups/monitoring:/backup \
alpine:3.20 \
sh -c 'cd /data && tar czf /backup/grafana-data.tgz .'
docker run --rm \
-v prometheus_data:/data:ro \
-v /var/backups/monitoring:/backup \
alpine:3.20 \
sh -c 'cd /data && tar czf /backup/prometheus-data.tgz .'
cp compose.yaml .env prometheus/prometheus.yml /var/backups/monitoring/
rsync -a /var/backups/monitoring/ backup-user@backup.example.net:/srv/backups/monitoring/
docker compose start
Schedule that flow with cron or a systemd timer. Rehearse restore on a scratch VPS: recreate volumes from the archives, restore Compose and config, docker compose up -d, confirm data sources and a sample dashboard. Treat backup archives as sensitive.
For upgrades, read Grafana and Prometheus release notes, take a fresh backup, pin image tags when ready, then:
cd /opt/monitoring
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=2m
Harden next: put Grafana behind SSO or VPN if required, add scrape targets for other hosts (or a federation path), consider cAdvisor for container metrics, keep 9090 and 9100 off the public firewall, and store provisioning YAML in Git so the monitoring plane is rebuildable.
The outcome of this guide is a private Grafana + Prometheus + node_exporter stack on Ubuntu 24.04 with HTTPS for the UI, provisioned data sources, host metrics on dashboards, a rehearsable backup path, and a starter alert contact point. From here, pin image versions, expand scrape jobs deliberately, and document who owns on-call for firing alerts.
Need this done on your server?
I deploy and harden Laravel, CodeCanyon, and open-source apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan