From a5b86263458b43261ae023cf1eaeaf2ea92f7ae4 Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Tue, 18 Aug 2026 17:24:15 +0700 Subject: [PATCH 1/6] fix: preserve selected stack log container --- .../dashboard/compose/logs/show-stack.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx index a7b4d3014..8b46b0666 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -72,16 +72,19 @@ export const ShowDockerLogsStack = ({ const containers = data?.filter((container) => container.containerId); useEffect(() => { - if (option === "native") { - if (containers && containers?.length > 0) { - setContainerId(containers[0]?.containerId); - } - } else { - if (services && services?.length > 0) { - setContainerId(services[0]?.containerId); - } + const currentContainers = option === "native" ? containers : services; + + if ( + currentContainers && + currentContainers.length > 0 && + (!containerId || + !currentContainers.some( + (container) => container.containerId === containerId, + )) + ) { + setContainerId(currentContainers[0]?.containerId); } - }, [option, services, containers]); + }, [option, services, containers, containerId]); const isLoading = option === "native" ? containersLoading : servicesLoading; const containersLength = From c77f6d889d088fff01790db2d59c107c1abdb4e5 Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Tue, 18 Aug 2026 17:34:18 +0700 Subject: [PATCH 2/6] fix: clear stale stack log container selection --- .../dashboard/compose/logs/show-stack.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx index 8b46b0666..68b0868fc 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -74,15 +74,16 @@ export const ShowDockerLogsStack = ({ useEffect(() => { const currentContainers = option === "native" ? containers : services; - if ( - currentContainers && - currentContainers.length > 0 && - (!containerId || - !currentContainers.some( - (container) => container.containerId === containerId, - )) - ) { - setContainerId(currentContainers[0]?.containerId); + if (currentContainers) { + const nextContainerId = currentContainers.some( + (container) => container.containerId === containerId, + ) + ? containerId + : currentContainers[0]?.containerId; + + if (nextContainerId !== containerId) { + setContainerId(nextContainerId); + } } }, [option, services, containers, containerId]); From 2221b8a99cb70d85a7a7527540f64151ca0c0bc9 Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Tue, 18 Aug 2026 17:47:59 +0700 Subject: [PATCH 3/6] fix: preserve stack log selection on fetch failures --- .../services/docker-stack-containers.test.ts | 69 +++++++++++++++++++ packages/server/src/services/docker.ts | 10 +-- 2 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 apps/dokploy/__test__/services/docker-stack-containers.test.ts diff --git a/apps/dokploy/__test__/services/docker-stack-containers.test.ts b/apps/dokploy/__test__/services/docker-stack-containers.test.ts new file mode 100644 index 000000000..b8d06b4f9 --- /dev/null +++ b/apps/dokploy/__test__/services/docker-stack-containers.test.ts @@ -0,0 +1,69 @@ +import { + getContainersByAppNameMatch, + getStackContainersByAppName, +} from "@dokploy/server/services/docker"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@dokploy/server/utils/process/execAsync", async (importOriginal) => ({ + ...(await importOriginal< + typeof import("@dokploy/server/utils/process/execAsync") + >()), + execAsync: vi.fn(), + execAsyncRemote: vi.fn(), +})); + +const execAsyncMock = vi.mocked(execAsync); +const execAsyncRemoteMock = vi.mocked(execAsyncRemote); + +describe("stack container lookup results", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it("returns undefined when the native stack lookup fails", async () => { + execAsyncMock.mockRejectedValueOnce(new Error("Docker is unavailable")); + + await expect( + getContainersByAppNameMatch("farmgate-odoo", "stack"), + ).resolves.toBeUndefined(); + }); + + it("returns an empty list for a successful native lookup with no tasks", async () => { + execAsyncMock.mockResolvedValueOnce({ stdout: "", stderr: "" }); + + await expect( + getContainersByAppNameMatch("farmgate-odoo", "stack"), + ).resolves.toEqual([]); + }); + + it("returns undefined when the remote native stack lookup fails", async () => { + execAsyncRemoteMock.mockRejectedValueOnce(new Error("SSH is unavailable")); + + await expect( + getContainersByAppNameMatch("farmgate-odoo", "stack", "server-1"), + ).resolves.toBeUndefined(); + }); + + it("returns undefined when the Swarm stack lookup reports an error", async () => { + execAsyncRemoteMock.mockResolvedValueOnce({ + stdout: "", + stderr: "node is unavailable", + }); + + await expect( + getStackContainersByAppName("farmgate-odoo", "server-1"), + ).resolves.toBeUndefined(); + }); + + it("returns an empty list for a successful Swarm lookup with no tasks", async () => { + execAsyncMock.mockResolvedValueOnce({ stdout: "", stderr: "" }); + + await expect(getStackContainersByAppName("farmgate-odoo")).resolves.toEqual( + [], + ); + }); +}); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 65eb29041..72d81fc3d 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -167,7 +167,7 @@ export const getContainersByAppNameMatch = async ( return containers || []; } catch {} - return []; + return appType === "stack" ? undefined : []; }; const getStackTaskContainers = async (appName: string, serverId?: string) => { @@ -227,7 +227,7 @@ const getStackTaskContainers = async (appName: string, serverId?: string) => { return containers; } catch {} - return []; + return undefined; }; export const getStackContainersByAppName = async ( @@ -243,7 +243,7 @@ export const getStackContainersByAppName = async ( const { stdout, stderr } = await execAsyncRemote(serverId, command); if (stderr) { - return []; + return undefined; } if (!stdout) return []; @@ -252,7 +252,7 @@ export const getStackContainersByAppName = async ( const { stdout, stderr } = await execAsync(command); if (stderr) { - return []; + return undefined; } if (!stdout) return []; @@ -292,7 +292,7 @@ export const getStackContainersByAppName = async ( return containers || []; } catch {} - return []; + return undefined; }; export const getServiceContainersByAppName = async ( From 49055a05b17bbe26543cf18a2e958bc14b2b03ad Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Tue, 18 Aug 2026 17:52:46 +0700 Subject: [PATCH 4/6] fix: surface native stack inspection failures --- .../dokploy/__test__/services/docker-stack-containers.test.ts | 4 ++++ packages/server/src/services/docker.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/__test__/services/docker-stack-containers.test.ts b/apps/dokploy/__test__/services/docker-stack-containers.test.ts index b8d06b4f9..c4de40613 100644 --- a/apps/dokploy/__test__/services/docker-stack-containers.test.ts +++ b/apps/dokploy/__test__/services/docker-stack-containers.test.ts @@ -30,6 +30,10 @@ describe("stack container lookup results", () => { await expect( getContainersByAppNameMatch("farmgate-odoo", "stack"), ).resolves.toBeUndefined(); + + const command = execAsyncMock.mock.calls[0]?.[0]; + expect(command).toBeDefined(); + expect(command).not.toContain("|| true"); }); it("returns an empty list for a successful native lookup with no tasks", async () => { diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 72d81fc3d..cda350e63 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -175,7 +175,7 @@ const getStackTaskContainers = async (appName: string, serverId?: string) => { 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)`; + const command = `${tasksCommand} && echo "${divider}" && ${inspectCommand}`; let stdout = ""; From 60612b098602688b726eb6f6b914a2920a3b648f Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Tue, 18 Aug 2026 22:10:54 +0700 Subject: [PATCH 5/6] fix: reset stack log selection when switching modes --- apps/dokploy/components/dashboard/compose/logs/show-stack.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx index 68b0868fc..558dc1c0f 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -110,6 +110,7 @@ export const ShowDockerLogsStack = ({ { + setContainerId(undefined); setOption(checked ? "native" : "swarm"); }} /> From 0be822ef5b1b074d42e16e3f3ee2bdd8fd0e44d5 Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Wed, 19 Aug 2026 10:33:49 +0700 Subject: [PATCH 6/6] refactor: keep stack log fix focused --- .../services/docker-stack-containers.test.ts | 73 ------------------- packages/server/src/services/docker.ts | 12 +-- 2 files changed, 6 insertions(+), 79 deletions(-) delete mode 100644 apps/dokploy/__test__/services/docker-stack-containers.test.ts diff --git a/apps/dokploy/__test__/services/docker-stack-containers.test.ts b/apps/dokploy/__test__/services/docker-stack-containers.test.ts deleted file mode 100644 index c4de40613..000000000 --- a/apps/dokploy/__test__/services/docker-stack-containers.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - getContainersByAppNameMatch, - getStackContainersByAppName, -} from "@dokploy/server/services/docker"; -import { - execAsync, - execAsyncRemote, -} from "@dokploy/server/utils/process/execAsync"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("@dokploy/server/utils/process/execAsync", async (importOriginal) => ({ - ...(await importOriginal< - typeof import("@dokploy/server/utils/process/execAsync") - >()), - execAsync: vi.fn(), - execAsyncRemote: vi.fn(), -})); - -const execAsyncMock = vi.mocked(execAsync); -const execAsyncRemoteMock = vi.mocked(execAsyncRemote); - -describe("stack container lookup results", () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it("returns undefined when the native stack lookup fails", async () => { - execAsyncMock.mockRejectedValueOnce(new Error("Docker is unavailable")); - - await expect( - getContainersByAppNameMatch("farmgate-odoo", "stack"), - ).resolves.toBeUndefined(); - - const command = execAsyncMock.mock.calls[0]?.[0]; - expect(command).toBeDefined(); - expect(command).not.toContain("|| true"); - }); - - it("returns an empty list for a successful native lookup with no tasks", async () => { - execAsyncMock.mockResolvedValueOnce({ stdout: "", stderr: "" }); - - await expect( - getContainersByAppNameMatch("farmgate-odoo", "stack"), - ).resolves.toEqual([]); - }); - - it("returns undefined when the remote native stack lookup fails", async () => { - execAsyncRemoteMock.mockRejectedValueOnce(new Error("SSH is unavailable")); - - await expect( - getContainersByAppNameMatch("farmgate-odoo", "stack", "server-1"), - ).resolves.toBeUndefined(); - }); - - it("returns undefined when the Swarm stack lookup reports an error", async () => { - execAsyncRemoteMock.mockResolvedValueOnce({ - stdout: "", - stderr: "node is unavailable", - }); - - await expect( - getStackContainersByAppName("farmgate-odoo", "server-1"), - ).resolves.toBeUndefined(); - }); - - it("returns an empty list for a successful Swarm lookup with no tasks", async () => { - execAsyncMock.mockResolvedValueOnce({ stdout: "", stderr: "" }); - - await expect(getStackContainersByAppName("farmgate-odoo")).resolves.toEqual( - [], - ); - }); -}); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index cda350e63..65eb29041 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -167,7 +167,7 @@ export const getContainersByAppNameMatch = async ( return containers || []; } catch {} - return appType === "stack" ? undefined : []; + return []; }; const getStackTaskContainers = async (appName: string, serverId?: string) => { @@ -175,7 +175,7 @@ const getStackTaskContainers = async (appName: string, serverId?: string) => { 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}`; + const command = `${tasksCommand} && echo "${divider}" && (${inspectCommand} || true)`; let stdout = ""; @@ -227,7 +227,7 @@ const getStackTaskContainers = async (appName: string, serverId?: string) => { return containers; } catch {} - return undefined; + return []; }; export const getStackContainersByAppName = async ( @@ -243,7 +243,7 @@ export const getStackContainersByAppName = async ( const { stdout, stderr } = await execAsyncRemote(serverId, command); if (stderr) { - return undefined; + return []; } if (!stdout) return []; @@ -252,7 +252,7 @@ export const getStackContainersByAppName = async ( const { stdout, stderr } = await execAsync(command); if (stderr) { - return undefined; + return []; } if (!stdout) return []; @@ -292,7 +292,7 @@ export const getStackContainersByAppName = async ( return containers || []; } catch {} - return undefined; + return []; }; export const getServiceContainersByAppName = async (