Repository: portainer/portainer

Issue:
Container Health Status Not Displayed Correctly in Dashboard
### Before you start please confirm the following.

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/portainer/portainer/issues).
- [x] Yes, I've checked whether this issue is covered in the Portainer [documentation](https://docs.portainer.io) or [knowledge base](https://portal.portainer.io/knowledge).

### Problem Description

I have a container with a health status set to "healthy". However, this status is not correctly reflected in the Portainer dashboard.

![image](https://github.com/user-attachments/assets/19875d4a-8243-4815-8a45-d22d31c57f3d)

![image](https://github.com/user-attachments/assets/5acb992a-3cb0-41ac-ad90-8914dfa3c5c4)


### Expected Behavior

In the dashboard, it should display 1 for "healthy":

![image](https://github.com/user-attachments/assets/e32cfdf2-1217-4fa5-8900-a93c297f22f4)

### Actual Behavior

It displays 0 for "healthy"

### Steps to Reproduce

1. Create and start a container with a functioning health check.
2. Verify if the health status is accurately displayed in the Portainer dashboard.

### Portainer logs or screenshots

_No response_

### Portainer version

2.22.0

### Portainer Edition

Community Edition (CE)

### Platform and Version

Podman 4.9.3

### OS and Architecture

Ubuntu 24.04 LTS

### Browser

_No response_

### What command did you use to deploy Portainer?

```bash
podman run -d -p 9443:9443 -p 9000:9000 --name=portainer --restart=always -v /run/user/$(id -u)/podman/podman.sock:/var/run/docker.sock:Z  -v portainer_data:/data docker.io/portainer/portainer-ce:2.22.0
```


### Additional Information

_No response_

Hints from issue comments:
@DiscreetG33k: I'm seeing this as well in v2.22.0 in both Portainer and edge agents.@gorucci: Hi.
I've encountered similar issue.
I have 2 environments: local and edge agent. Both of them are showing the correct number of healthy containers (2 in each environment) at their dashboards. 
However, the home page is showing 0 for healthy counter.

I'm using `portainer/portainer-ce:2.21.4-alpine` and `portainer/agent:2.21.4`. Each environment is a VM running `ubuntu-server 24.04.1`.

P.s. I can create separate issue in case you guys think they are not related.@gorucci: My issue is already been reported -- #10508.@wookiesh: I have the same issue with my setup: 2 machines, one running portainer version 2.21.4 and the second running portainer-agent:latest.

@Adagis-MC: Issue also occurs on 2.24.0:

![image](https://github.com/user-attachments/assets/601fc6a7-1b00-4f09-b4cf-890cae5e677b)
Dashboard

![image](https://github.com/user-attachments/assets/1eb6f6cc-7115-4475-9aff-428b7749e6a2)
Containers@RafikiHome: I just upgraded to 2.25.1 STS and the problem still persists. I see the other ticket ( https://github.com/portainer/portainer/issues/10508 ) referred to above describes a slightly different problem. Is the issue that incorrect (0) healthy containers reported still open, or must a ticket be raised ? 


![Screenshot 2024-12-28 102534](https://github.com/user-attachments/assets/63c64754-6131-492f-bdb2-560a5889f322)


![Screenshot 2024-12-28 102752](https://github.com/user-attachments/assets/c20933df-950a-49c3-95f8-ed8adf4cb0ce)
@OdinVex: As I'm migrating away from Portainer I'm still developing an in-house alternative and came across the same issue. BE:2.26.0 STS.@lowlyocean: Still an issue with 2.27.1 LTS@supaeasy: I have the same Issue ever since I upgraded my docker version@Nick-Portainer: Hello, I have raised this internally.

```internal: BE-2290```@spl33f: Hello,
I have the same issue:

![Image](https://github.com/user-attachments/assets/94e67a79-86ec-41a5-9ba7-abeec5b65338)

I have 18 healthy containers out of 25.
7 in running state.

Did you manage to fix it?

Thank you
@Heliospeed: I exactly the same problem (Portainer Business Edition 2.2.7.3 LTS running Docker version 28.0.4, build b8034c0).

Thank you for correcting this.@BigCrustyOne: Same issue with Portainer Business Edition 2.2.7.3 LTS running Docker version 27.3.1, build 2.fc41. @khouahabi: hello, I'm using Portainer 2.27.3 LTS. I have the same issue @Heliospeed: Hello, it’s same with 2.27.4 LTS. @supaeasy: Will this ever be adressed?@RafaelUR1: The issue persist in Portainer Business Edition 2.27.4 LTS.
Can we expect a solution ?@Nick-Portainer: Hello, we are working on this issue at the moment but there is no ETA I can provide at this time.@gkurbesov: Hi! it’s same with 2.27.9 LTS.

<img width="1285" height="248" alt="Image" src="https://github.com/user-attachments/assets/8bf726bb-a789-4e42-9ee7-5f55180d51c9" />

<img width="485" height="890" alt="Image" src="https://github.com/user-attachments/assets/c40ea7d6-e1c4-4c92-a45b-d35acf82251a" />@gkurbesov: @Nick-Portainer Can you provide update for fix this bug?@j0nny55555: this is still an issue, for the main Portainer and side connected Portainer-Agent environments
running 2.27.9 LTS@UMDTERPS: I am still having the same issue with one of the latest versions of Portainer. Statues are not properly displayed, "exited" ( i guess would be stopped) and "restarted" states don't show on the dashabord of the container. @khouahabi: i update to 2.33.0 LTS -CE , and still not fixed :( yet@dergreg: I guess I got it fixed, my assumptions from above were all correct.

@Nick-Portainer please have a review with the team, the issue has been open for more than a year now, thanks :)@dergreg: @oscarzhou-portainer thanks for merging! Can you please assign a milestone here and close the issue afterwards?@oscarzhou-portainer: We don’t use milestones in our workflow, but the corresponding internal ticket has been tagged for the 2.34 release. Our developer needs to sync the changes to the BE version, and once that’s done, I’ll close the issue.@gkurbesov: > We don’t use milestones in our workflow, but the corresponding internal ticket has been tagged for the 2.34 release. Our developer needs to sync the changes to the BE version, and once that’s done, I’ll close the issue.

@oscarzhou-portainer Thanks for the update! Could you please share the expected timeline for the 2.34 release, so we know when the changes might become available?@oscarzhou-portainer: Hey @gkurbesov , I just got confirmation. The release date for 2.34 will be Sep. 18th. @Nick-Portainer: Patched in the latest release 2.34.0 STS

Solution patch:
diff --git a/api/docker/container_stats.go b/api/docker/stats/container_stats.go
similarity index 64%
rename from api/docker/container_stats.go
rename to api/docker/stats/container_stats.go
index 321d2e22c..bf3dc532e 100644
--- a/api/docker/container_stats.go
+++ b/api/docker/stats/container_stats.go
@@ -1,6 +1,11 @@
-package docker
+package stats
 
-import "github.com/docker/docker/api/types"
+import (
+	"strings"
+
+	"github.com/docker/docker/api/types"
+	"github.com/rs/zerolog/log"
+)
 
 type ContainerStats struct {
 	Running   int `json:"running"`
@@ -13,18 +18,20 @@ type ContainerStats struct {
 func CalculateContainerStats(containers []types.Container) ContainerStats {
 	var running, stopped, healthy, unhealthy int
 	for _, container := range containers {
+		log.Debug().Str("containerId", container.ID).Str("state", container.State).Str("status", container.Status).Msg("Container info")
+
 		switch container.State {
 		case "running":
 			running++
-		case "healthy":
-			running++
-			healthy++
-		case "unhealthy":
-			running++
-			unhealthy++
 		case "exited", "stopped":
 			stopped++
 		}
+
+		if strings.Contains(container.Status, "(healthy)") {
+			healthy++
+		} else if strings.Contains(container.Status, "(unhealthy)") {
+			unhealthy++
+		}
 	}
 
 	return ContainerStats{
diff --git a/api/http/handler/docker/dashboard.go b/api/http/handler/docker/dashboard.go
index c220106b7..f872ba586 100644
--- a/api/http/handler/docker/dashboard.go
+++ b/api/http/handler/docker/dashboard.go
@@ -11,7 +11,7 @@ import (
 	"github.com/docker/docker/api/types/volume"
 	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/dataservices"
-	"github.com/portainer/portainer/api/docker"
+	"github.com/portainer/portainer/api/docker/stats"
 	"github.com/portainer/portainer/api/http/errors"
 	"github.com/portainer/portainer/api/http/handler/docker/utils"
 	"github.com/portainer/portainer/api/http/middlewares"
@@ -26,12 +26,12 @@ type imagesCounters struct {
 }
 
 type dashboardResponse struct {
-	Containers docker.ContainerStats `json:"containers"`
-	Services   int                   `json:"services"`
-	Images     imagesCounters        `json:"images"`
-	Volumes    int                   `json:"volumes"`
-	Networks   int                   `json:"networks"`
-	Stacks     int                   `json:"stacks"`
+	Containers stats.ContainerStats `json:"containers"`
+	Services   int                  `json:"services"`
+	Images     imagesCounters       `json:"images"`
+	Volumes    int                  `json:"volumes"`
+	Networks   int                  `json:"networks"`
+	Stacks     int                  `json:"stacks"`
 }
 
 // @id dockerDashboard
@@ -150,7 +150,7 @@ func (h *Handler) dashboard(w http.ResponseWriter, r *http.Request) *httperror.H
 				Size:  totalSize,
 			},
 			Services:   len(services),
-			Containers: docker.CalculateContainerStats(containers),
+			Containers: stats.CalculateContainerStats(containers),
 			Networks:   len(networks),
 			Volumes:    len(volumes),
 			Stacks:     stackCount,
diff --git a/app/react/docker/DashboardView/ContainerStatus.tsx b/app/react/docker/DashboardView/ContainerStatus.tsx
index 2c22bd181..09ee39cdf 100644
--- a/app/react/docker/DashboardView/ContainerStatus.tsx
+++ b/app/react/docker/DashboardView/ContainerStatus.tsx
@@ -30,7 +30,7 @@ export function ContainerStatus({ stats }: Props) {
           {stats.healthy} healthy
         </div>
         <div className="vertical-center space-right">
-          <Icon icon={Heart} mode="danger" size="sm" />
+          <Icon icon={Heart} mode="warning" size="sm" />
           {stats.unhealthy} unhealthy
         </div>
       </div>
diff --git a/pkg/snapshot/docker.go b/pkg/snapshot/docker.go
index 12f3a3089..a9d8dce33 100644
--- a/pkg/snapshot/docker.go
+++ b/pkg/snapshot/docker.go
@@ -10,6 +10,7 @@ import (
 
 	portainer "github.com/portainer/portainer/api"
 	"github.com/portainer/portainer/api/docker/consts"
+	"github.com/portainer/portainer/api/docker/stats"
 	edgeutils "github.com/portainer/portainer/pkg/edge"
 	networkingutils "github.com/portainer/portainer/pkg/networking"
 
@@ -207,7 +208,7 @@ func dockerSnapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Cl
 	snapshot.GpuUseAll = gpuUseAll
 	snapshot.GpuUseList = gpuUseList
 
-	stats := calculateContainerStats(containers)
+	stats := stats.CalculateContainerStats(containers)
 
 	snapshot.ContainerCount = stats.Total
 	snapshot.RunningContainerCount = stats.Running
@@ -344,37 +345,3 @@ func isPodman(version types.Version) bool {
 
 	return false
 }
-
-type ContainerStats struct {
-	Running   int
-	Stopped   int
-	Healthy   int
-	Unhealthy int
-	Total     int
-}
-
-func calculateContainerStats(containers []types.Container) ContainerStats {
-	var running, stopped, healthy, unhealthy int
-	for _, container := range containers {
-		switch container.State {
-		case "running":
-			running++
-		case "healthy":
-			running++
-			healthy++
-		case "unhealthy":
-			running++
-			unhealthy++
-		case "exited", "stopped":
-			stopped++
-		}
-	}
-
-	return ContainerStats{
-		Running:   running,
-		Stopped:   stopped,
-		Healthy:   healthy,
-		Unhealthy: unhealthy,
-		Total:     len(containers),
-	}
-}



Test patch:
diff --git a/api/docker/container_stats_test.go b/api/docker/stats/container_stats_test.go
similarity index 72%
rename from api/docker/container_stats_test.go
rename to api/docker/stats/container_stats_test.go
index 5422a276c..1c7d5e0a8 100644
--- a/api/docker/container_stats_test.go
+++ b/api/docker/stats/container_stats_test.go
@@ -1,4 +1,4 @@
-package docker
+package stats
 
 import (
 	"testing"
@@ -10,11 +10,11 @@ import (
 func TestCalculateContainerStats(t *testing.T) {
 	containers := []types.Container{
 		{State: "running"},
-		{State: "running"},
+		{State: "running", Status: "Up 5 minutes (healthy)"},
 		{State: "exited"},
 		{State: "stopped"},
-		{State: "healthy"},
-		{State: "unhealthy"},
+		{State: "running", Status: "Up 10 minutes"},
+		{State: "running", Status: "Up about an hour (unhealthy)"},
 	}
 
 	stats := CalculateContainerStats(containers)