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 || "-"}
-
+ 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);