Containers
banshee/containers-collection/containers/docker/ssh/
compose
Whether the services of each Docker Compose project of a host run and are healthy, by SSH, through a restricted sudo or the docker group.
by banshee1 installation1 version
Published on October 27, 2026. First version.
// The services of the Docker Compose projects of a Docker host, by SSH: for each service, whether all its
// containers run, their health, and how many of them run. A service is named <project>/<service>, so that
// the selection of the probe can keep one project (myapp/*) or leave out a service that runs once and stops,
// such as a migration (myapp/migrate).
// The projects and services are read from the labels Compose puts on each container
// (com.docker.compose.project, com.docker.compose.service), the same for Compose v1 and v2, through the
// very docker ps of the collector of the containers and its sudoers line: the docker compose plugin is not
// needed. The containers of docker compose run (com.docker.compose.oneoff=True) are left out.
need('protocol:ssh');
$sudo = variable('docker:sudo', 'boolean', array(
'label' => 'Through sudo',
'help' => 'Runs docker through sudo, with the sudoers line of the documentation. Off when the account is in the docker group instead.',
'default' => true,
));
per('service', 'Services');
output('state', 'text', array('label' => 'State', 'per' => 'service'));
output('health', 'text', array('label' => 'Health', 'per' => 'service'));
output('containers', 'number', array('label' => 'Containers', 'per' => 'service'));
output('running', 'number', array('label' => 'Containers running', 'per' => 'service'));
watch.expect('not-running', 'Service not running', 'state', array('critical' => array('running')));
watch.state('unhealthy', 'Service unhealthy', 'health', array('critical' => array('unhealthy')));
watch.missing('missing', 'Service missing', 'state', array('enabled' => false));
// Runs a docker command, through sudo -n when asked, and returns what it wrote. A daemon that does not
// answer is a host whose containers are all down: critical. Anything else that keeps docker from answering
// (sudo refusing, docker missing, the account outside the docker group) is a setting to fix.
function docker_run($sudo, $arguments) {
$command = '/usr/bin/docker ' . $arguments;
if ($sudo) {
$command = 'sudo -n ' . $command;
}
$read = ssh.run($command);
if ($read['exit'] != 0) {
if (text.contains($read['errors'], 'Cannot connect to the Docker daemon')) {
fail('critical', 'The Docker daemon is not running', $read['errors']);
}
if (text.contains($read['errors'], 'sudo')) {
fail('misconfigured', 'sudo refused to run docker: see the sudoers line of the documentation', $read['errors']);
}
fail('misconfigured', 'docker did not answer', $read['errors']);
}
return $read['output'];
}
// One JSON object per line: {"Names": "myapp-web-1", "State": "running", "Status": "Up 2 hours (healthy)",
// "Labels": "com.docker.compose.project=myapp,com.docker.compose.service=web,...", ...}. The labels are
// written key=value, separated by commas.
$output = docker_run($sudo, "ps --all --format '{{json .}}'");
$services = array();
foreach (text.lines($output) as $line) {
$container = json.read($line);
$labels = $container['Labels'];
$project = text.match($labels, '(?:^|,)com\.docker\.compose\.project=([^,]*)');
$service = text.match($labels, '(?:^|,)com\.docker\.compose\.service=([^,]*)');
if (list.count($project) == 0 || list.count($service) == 0) {
continue;
}
if (text.contains($labels, 'com.docker.compose.oneoff=True')) {
continue;
}
$name = $project[1] . '/' . $service[1];
if (!list.has($services, $name)) {
$services[$name] = array('state' => 'running', 'health' => 'none', 'containers' => 0, 'running' => 0);
}
$services[$name]['containers'] = $services[$name]['containers'] + 1;
// The service runs when all its containers run; otherwise its state is that of the first that does not.
if ($container['State'] == 'running') {
$services[$name]['running'] = $services[$name]['running'] + 1;
} elseif ($services[$name]['state'] == 'running') {
$services[$name]['state'] = $container['State'];
}
// The worst health of its containers: unhealthy, then starting, then healthy; none without a health check.
$status = $container['Status'];
if (text.contains($status, '(unhealthy)')) {
$services[$name]['health'] = 'unhealthy';
} elseif (text.contains($status, '(health: starting)') && $services[$name]['health'] != 'unhealthy') {
$services[$name]['health'] = 'starting';
} elseif (text.contains($status, '(healthy)') && $services[$name]['health'] == 'none') {
$services[$name]['health'] = 'healthy';
}
}
foreach ($services as $name => $service) {
value('state', $service['state'], $name);
value('health', $service['health'], $name);
value('containers', $service['containers'], $name);
value('running', $service['running'], $name);
}