June 9, 2026
Docker Swarm Monitoring with Prometheus and Grafana: The Stack, the Alerts and What to Watch (2026)
A deployable monitoring stack for Docker Swarm: Prometheus with service discovery, cAdvisor and node-exporter on every node, the Docker daemon’s own metrics, Grafana provisioned from a config, the seven alerts that matter, and where AI-assisted summaries help.

Monitoring a swarm is easier than monitoring most things, because the swarm already knows where everything is. A global service puts one exporter on every node, an overlay network gives Prometheus a DNS name that resolves to all of them, and the daemon can publish its own metrics. This is the stack we run, as a file you can deploy, followed by the alerts worth waking up for and a short, honest section on what AI adds.
The stack file
Four services: Prometheus on a manager with a local volume, cAdvisor and node-exporter on every node, Grafana wherever, all on one overlay network that nothing else joins. Configuration comes in as Swarm configs so the stack file is the whole deployment.
services:
prometheus:
image: prom/prometheus:v3.5.0
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=30d
deploy:
placement:
constraints: [node.role == manager]
resources:
limits: { memory: 2G }
configs:
- source: prometheus_yml
target: /etc/prometheus/prometheus.yml
- source: alert_rules
target: /etc/prometheus/alerts.yml
volumes:
- prometheus_data:/prometheus
networks: [monitoring]
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.52.1
command: ['--docker_only=true', '--housekeeping_interval=15s']
deploy:
mode: global
resources:
limits: { memory: 256M }
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
networks: [monitoring]
node-exporter:
image: prom/node-exporter:v1.9.1
command: ['--path.rootfs=/host']
deploy:
mode: global
resources:
limits: { memory: 128M }
volumes:
- /:/host:ro,rslave
networks: [monitoring]
grafana:
image: grafana/grafana:12.1.0
deploy:
placement:
constraints: [node.labels.grafana == true]
environment:
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin
GF_SERVER_ROOT_URL: https://grafana.example.com
secrets: [grafana_admin]
configs:
- source: grafana_datasource
target: /etc/grafana/provisioning/datasources/prometheus.yml
volumes:
- grafana_data:/var/lib/grafana
networks: [monitoring, traefik-public]
# publish through your reverse proxy; do not expose 3000 on the mesh
volumes:
prometheus_data:
grafana_data:
networks:
monitoring:
driver: overlay
traefik-public:
external: true
configs:
prometheus_yml:
file: ./prometheus.yml
alert_rules:
file: ./alerts.yml
grafana_datasource:
file: ./grafana-datasource.yml
secrets:
grafana_admin:
external: true
Pin the image tags to what you tested; the ones above are the current releases at the time of writing. Prometheus and Grafana are pinned to a node because their data is a local volume that does not follow a task; the HA database guide explains why that is the right call rather than a distributed filesystem.
Discovery: tasks.<service>
prometheus.yml needs no list of nodes. On an overlay network, tasks.cadvisor resolves to the IP of every cAdvisor task, so a scrape target that names it scrapes every node, and a node that joins tomorrow is scraped tomorrow:
global:
scrape_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: cadvisor
dns_sd_configs:
- names: ['tasks.cadvisor']
type: A
port: 8080
- job_name: node-exporter
dns_sd_configs:
- names: ['tasks.node-exporter']
type: A
port: 9100
- job_name: docker-daemon
dns_sd_configs:
- names: ['tasks.node-exporter'] # one entry per node; the daemon listens on the host
type: A
relabel_configs:
- source_labels: [__address__]
regex: '([^:]+):.*'
target_label: __address__
replacement: '${1}:9323'
The third job scrapes the Docker daemon itself, which publishes task counts, engine state and Swarm manager metrics once daemon.json on each node carries "metrics-addr": "0.0.0.0:9323" and the daemon is restarted. Bind it to the node's private address rather than 0.0.0.0 if the interface is reachable from outside the swarm; the metrics are not secret, but they are not for the internet either.
Grafana's data source is provisioned from a file, so a fresh Grafana comes up already pointed at Prometheus:
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
Deploy with docker stack deploy -c monitoring-stack.yml monitoring after creating the admin secret and labelling the Grafana node.
The seven alerts that page
Everything else is a dashboard. These go in alerts.yml:
groups:
- name: swarm
rules:
- alert: NodeDown
expr: up{job="node-exporter"} == 0
for: 3m
labels: { severity: page }
annotations: { summary: 'node-exporter on {{ $labels.instance }} unreachable for 3 minutes' }
- alert: DiskFilling
expr: (node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|overlay"}) < 0.2
for: 10m
labels: { severity: page }
annotations: { summary: 'root filesystem on {{ $labels.instance }} under 20% free' }
- alert: TaskRestarting
expr: changes(container_start_time_seconds{container_label_com_docker_swarm_service_name!=""}[1h]) > 3
for: 0m
labels: { severity: page }
annotations:
{
summary: '{{ $labels.container_label_com_docker_swarm_service_name }} restarted more than 3 times in an hour',
}
- alert: ContainerNearMemoryLimit
expr: container_memory_working_set_bytes{container_label_com_docker_swarm_service_name!=""} / container_spec_memory_limit_bytes{container_label_com_docker_swarm_service_name!=""} > 0.9
for: 5m
labels: { severity: warn }
annotations:
{
summary: '{{ $labels.container_label_com_docker_swarm_service_name }} above 90% of its memory limit',
}
- alert: SwarmManagerQuorumAtRisk
expr: swarm_manager_nodes{state="ready"} < swarm_manager_nodes{state="ready"} offset 30m
for: 5m
labels: { severity: page }
annotations: { summary: 'fewer manager nodes ready than 30 minutes ago' }
- alert: NodeMemoryPressure
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 0.9
for: 10m
labels: { severity: warn }
annotations: { summary: '{{ $labels.instance }} above 90% memory for 10 minutes' }
- alert: HighLoad
expr: node_load5 / count without (cpu, mode) (node_cpu_seconds_total{mode="idle"}) > 2
for: 15m
labels: { severity: warn }
annotations: { summary: '{{ $labels.instance }} load is twice its core count' }
Two of these deserve a note. TaskRestarting reads cAdvisor's container start time with the Swarm service label cAdvisor attaches, which is what turns "a container restarted" into "this service is crash-looping". SwarmManagerQuorumAtRisk reads the daemon's own swarm_manager_nodes gauge, which is why the daemon job is worth the daemon.json change: the manager count is the one number a swarm cannot afford to lose track of, and only the daemon knows it.
The eighth alert is a service below its desired replica count. cAdvisor cannot see desired state, so it needs either the daemon metrics on a manager, or a small exporter that reads docker service ls; several community exporters do this, and SwarmCLI shows it live in the services view, which is where most teams look first anyway.
Route them with Alertmanager, or with Grafana's built-in alerting reading the same rules; both work, and the choice is which UI you want to manage silences in.
Dashboards
Three, and no more until you need them:
- Nodes: CPU, memory, disk and network per node from node-exporter, with the node's Swarm role in the legend.
- Services: memory and CPU per service from cAdvisor, grouped by
container_label_com_docker_swarm_service_name, and restarts per service over the last day. - Swarm: manager count, node count by state, tasks by state, from the daemon job.
Grafana's community dashboard library has starting points for node-exporter and cAdvisor; import one, then delete two thirds of its panels. A dashboard nobody reads is a dashboard.
Logs
Metrics say that something is wrong; logs say what. Loki with a Promtail or Alloy agent as a global service, reading /var/lib/docker/containers, gives Grafana one place for both, with the Swarm service name as a label. It is a second stack and a second retention decision, so add it once the metrics stack has been up for a month and you know what you are missing. Until then, docker service logs and SwarmCLI's log view across replicas cover the incident.
Where AI helps, and where it does not
An alert that fires at 3 a.m. with a summary line is better than one without, and a language model is good at writing that line: give it the alert, the last fifty log lines of the service and the last three deploy events, and ask for a one-paragraph account of what probably happened and what to check first. A webhook receiver that does this and posts the paragraph beside the alert is a hundred lines of code, and with Ollama on the swarm nothing leaves your network.
What it is not good at is deciding thresholds or restarting things. Anomaly detection on a swarm of a dozen services produces more noise than the seven rules above, because the workloads are few enough that a person already knows what normal looks like. And an automated remediation that restarts a service on an alert is a loop waiting to happen; the restart belongs in restart_policy and update_config, where Swarm already does it deliberately. Use the model to explain, and let the scheduler heal.
Common concerns, answered
How do I monitor Docker Swarm with Prometheus? Run cAdvisor and node-exporter as global services so every node has one, put them and Prometheus on an overlay network, and point Prometheus at tasks.cadvisor and tasks.node-exporter with dns_sd_configs, which Swarm resolves to every task IP. Add the Docker daemon’s own metrics with metrics-addr in daemon.json.
What should I alert on in a Docker Swarm? A node down, a disk over 80 percent, a task restarting repeatedly, a container near its memory limit, manager quorum at risk, a node under memory pressure, and sustained high load. Those are the seven rules this article ships; a service below its desired replica count is the eighth, and it needs an exporter that can see desired state. Everything else is a dashboard.
Can Grafana discover Docker Swarm services? Grafana reads from Prometheus, and Prometheus does the discovery: the tasks.<service> DNS name on an overlay network returns every task of that service, so a global exporter is scraped on every node without listing nodes anywhere.
What to watch on day one
upfor every job equals the node count, or a node is not scraped.- The daemon job returns
swarm_manager_nodes; if it does not,metrics-addris missing on that node. - Disk on every node, because the first thing a monitoring stack does is fill a disk with metrics, and 30 days of retention on a busy swarm is a few gigabytes.
From there, the best-practices checklist has the operational rules the alerts above assume, and the ten most common issues are what the alerts will mostly be telling you about.