mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-14 02:56:13 +05:00
fix: report remote-node containers correctly in stack monitoring
Free monitoring resolved containers via a local Dockerode listContainers() call, which only sees containers scheduled on the host running Dokploy. For Stack services with tasks on other Swarm nodes, this always fell into the "Container not running" branch even when the task was healthy elsewhere, since local docker.listContainers() can't see remote-node containers. Now falls back to `docker service ps` (Swarm-aggregated, works from any manager regardless of task placement) to tell a genuinely stopped container apart from one running on another node, and reports that distinction in the WS close reason instead of the misleading message. Also fixes a client-side race where the stats websocket connected with an empty appName on first mount (before the container selector settled), and surfaces the close reason as a toast instead of only logging it.
This commit is contained in:
parent
474081a264
commit
0efe7feaf5
@ -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();
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user