- Shell 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| test | ||
| compose-exporter-dashboard.json | ||
| compose-exporter.alerts.yml | ||
| compose-exporter.env.example | ||
| compose-exporter.overrides.example | ||
| compose-exporter.service | ||
| compose-exporter.sh | ||
| compose-exporter.timer | ||
| LICENSE | ||
| README.md | ||
| screenshot.png | ||
compose-exporter
Exports Docker Compose stack and service status and image version drift as Prometheus metrics. It runs on each Docker host from a systemd timer and writes the metrics for node-exporter's textfile collector, so they're scraped along with the host's other metrics (it can also push straight to VictoriaMetrics). A Grafana dashboard aggregates across hosts.
It covers every container started by Compose (those with a com.docker.compose.project label); containers started with plain docker run are ignored.
An update is detected two ways:
- Newer tag: a newer tag of the same shape exists (
16.4-alpine→16.5-alpine). - Re-pushed tag: the registry digest of the running tag no longer matches the local image (catches
:latest,:16, etc.).
Registry answers are cached (1h by default), so the timer can run every 5 minutes without hitting rate limits.
Requirements
On each Docker host: bash 4.4+, docker, curl, jq, coreutils, systemd, and node-exporter (unless you push to VictoriaMetrics instead). The script runs as root, for the Docker socket and root's registry logins. The docker compose plugin is only needed if STACK_GLOB is set.
Elsewhere: Prometheus or VictoriaMetrics scraping those node-exporters, and Grafana for the dashboard and alerts.
Setup
Clone the repo on each host. Then, as root, symlink the script and the units, so git pull updates them. Copy the config, so each host has its own:
REPO=/opt/compose-exporter # wherever you cloned it
ln -s $REPO/compose-exporter.sh /usr/local/bin/compose-exporter.sh
ln -s $REPO/compose-exporter.service /etc/systemd/system/compose-exporter.service
ln -s $REPO/compose-exporter.timer /etc/systemd/system/compose-exporter.timer
install -m 600 $REPO/compose-exporter.env.example /etc/compose-exporter.env
install -m 644 $REPO/compose-exporter.overrides.example /etc/compose-exporter.overrides
mkdir -p /var/lib/node_exporter/textfile_collector
systemctl daemon-reload
systemctl enable --now compose-exporter.timer
In /etc/compose-exporter.env:
STACK_GLOB: point it at your compose files. Without it, a stack that's beendocker compose downed has no containers left and simply disappears, instead of reporting as down.HOST_LABEL: the metrics carry ahostlabel,hostname -sby default. If your scrape config already gives this node-exporter target ahostlabel, setHOST_LABELto the same value, or the series end up with an extraexported_hostlabel.
node-exporter has to read the textfile directory. For node-exporter in a container, add a flag and a mount:
command:
# ...existing flags...
- '--collector.textfile.directory=/textfile'
volumes:
# ...existing mounts...
- /var/lib/node_exporter/textfile_collector:/textfile:ro
Ubuntu's prometheus-node-exporter package already reads /var/lib/prometheus/node-exporter; point TEXTFILE_DIR there instead.
Check with compose-exporter.sh --stdout, which prints the metrics instead of writing them, and then curl -s localhost:9100/metrics | grep ^compose_ (or wherever node-exporter listens).
Finally, import compose-exporter-dashboard.json into Grafana and pick your Prometheus/VictoriaMetrics data source.
Configuration
/etc/compose-exporter.env: where to write or push metrics, host label, expected stacks, cache timings, registry auth. Every option is documented in the header ofcompose-exporter.sh.- Container label
monitor.periodic: "true": for services that run a job, exit and are restarted by their restart policy. They count as up while exited or restarting, as long as the last run exited with 0. Long-running services that exit cleanly now and then (e.g.queue:work --max-time) don't need it: a service with analwaysorunless-stoppedpolicy counts as up while it's being restarted, if it exited with 0 after running for at least 10s, less than a minute ago. /etc/compose-exporter.overrides: per-image rules (digest,regex:<ERE>,ignore) for images whose tags shouldn't be compared by shape, e.g. staying on Postgres 16.- The timer runs every 5 minutes. The dashboard and alerts look back 10 minutes and treat a check older than 15 minutes as stale, so if you run it less often, raise those to match or hosts will drop out of the dashboard and fire stale alerts.
Alerts
compose-exporter.alerts.yml has Grafana alert rules. Copy it into Grafana's provisioning/alerting/ directory, adjust it to your setup, and restart Grafana. Copy rather than symlink: the provisioning directory is usually mounted into the Grafana container, and a symlink out of it won't resolve there.
To adjust:
datasourceUid: the uid of your Prometheus/VictoriaMetrics data source (in its URL under Connections → Data sources). It's set once per rule:sed -i 's/datasourceUid: victoriametrics/datasourceUid: <your-uid>/' compose-exporter.alerts.ymlfolder: the alert folder to file the rules under. Grafana creates it if it doesn't exist.orgId: only if the rules belong in an organisation other than the default.
The rules carry the labels service=compose-exporter and severity (critical, warning or info). Without a notification policy matching them, every alert goes to your default contact point, including the low-priority update alerts.
| Alert | Fires when |
|---|---|
| compose-exporter check is stale | a host's last check is over 15 min old (script failing, Docker down, host down). Also fires if no host reports at all. Stops after a day, so decommissioned hosts clear themselves. |
| Compose stack down | a stack has been down for 10 min |
| compose-exporter registry lookups failing | registry lookups have been failing for an hour |
| Image update available | a service's image has a newer tag, or its tag was re-pushed, for 15 min. Fires again for each further release |
The stale check matters most with the textfile collector. If the script stops, node-exporter keeps serving the last file, and the dashboard shows frozen data that looks live. The alert reads the timestamp inside the metrics, so it catches this.
Images on floating tags (:latest, nightly builds) might be re-pushed often, and each re-push fires the update alert. To alert on new tags only, change update=~"tag|digest" to update="tag" in that rule's query. To mute it for particular images, add an ignore override.
Authentication
Public images need no credentials.
Registries (private images, or higher Docker Hub rate limits): run docker login <registry> as root. The script reads /root/.docker/config.json, but only inline auths entries. Credential helpers (credsStore / credHelpers) aren't supported. If one is configured, lookups fail, and the image reports update="unknown" and counts towards compose_registry_errors. A read-only token is enough, e.g. a Forgejo token with read:package scope.
VictoriaMetrics (only when pushing with VM_URL): set VM_AUTH=user:password in /etc/compose-exporter.env, which is root-only.
Metrics
Every series has a host label. Stack metrics add stack, and service and image metrics add stack and service.
| Metric | Meaning |
|---|---|
compose_stack_up |
1 if the stack has containers and all its services are up |
compose_stack_containers, compose_stack_containers_running |
container counts per stack |
compose_service_up |
1 if all of the service's containers are running and not unhealthy, or are one-shot or periodic jobs whose last run succeeded |
compose_image_info |
labels image, tag, current, latest, update (none, tag, digest, unknown, local, pinned, ignored) |
compose_image_update_available |
1 if a newer tag or re-pushed image exists |
compose_image_checked_timestamp_seconds |
when the registry was last asked about the service's image (answers are cached for CACHE_TTL). Missing for local, pinned and ignored images |
compose_registry_errors |
number of failed registry lookups |
compose_check_timestamp_seconds, compose_check_duration_seconds |
when the last run finished and how long it took |
compose_image_info's value is the time of the check, not 1. When an image's labels change (say, after an update), the old series lingers for the query's lookback window, so keep only the newest per service:
last_over_time(compose_image_info[10m])
== on (host, stack, service) group_left
max by (host, stack, service) (last_over_time(compose_image_info[10m]))
Tests
bats test/
Needs bats-core (brew install bats-core / apt install bats) and bash 4.4+. docker and curl are replaced by stubs in test/stubs/, so no Docker or network access is needed.
License
MIT, see LICENSE.
