Your homelab is growing. You've got containers spinning up, databases running, reverse proxies routing traffic. But do you actually know what's happening inside? Is your disk filling up? Is that API response time creeping up? Is a container crash-looping at 3 AM?
Prometheus and Grafana answer those questions. Together, they form the industry-standard monitoring stack — Prometheus scrapes and stores metrics, Grafana turns them into beautiful, actionable dashboards. And yes, you can self-host the whole thing in about 20 minutes.
Why Self-Host Your Monitoring Stack?
Cloud monitoring solutions are convenient until they're not. Datadog bills can spiral fast. New Relic's pricing model punishes you for growing. And do you really want your infrastructure metrics sitting on someone else's servers?
Self-hosting Prometheus + Grafana gives you:
- Full data ownership — your metrics never leave your infrastructure
- Zero per-metric pricing — monitor as much as you want
- No vendor lock-in — open-source, portable, future-proof
- Deep integration — works natively with Docker, Node Exporter, and hundreds of exporters
What You'll Build
By the end of this guide, you'll have:
- Prometheus scraping metrics from your Docker host and containers
- Grafana with pre-built dashboards for system and container metrics
- Alertmanager configured for notifications (optional but recommended)
- Everything running in Docker Compose with persistent storage
Prerequisites
- A VPS or homelab server with Docker and Docker Compose installed
- Minimum 2GB RAM (4GB recommended for longer metric retention)
- 10GB+ free disk space (metrics add up over time)
- Basic familiarity with Docker Compose
Step 1: Create the Project Structure
mkdir -p ~/monitoring/{prometheus,grafana/provisioning/datasources,grafana/provisioning/dashboards,grafana/dashboards,alertmanager}
cd ~/monitoring
Step 2: Configure Prometheus
Create prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
rule_files:
- "alert_rules.yml"
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']
- job_name: 'docker'
static_configs:
- targets: ['host.docker.internal:9323']
Create prometheus/alert_rules.yml with some basic alerts:
groups:
- name: system_alerts
rules:
- alert: HighCPUUsage
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
- alert: LowDiskSpace
expr: (node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100 < 15
for: 5m
labels:
severity: critical
annotations:
summary: "Low disk space on {{ $labels.instance }}"
- alert: ContainerDown
expr: up{job="cadvisor"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "cAdvisor target is down"
Step 3: Configure Grafana Provisioning
Create grafana/provisioning/datasources/datasource.yml:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
Create grafana/provisioning/dashboards/dashboard.yml:
apiVersion: 1
providers:
- name: 'default'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards
foldersFromFilesStructure: true
Step 4: Docker Compose Stack
Create docker-compose.yml in the project root:
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- monitoring
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
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
privileged: true
devices:
- /dev/kmsg
ports:
- "8080:8080"
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
- ./grafana/dashboards:/var/lib/grafana/dashboards
- grafana_data:/var/lib/grafana
ports:
- "3000:3000"
networks:
- monitoring
depends_on:
- prometheus
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
restart: unless-stopped
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
ports:
- "9093:9093"
networks:
- monitoring
networks:
monitoring:
driver: bridge
volumes:
prometheus_data:
grafana_data:
Step 5: Configure Alertmanager
Create alertmanager/alertmanager.yml:
global:
smtp_smarthost: 'localhost:587'
smtp_from: '[email protected]'
route:
receiver: 'default'
receivers:
- name: 'default'
# Add your notification config here:
# email_configs:
# - to: '[email protected]'
# slack_configs:
# - api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
# channel: '#alerts'
Step 6: Launch the Stack
docker compose up -d
Verify all containers are running:
docker compose ps
You should see five containers: prometheus, node-exporter, cadvisor, grafana, and alertmanager.
Step 7: Verify Prometheus Is Scraping
Open http://your-server-ip:9090/targets in your browser. You should see all four scrape targets in an UP state:
- prometheus — Prometheus monitoring itself
- node-exporter — Host-level metrics (CPU, memory, disk, network)
- cadvisor — Container-level metrics (per-container CPU, memory, network)
- docker — Docker daemon metrics (if enabled)
If any target shows DOWN, check the container logs:
docker logs prometheus
docker logs node-exporter
Step 8: Access Grafana and Import Dashboards
Open http://your-server-ip:3000 and log in with admin / admin123.
The Prometheus datasource is already configured via provisioning. Now import some dashboards:
- Go to Dashboards → Import
- Import ID
1860for Node Exporter Full — a comprehensive host metrics dashboard - Import ID
14282for cAdvisor Docker monitoring — per-container stats
Within seconds, you'll see live dashboards with CPU usage, memory consumption, disk I/O, network throughput, and per-container breakdowns.
Step 9: (Optional) Enable Docker Daemon Metrics
To get Docker engine-level metrics, enable the metrics endpoint on your Docker daemon. Edit /etc/docker/daemon.json:
{
"metrics-addr": "0.0.0.0:9323",
"experimental": true
}
Restart Docker:
sudo systemctl restart docker
Now the docker job in Prometheus will show engine-level metrics like container counts, image counts, and build info.
Step 10: (Optional) Add HTTPS with a Reverse Proxy
If you're exposing Grafana publicly, put it behind a reverse proxy like Traefik or Nginx with TLS. Check out our guides on self-hosting Traefik for a complete setup.
What's Next?
Now that you have metrics flowing, here are natural next steps:
- Add more exporters — PostgreSQL, Redis, Nginx, Blackbox exporter for endpoint probing
- Set up alert notifications — Connect Alertmanager to Slack, Discord, or email
- Long-term storage — Add Thanos or Mimir for multi-year metric retention
- Log aggregation — Pair with Loki for a complete observability stack (Grafana's "LGTM" stack)
Monitoring isn't optional once you run more than a couple of services. With Prometheus and Grafana self-hosted, you get enterprise-grade observability without the enterprise-grade invoice.
Happy monitoring. 🔥