From 1f73ebd0b0f89e827e3294d044a46ac7a29c7539 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 9 Aug 2026 11:37:19 -0600 Subject: [PATCH] fix: list stack containers across swarm nodes via docker stack ps --- .../containers/show-compose-containers.tsx | 184 ++++++++++-------- .../dashboard/compose/logs/show-stack.tsx | 4 +- packages/server/src/services/docker.ts | 64 +++++- 3 files changed, 165 insertions(+), 87 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/containers/show-compose-containers.tsx b/apps/dokploy/components/dashboard/compose/containers/show-compose-containers.tsx index 460223fa5..18ff35738 100644 --- a/apps/dokploy/components/dashboard/compose/containers/show-compose-containers.tsx +++ b/apps/dokploy/components/dashboard/compose/containers/show-compose-containers.tsx @@ -114,6 +114,7 @@ export const ShowComposeContainers = ({ Name State Status + {appType === "stack" && Node} Container ID @@ -121,8 +122,9 @@ export const ShowComposeContainers = ({ {data.map((container) => ( refetch()} @@ -143,7 +145,9 @@ interface ContainerRowProps { name: string; state: string; status: string; + node?: string; }; + appType: "stack" | "docker-compose"; serverId?: string; serviceId?: string; onActionComplete: () => void; @@ -151,6 +155,7 @@ interface ContainerRowProps { const ContainerRow = ({ container, + appType, serverId, serviceId, onActionComplete, @@ -192,7 +197,13 @@ const ContainerRow = ({ variant={ container.state === "running" ? "default" - : container.state === "exited" + : [ + "exited", + "pending", + "preparing", + "starting", + "ready", + ].includes(container.state) ? "secondary" : "destructive" } @@ -201,96 +212,99 @@ const ContainerRow = ({ {container.status} + {appType === "stack" && {container.node || "-"}} - {container.containerId} + {container.containerId || "-"} - - - - - - - Actions - + {!container.containerId ? null : ( + + + + + + + Actions + + e.preventDefault()} + > + View Logs + + + + + + + Terminal + + e.preventDefault()} + disabled={actionLoading !== null} + onClick={() => handleAction("restart", restartMutation)} > - View Logs + Restart - - - - - - Terminal - - - handleAction("restart", restartMutation)} - > - Restart - - handleAction("start", startMutation)} - > - Start - - handleAction("stop", stopMutation)} - > - Stop - - handleAction("kill", killMutation)} - > - Kill - - - - - - View Logs - Logs for {container.name} - -
- -
-
-
+ handleAction("start", startMutation)} + > + Start + + handleAction("stop", stopMutation)} + > + Stop + + handleAction("kill", killMutation)} + > + Kill + + + + + + View Logs + Logs for {container.name} + +
+ +
+
+ + )}
); diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx index 23bb4536d..a7b4d3014 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -57,7 +57,7 @@ export const ShowDockerLogsStack = ({ }, ); - const { data: containers, isPending: containersLoading } = + const { data, isPending: containersLoading } = api.docker.getContainersByAppNameMatch.useQuery( { appName, @@ -69,6 +69,8 @@ export const ShowDockerLogsStack = ({ }, ); + const containers = data?.filter((container) => container.containerId); + useEffect(() => { if (option === "native") { if (containers && containers?.length > 0) { diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index ea200e0c0..e95a84392 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -108,6 +108,9 @@ export const getContainersByAppNameMatch = async ( serverId?: string, ) => { try { + if (appType === "stack") { + return await getStackTaskContainers(appName, serverId); + } let result: string[] = []; const cmd = "docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}} | Status: {{.Status}}'"; @@ -166,6 +169,66 @@ export const getContainersByAppNameMatch = async ( return []; }; +const getStackTaskContainers = async (appName: string, serverId?: string) => { + try { + const divider = "__DOKPLOY_DIVIDER__"; + const tasksCommand = `docker stack ps ${appName} --no-trunc --filter "desired-state=running" --format 'TASK : {{.ID}} | Name: {{.Name}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`; + const inspectCommand = `docker stack ps ${appName} -q --no-trunc --filter "desired-state=running" | xargs -r docker inspect --format '{{if .Status.ContainerStatus}}TASK : {{.ID}} | ContainerId: {{.Status.ContainerStatus.ContainerID}}{{end}}' 2>/dev/null`; + const command = `${tasksCommand} && echo "${divider}" && (${inspectCommand} || true)`; + + let stdout = ""; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + } else { + const result = await execAsync(command); + stdout = result.stdout; + } + + if (!stdout) return []; + + const [tasksSection = "", inspectSection = ""] = stdout.split(divider); + + const containerIdByTask = new Map(); + for (const line of inspectSection.trim().split("\n")) { + const parts = line.split(" | "); + const taskId = parts[0]?.replace("TASK : ", "").trim(); + const containerId = parts[1]?.replace("ContainerId: ", "").trim(); + if (taskId && containerId) { + containerIdByTask.set(taskId, containerId); + } + } + + const containers = []; + + for (const line of tasksSection.trim().split("\n")) { + if (!line) continue; + const parts = line.split(" | "); + const taskId = parts[0]?.replace("TASK : ", "").trim() ?? ""; + const name = parts[1]?.replace("Name: ", "").trim() ?? ""; + const node = parts[2]?.replace("Node: ", "").trim() ?? ""; + const currentState = parts[3] + ? parts[3].replace("CurrentState: ", "").trim() + : ""; + const error = parts[4] ? parts[4].replace("Error: ", "").trim() : ""; + const containerId = containerIdByTask.get(taskId) ?? ""; + + containers.push({ + containerId: containerId.slice(0, 12), + name: `${name}.${taskId}`, + node, + state: currentState.split(" ")[0]?.toLowerCase() ?? "No state", + status: error ? `${currentState} (${error})` : currentState, + }); + } + + return containers; + } catch {} + + return []; +}; + export const getStackContainersByAppName = async ( appName: string, serverId?: string, @@ -175,7 +238,6 @@ export const getStackContainersByAppName = async ( const command = `docker stack ps ${appName} --no-trunc --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}} | CurrentState: {{.CurrentState}} | Error: {{.Error}}'`; - console.log("command ", command); if (serverId) { const { stdout, stderr } = await execAsyncRemote(serverId, command);