Merge pull request #5213 from Dokploy/fix/issue-5133-stack-monitoring-remote-node

fix: report remote-node containers correctly in stack monitoring
This commit is contained in:
Mauricio Siu
2026-08-28 16:19:47 -06:00
committed by GitHub
2 changed files with 61 additions and 2 deletions
@@ -1,5 +1,6 @@
import { formatMb } from "@dokploy/server/monitoring/units";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { api } from "@/utils/api";
@@ -167,6 +168,8 @@ export const ContainerFreeMonitoring = ({
}, [data]);
useEffect(() => {
if (!appName) return;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`;
const ws = new WebSocket(wsUrl);
@@ -196,7 +199,9 @@ export const ContainerFreeMonitoring = ({
};
ws.onclose = (e) => {
console.log(e.reason);
if (e.reason) {
toast.error(e.reason);
}
};
return () => ws.close();
+55 -1
View File
@@ -8,9 +8,57 @@ import {
recordAdvancedStats,
validateRequest,
} from "@dokploy/server";
import { quote } from "shell-quote";
import { WebSocketServer } from "ws";
import { canAccessDockerOverWss } from "./authorize";
type AppType = "application" | "stack" | "docker-compose";
// Swarm task names are "<service>.<slot>.<taskId>"; the manager's local
// `docker ps` only sees containers scheduled on this host, so a task running
// on another node looks identical to a stopped one. `docker service ps` is
// swarm-aggregated and answers from the manager regardless of which node the
// task landed on, so we use it here to tell the two cases apart.
const findRemoteSwarmNode = async (
appName: string,
appType: AppType,
): Promise<string | null> => {
if (appType === "docker-compose") {
return null;
}
// `docker service ps --format {{.Name}}` only prints "<service>.<slot>",
// never the taskId, so we match on the task ID instead — the last segment
// of the stack task name — rather than trying to reconstruct the full name.
const [serviceName, taskId] =
appType === "stack"
? [appName.split(".").slice(0, -2).join("."), appName.split(".").pop()]
: [appName, null];
if (!serviceName) {
return null;
}
try {
const { stdout } = await execAsync(
`docker service ps ${quote([serviceName])} --filter "desired-state=running" --no-trunc --format '{"ID":"{{.ID}}","Node":"{{.Node}}","CurrentState":"{{.CurrentState}}"}'`,
);
for (const line of stdout.trim().split("\n")) {
if (!line) continue;
const task = JSON.parse(line);
const isMatch = taskId ? task.ID === taskId : true;
if (isMatch && task.CurrentState?.startsWith("Running")) {
return task.Node;
}
}
} catch {
// Not a swarm service (or the swarm CLI isn't available) — fall back to "not running".
}
return null;
};
export const setupDockerStatsMonitoringSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
) => {
@@ -98,7 +146,13 @@ export const setupDockerStatsMonitoringSocketServer = (
const container = containers[0];
if (!container || container?.State !== "running") {
ws.close(4000, "Container not running");
const remoteNode = await findRemoteSwarmNode(appName, appType);
ws.close(
4000,
remoteNode
? `Container running on remote node "${remoteNode}"`.slice(0, 123)
: "Container not running",
);
return;
}
const { stdout, stderr } = await execAsync(