Containerized monitoring stack for two Docker hosts using Prometheus, Grafana, node_exporter, cAdvisor, and n8n-driven Telegram alerts.
This repository is sanitized for public use. Replace the placeholders in .env.example and the n8n workflow before deployment. Do not commit .env or exported workflow files containing real bot tokens, chat IDs, hostnames, or private IPs.
- Host A runs the core monitoring stack:
- Prometheus with 90-day TSDB retention
- Grafana OSS
- host-level
node_exporter - cAdvisor for Docker/container metrics
- Host B runs a lightweight monitoring-agent stack:
- host-level
node_exporter - cAdvisor
- host-level
- Prometheus scrapes both hosts every 15 seconds.
- An example application scrape target is included for Qdrant-style
/metricsendpoints. Replace it with your own app target or remove it. - Grafana imports community dashboards for host and container visibility.
- n8n polls Prometheus
/api/v1/queryevery 5 minutes, deduplicates alerts for 60 minutes using workflow static data, and routes messages to Telegram.
This stack intentionally keeps the alert path simple:
- No Alertmanager.
- No Blackbox exporter.
- n8n owns the polling, dedupe, formatting, and Telegram routing.
That trades some native Prometheus alerting features for a workflow users can inspect and modify in n8n. It also makes it easy to reuse an existing Telegram bot/chat without creating another bot.
The included n8n workflow implements five Prometheus queries:
- Host down
- Query:
up == 0 - Requires two consecutive 5-minute polls before alerting.
- Query:
- Disk available below threshold
- Default threshold:
< 15%available. - Excludes ephemeral/container filesystems such as
tmpfs,overlay,squashfs,ramfs,devtmpfs,nsfs, and fuse mounts.
- Default threshold:
- Memory usage above 90%
- Uses
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes.
- Uses
- Sustained CPU above 90% over 10 minutes
- Uses idle CPU rate over a 10-minute window.
- Container restarted within 15 minutes
- Uses
changes(container_start_time_seconds{name!=""}[15m]) > 0from cAdvisor.
- Uses
Alert message format:
🚨 Prometheus Alert
Host: <host>
Rule: <rule>
Value: <value>
Time: <ISO timestamp>
Import these dashboard IDs and bind them to the Prometheus datasource:
1860— Node Exporter Full14282— cAdvisor exporter
compose/host-a/docker-compose.yml # Prometheus + Grafana + exporters
compose/host-b/docker-compose.yml # exporter-only agent stack
prometheus/prometheus.yml # sanitized scrape config
n8n/prometheus-alerts.workflow.json # sanitized n8n workflow export
.env.example # required variables and placeholders
.gitignore # keeps secrets out from commit one
-
Clone the repo on Host A and Host B.
-
Copy
.env.exampleto.envand replace placeholders. -
On Host A, edit or render
prometheus/prometheus.ymlso target placeholders resolve to your hosts.- Prometheus does not automatically expand shell environment variables inside YAML in all configurations.
- A common pattern is to keep
prometheus.ymlas a template and render it withenvsubstbefore starting Prometheus.
-
Start Host A:
cd compose/host-a docker compose up -d -
Start Host B:
cd compose/host-b docker compose up -d -
Verify Prometheus targets:
curl http://localhost:${PROMETHEUS_PORT:-9090}/api/v1/targets -
Configure Grafana:
- URL:
http://<host-a>:${GRAFANA_PORT:-3000} - Add Prometheus datasource:
http://prometheus:9090if Grafana and Prometheus share the same Compose network. - Import dashboards
1860and14282.
- URL:
-
Import the n8n workflow:
n8n import:workflow --input=n8n/prometheus-alerts.workflow.json --activeState=false n8n update:workflow --id=<imported-id> --active=true
Depending on your n8n version/deployment mode, you may need to restart n8n so the scheduler picks up the newly activated workflow.
The workflow is sanitized. Replace these placeholders before use:
{{TELEGRAM_BOT_TOKEN}}{{TELEGRAM_CHAT_ID}}PROMETHEUS_URL/$env.PROMETHEUS_URL
If your n8n container is not on the same Docker network as Prometheus, do not use the Compose service name prometheus. Use a URL reachable from the n8n container.
- Bind-mounted config permissions matter. Prometheus runs as a non-root user; parent directories and
prometheus.ymlmust be traversable/readable by that user. A readable file inside an unreadable directory is still unreadable. Linux, refreshingly, does not care about intent. - Host port collisions happen. cAdvisor defaults often collide with existing services. Pick the nearest free port and document the deviation.
- Grafana first-run state matters. If the Grafana data volume already exists, changing
GF_SECURITY_ADMIN_PASSWORDin Compose may not reset the admin password. Use the Grafana CLI to reset it deliberately. - n8n scheduler pickup may require restart. CLI activation can mark a workflow active in the database while the running process has not loaded the new schedule yet.
- n8n CLI execution is not the same as scheduler execution. A Schedule Trigger workflow may not run through
n8n executeunless it also has an Execute Workflow Trigger. Test scheduler behavior with scheduler-driven runs. - Public repos need a first-push audit. Sanitize before commit one. Git history is permanent enough to punish optimism.
Before publishing changes, run:
gitleaks detect --no-git
# plus explicit private-IP and chat-ID greps for your environmentDo not push real IPs, bot tokens, chat IDs, passwords, API keys, or exported workflow instance IDs.