Repository: moby/moby (2025-06-28T16:42:04Z)

Issue:
Add container `health` to list containers API response for ease of monitoring
### Description

It'd be great if the [list containers](https://docs.docker.com/reference/api/engine/version/v1.50/#tag/Container/operation/ContainerList) endpoint included the container's `health` status. Currently, monitoring containers requires multiple API calls.

1. Call `/containers/json` to get running containers
2. Separate calls to `{id}/stats` for each container to get resource usage
3. Separate calls to  `{id}/json` for each container to get the health status

Including it in `stats` would also be an improvement, but I think including it in the container lists response makes the most sense. That way you could monitor the health of all containers on a host using a single endpoint:

```JSON
[
  {
    "Id": "aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf",
    "Names": [],
    "Image": "docker.io/library/ubuntu:latest",
    "ImageID": "sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
    "ImageManifestDescriptor": {},
    "Command": "/bin/bash",
    "Created": "1739811096",
    "Ports": [],
    "SizeRw": "122880",
    "SizeRootFs": "1653948416",
    "Labels": {},
    "State": "running",
    "Health": "healthy",
    "Status": "Up 4 days",
    "HostConfig": {},
    "NetworkSettings": {},
    "Mounts": []
  }
]
```


Discussion:
It's already possible to determine if container is healthy by checking the `Status` like:
```json
    "Status": "Up 11 days (healthy)",
```

However, I don't like this and I think we should indeed have a separate `HealthStatus` field and the actual string presentation should be handled by the CLI.


+1 for adding a separate `HealthStatus` to `/containers/json` from me! Maybe even that could be [`Health` struct](https://github.com/moby/moby/blob/ee49437e38bb457a7724922c21610aaa4d9c4282/api/types/container/health.go#L23-L27) excluding the `Log`.


We discussed this in the maintainers meeting yesterday and there was no objections to adding the include the `Health` object that would reflect the [`Health` struct](https://github.com/moby/moby/blob/ee49437e38bb457a7724922c21610aaa4d9c4282/api/types/container/health.go#L23-L27) excluding the `Log`. 

Would you like to open a PR for that?
> We discussed this in the maintainers meeting yesterday and there was no objections to adding the include the `Health` object that would reflect the [`Health` struct](https://github.com/moby/moby/blob/ee49437e38bb457a7724922c21610aaa4d9c4282/api/types/container/health.go#L23-L27) excluding the `Log`.
> 
> Would you like to open a PR for that?

@vvoland Great! Thanks for taking this to the team. Unfortunately I'm not familiar with Go - hopefully someone else can help with a PR.
I would like to work on this. May i take it?
> I would like to work on this. May i take it?

That’d be great, thank you!



Patch:
diff --git a/api/swagger.yaml b/api/swagger.yaml
index 6bc486be5ad95..2b51cf6288e89 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -5346,6 +5346,29 @@ definitions:
           List of mounts used by the container.
         items:
           $ref: "#/definitions/MountPoint"
+      Health:
+        type: "object"
+        description: |-
+          Summary of health status
+
+          Added in v1.52, before that version all container summary not include Health.
+          After this attribute introduced, it includes containers with no health checks configured,
+          or containers that are not running with none
+        properties:
+          Status:
+            type: "string"
+            description: |-
+              the health status of the container
+            enum:
+              - "none"
+              - "starting"
+              - "healthy"
+              - "unhealthy"
+            example: "healthy"
+          FailingStreak:
+            description: "FailingStreak is the number of consecutive failures"
+            type: "integer"
+            example: 0
 
   Driver:
     description: "Driver represents a driver (network, logging, secrets)."
diff --git a/api/types/container/container.go b/api/types/container/container.go
index 8e9d53d58e3b6..3e1e37eed06c8 100644
--- a/api/types/container/container.go
+++ b/api/types/container/container.go
@@ -128,6 +128,7 @@ type Summary struct {
 		NetworkMode string            `json:",omitempty"`
 		Annotations map[string]string `json:",omitempty"`
 	}
+	Health          *HealthSummary `json:",omitempty"`
 	NetworkSettings *NetworkSettingsSummary
 	Mounts          []MountPoint
 }
diff --git a/api/types/container/health.go b/api/types/container/health.go
index 96e91cc8d8a8e..b7fe592cc25b7 100644
--- a/api/types/container/health.go
+++ b/api/types/container/health.go
@@ -26,6 +26,12 @@ type Health struct {
 	Log           []*HealthcheckResult // Log contains the last few results (oldest first)
 }
 
+// HealthSummary stores a summary of the container's healthcheck results.
+type HealthSummary struct {
+	Status        HealthStatus // Status is one of [NoHealthcheck], [Starting], [Healthy] or [Unhealthy].
+	FailingStreak int          // FailingStreak is the number of consecutive failures
+}
+
 // HealthcheckResult stores information about a single run of a healthcheck probe
 type HealthcheckResult struct {
 	Start    time.Time // Start is the time this check started
diff --git a/daemon/container/view.go b/daemon/container/view.go
index 3daed8bc0245d..4aac9525787af 100644
--- a/daemon/container/view.go
+++ b/daemon/container/view.go
@@ -296,9 +296,17 @@ func (v *View) GetAllNames() map[string][]string {
 // A lock on the Container is not held because these are immutable deep copies.
 func (v *View) transform(ctr *Container) *Snapshot {
 	health := container.NoHealthcheck
+	failingStreak := 0
 	if ctr.Health != nil {
 		health = ctr.Health.Status()
+		failingStreak = ctr.Health.FailingStreak
 	}
+
+	healthSummary := &container.HealthSummary{
+		Status:        health,
+		FailingStreak: failingStreak,
+	}
+
 	snapshot := &Snapshot{
 		Summary: container.Summary{
 			ID:      ctr.ID,
@@ -308,6 +316,7 @@ func (v *View) transform(ctr *Container) *Snapshot {
 			Mounts:  ctr.GetMountPoints(),
 			State:   ctr.State.StateString(),
 			Status:  ctr.State.String(),
+			Health:  healthSummary,
 			Created: ctr.Created.Unix(),
 		},
 		CreatedAt:    ctr.Created,
diff --git a/daemon/server/router/container/container_routes.go b/daemon/server/router/container/container_routes.go
index 5fd27ce15a5b7..e84772b3341b1 100644
--- a/daemon/server/router/container/container_routes.go
+++ b/daemon/server/router/container/container_routes.go
@@ -119,6 +119,12 @@ func (c *containerRouter) getContainersJSON(ctx context.Context, w http.Response
 		}
 	}
 
+	if versions.LessThan(version, "1.52") {
+		for _, c := range containers {
+			c.Health = nil
+		}
+	}
+
 	return httputils.WriteJSON(w, http.StatusOK, containers)
 }
 
diff --git a/vendor/github.com/moby/moby/api/swagger.yaml b/vendor/github.com/moby/moby/api/swagger.yaml
index 6bc486be5ad95..2b51cf6288e89 100644
--- a/vendor/github.com/moby/moby/api/swagger.yaml
+++ b/vendor/github.com/moby/moby/api/swagger.yaml
@@ -5346,6 +5346,29 @@ definitions:
           List of mounts used by the container.
         items:
           $ref: "#/definitions/MountPoint"
+      Health:
+        type: "object"
+        description: |-
+          Summary of health status
+
+          Added in v1.52, before that version all container summary not include Health.
+          After this attribute introduced, it includes containers with no health checks configured,
+          or containers that are not running with none
+        properties:
+          Status:
+            type: "string"
+            description: |-
+              the health status of the container
+            enum:
+              - "none"
+              - "starting"
+              - "healthy"
+              - "unhealthy"
+            example: "healthy"
+          FailingStreak:
+            description: "FailingStreak is the number of consecutive failures"
+            type: "integer"
+            example: 0
 
   Driver:
     description: "Driver represents a driver (network, logging, secrets)."
diff --git a/vendor/github.com/moby/moby/api/types/container/container.go b/vendor/github.com/moby/moby/api/types/container/container.go
index 8e9d53d58e3b6..3e1e37eed06c8 100644
--- a/vendor/github.com/moby/moby/api/types/container/container.go
+++ b/vendor/github.com/moby/moby/api/types/container/container.go
@@ -128,6 +128,7 @@ type Summary struct {
 		NetworkMode string            `json:",omitempty"`
 		Annotations map[string]string `json:",omitempty"`
 	}
+	Health          *HealthSummary `json:",omitempty"`
 	NetworkSettings *NetworkSettingsSummary
 	Mounts          []MountPoint
 }
diff --git a/vendor/github.com/moby/moby/api/types/container/health.go b/vendor/github.com/moby/moby/api/types/container/health.go
index 96e91cc8d8a8e..b7fe592cc25b7 100644
--- a/vendor/github.com/moby/moby/api/types/container/health.go
+++ b/vendor/github.com/moby/moby/api/types/container/health.go
@@ -26,6 +26,12 @@ type Health struct {
 	Log           []*HealthcheckResult // Log contains the last few results (oldest first)
 }
 
+// HealthSummary stores a summary of the container's healthcheck results.
+type HealthSummary struct {
+	Status        HealthStatus // Status is one of [NoHealthcheck], [Starting], [Healthy] or [Unhealthy].
+	FailingStreak int          // FailingStreak is the number of consecutive failures
+}
+
 // HealthcheckResult stores information about a single run of a healthcheck probe
 type HealthcheckResult struct {
 	Start    time.Time // Start is the time this check started


Test patch:
diff --git a/integration/container/list_test.go b/integration/container/list_test.go
index 390fffcb7a467..088344561ac39 100644
--- a/integration/container/list_test.go
+++ b/integration/container/list_test.go
@@ -1,17 +1,21 @@
 package container
 
 import (
+	"context"
 	"fmt"
 	"testing"
+	"time"
 
 	"github.com/docker/docker/integration/internal/container"
 	"github.com/docker/docker/testutil"
 	"github.com/docker/docker/testutil/request"
 	containertypes "github.com/moby/moby/api/types/container"
 	"github.com/moby/moby/api/types/filters"
+	"github.com/moby/moby/api/types/versions"
 	"github.com/moby/moby/client"
 	"gotest.tools/v3/assert"
 	is "gotest.tools/v3/assert/cmp"
+	"gotest.tools/v3/poll"
 	"gotest.tools/v3/skip"
 )
 
@@ -143,3 +147,57 @@ func TestContainerList_ImageManifestPlatform(t *testing.T) {
 		assert.Check(t, ctr.ImageManifestDescriptor.Platform.Architecture != "")
 	}
 }
+
+func pollForHealthStatusSummary(ctx context.Context, client client.APIClient, containerID string, healthStatus containertypes.HealthStatus) func(log poll.LogT) poll.Result {
+	return func(log poll.LogT) poll.Result {
+		containers, err := client.ContainerList(ctx, containertypes.ListOptions{
+			All:     true,
+			Filters: filters.NewArgs(filters.Arg("id", containerID)),
+		})
+		if err != nil {
+			return poll.Error(err)
+		}
+		total := 0
+		version := client.ClientVersion()
+		for _, ctr := range containers {
+			if ctr.Health == nil && versions.LessThan(version, "1.52") {
+				total++
+			} else if ctr.Health != nil && ctr.Health.Status == healthStatus && versions.GreaterThanOrEqualTo(version, "1.52") {
+				total++
+			}
+		}
+
+		if total == len(containers) {
+			return poll.Success()
+		}
+
+		return poll.Continue("waiting for container to become %s", healthStatus)
+	}
+}
+
+func TestContainerList_HealthSummary(t *testing.T) {
+	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
+	ctx := setupTest(t)
+	testcases := []struct {
+		apiVersion string
+	}{
+		{apiVersion: "1.51"},
+		{apiVersion: "1.52"},
+	}
+
+	for _, tc := range testcases {
+		t.Run(fmt.Sprintf("run with version v%s", tc.apiVersion), func(t *testing.T) {
+			apiClient := request.NewAPIClient(t, client.WithVersion(tc.apiVersion))
+
+			cID := container.Run(ctx, t, apiClient, container.WithTty(true), container.WithWorkingDir("/foo"), func(c *container.TestContainerConfig) {
+				c.Config.Healthcheck = &containertypes.HealthConfig{
+					Test:     []string{"CMD-SHELL", "if [ \"$PWD\" = \"/foo\" ]; then exit 0; else exit 1; fi;"},
+					Interval: 50 * time.Millisecond,
+					Retries:  3,
+				}
+			})
+
+			poll.WaitOn(t, pollForHealthStatusSummary(ctx, apiClient, cID, containertypes.Healthy))
+		})
+	}
+}