Why Monitor Your Own Infrastructure?
Here is a truth that hits every homelab enthusiast eventually: you have got services running on three machines, a NAS in the closet, and a Raspberry Pi doing something. But when something breaks at 2 AM, you find out from a user (or a failed cron job log) instead of a nice alert.
That is where Prometheus and Grafana come in. Prometheus collects metrics — CPU, RAM, disk, network, application-specific numbers — and Grafana turns those metrics into beautiful, actionable dashboards. Together, they are the gold standard for open-source monitoring, and they are surprisingly easy to set up with Docker Compose.
By the end of this guide, you will have a full monitoring stack running that scrapes metrics from your Docker hosts and any other machines on your network, with dashboards that would make a DevOps engineer jealous.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended) with Docker and Docker Compose installed
- At least 2 GB of RAM available (Prometheus + Grafana together use ~1 GB idle)
- Port 3000 (Grafana) and 9090 (Prometheus) accessible on the server
- Basic comfort with the command line and editing YAML files
Do not have Docker installed yet? Follow the official install guide for your distro first, then come back.
Step 1: Create the Project Directory
Let us keep everything tidy in one directory:
mkdir -p ~/monitoring-stack && cd ~/monitoring-stack
mkdir -p prometheus grafana/provisioning/datasources grafana/provisioning/dashboards
This gives us separate folders for Prometheus config, Grafana data sources, and dashboards. Keeping it organized now saves headaches later.
Step 2: Write the Docker Compose File
Create docker-compose.yml in the ~/monitoring-stack directory:
version: "3.8"
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle"
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=changeme
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
ports:
- "3000:3000"
depends_on:
- prometheus
networks:
- monitoring
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
command:
- "--path.procfs=/host/proc"
- "--path.rootfs=/rootfs"
- "--path.sysfs=/host/sys"
- "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($|/)"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
ports:
- "9100:9100"
networks:
- monitoring
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
ports:
- "8080:8080"
devices:
- /dev/kmsg
networks:
- monitoring
volumes:
prometheus_data:
grafana_data:
networks:
monitoring:
driver: bridge
Let us break down what each service does:
- Prometheus — The metrics database. It scrapes data from exporters every 15 seconds (by default) and stores it. The
--web.enable-lifecycleflag lets you reload config without restarting. - Grafana — The visualization layer. It queries Prometheus and renders dashboards. We pre-configure the admin user and disable public sign-ups.
- Node Exporter — Exposes hardware and OS metrics (CPU, memory, disk I/O, network) from the host machine.
- cAdvisor — Monitors Docker containers specifically: CPU, memory, network, and filesystem usage per container.
Step 3: Configure Prometheus
Create prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "cadvisor"
static_configs:
- targets: ["cadvisor:8080"]
Notice we are using Docker service names (node-exporter, cadvisor) as targets instead of IP addresses. Because they are all on the same Docker network, DNS resolution just works. This is one of the nice things about Compose — service discovery is built in.
Step 4: Pre-Configure Grafana Data Source
Instead of manually adding Prometheus as a data source in Grafana UI, let us automate it. Create grafana/provisioning/datasources/prometheus.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
This tells Grafana: “On startup, connect to Prometheus and make it the default data source.” No clicking required.
Step 5: Launch the Stack
From the ~/monitoring-stack directory:
docker compose up -d
Give it about 30 seconds for everything to initialize, then verify all containers are running:
docker compose ps
You should see four containers: prometheus, grafana, node-exporter, and cadvisor — all with status Up.
Step 6: Access Your Dashboards
Open your browser and navigate to:
- Grafana:
http://YOUR_SERVER_IP:3000— Log in withadmin/changeme - Prometheus:
http://YOUR_SERVER_IP:9090— No auth by default (we will fix that later)
Once you are in Grafana, head to Dashboards → Browse → Import. There are two excellent community dashboards to import:
- Node Exporter Full (Dashboard ID:
1860) — Comprehensive host-level metrics: CPU, memory, disk, network, temperatures, and more. - Docker and Host Monitoring (Dashboard ID:
10566) — Container-level stats from cAdvisor alongside host metrics.
Just paste the dashboard ID, click Load, select Prometheus as the data source, and click Import. Boom — instant visibility into your infrastructure.
Step 7: Add More Machines (Optional but Powerful)
Here is where it gets really good. You can monitor any machine on your network by installing Node Exporter on it and adding it to Prometheus scrape config.
On each remote machine, install Node Exporter:
# Download and install Node Exporter
wget https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz
tar xvfz node_exporter-1.7.0.linux-amd64.tar.gz
sudo cp node_exporter-1.7.0.linux-amd64/node_exporter /usr/local/bin/
# Create a systemd service
sudo tee /etc/systemd/system/node_exporter.service > /dev/null << 'SVCEOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/node_exporter
Restart=always
[Install]
WantedBy=multi-user.target
SVCEOF
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
Then add the new target to prometheus/prometheus.yml:
- job_name: "remote-servers"
static_configs:
- targets:
- "192.168.1.50:9100"
- "192.168.1.51:9100"
- "192.168.1.52:9100"
Reload Prometheus without restarting:
curl -X POST http://localhost:9090/-/reload
That is it. All your machines, one dashboard.
Common Pitfalls and Troubleshooting
“Connection refused” in Grafana: Make sure the data source URL uses http://prometheus:9090 (the Docker service name), not localhost. Grafana runs in its own container, so localhost refers to the Grafana container, not the host.
Permission denied on /dev/kmsg (cAdvisor): On some systems, cAdvisor needs privileged access. If it will not start, add privileged: true to the cadvisor service in docker-compose.yml.
Prometheus using too much disk: The default retention is 15 days. We set it to 30 days in our config. If you are tight on space, reduce it: --storage.tsdb.retention.time=7d.
Metrics look stale or missing: Check Prometheus Targets page (http://YOUR_IP:9090/targets). Green = healthy, red = problem. Click on a target to see the error.
Forgot the Grafana password: Reset it from the command line:
docker exec -it grafana grafana-cli admin reset-admin-password newpassword
Securing Your Stack
This setup is great for a homelab, but if you are exposing it beyond your local network, consider these hardening steps:
- Change the default Grafana password immediately. Seriously.
changemeis not a password. - Add basic auth or OAuth to Grafana (Settings → Authentication).
- Put Prometheus behind a reverse proxy with authentication, or restrict port 9090 to localhost with a firewall rule:
sudo ufw deny 9090. - Use HTTPS for Grafana with Let us Encrypt (we covered that in a previous tutorial).
Wrapping Up
You now have a production-grade monitoring stack running in Docker, collecting metrics from your host and containers, with beautiful dashboards to visualize it all. The best part? It is entirely self-hosted, open-source, and free — no SaaS subscriptions, no data leaving your network.
From here, you can extend this setup in all sorts of directions: set up Alertmanager for notifications (Slack, email, PagerDuty), monitor specific application metrics with custom exporters, or even add Loki for log aggregation alongside your metrics.
The infrastructure you build to observe your infrastructure is some of the most satisfying work in the homelab world. Happy monitoring!
Leave a Reply