mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-01 20:15:29 +02:00
feat(swarm): add container breakdown by node with live metrics
TL;DR: New "Containers" tab on the Swarm page showing which containers
run on which nodes, with live CPU/Memory/Block I/O/Network I/O metrics
refreshing every 5 seconds. Comprehensive edge case handling guides
users through prerequisites (swarm init, registry, service deployment).
---
## New Files
- `apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx`
Main component: data fetching, error/empty states, summary cards,
and the node-grouped container layout.
- `apps/dokploy/components/dashboard/swarm/containers/node-section.tsx`
Collapsible per-node section with container table, role badge,
and down-node indicator.
- `apps/dokploy/components/dashboard/swarm/containers/container-row.tsx`
Table row for a single container: state badge with error tooltip,
formatted CPU/memory/IO metrics.
- `apps/dokploy/components/dashboard/swarm/containers/utils.ts`
Formatting helpers for docker stats values (CPU %, memory, I/O).
- `apps/dokploy/components/dashboard/swarm/containers/types.ts`
Shared TypeScript interfaces (ContainerStat, ContainerInfo, SwarmNode,
NodeGroup).
## Modified Files
- `apps/dokploy/pages/dashboard/swarm.tsx`
Added Tabs (Overview / Containers) wrapping existing SwarmMonitorCard
and the new ShowSwarmContainers component.
- `apps/dokploy/server/api/routers/swarm.ts`
Added `getContainerStats` tRPC endpoint calling `getAllContainerStats`,
following existing auth/validation patterns.
- `packages/server/src/services/docker.ts`
- Added `getAllContainerStats()` — runs `docker stats --no-stream` for
cluster-wide container metrics.
- Fixed `getSwarmNodes`, `getNodeApplications`, `getApplicationInfo` to
return `[]` instead of `undefined` on errors (prevents tRPC
serialization crashes) and added `console.error` logging.
- Added empty stdout guard (`if (!stdout.trim()) return []`) to prevent
`JSON.parse("")` crashes when no services exist.
## Features
- Container table per node: name, image, state, CPU %, memory usage,
block I/O, and network I/O
- Resource formatting: values rounded to 1 decimal (2.711MiB → 2.7 MiB),
CPU to 1 decimal (0.00% → 0.0%)
- Node role badges (Leader / Reachable / Worker) on each section header
- Error tooltips: hover the status badge to see Docker error details
- Down/drained node detection with red indicator dot and warning banner
- Multi-node metrics banner explaining docker stats manager-only limitation
- Unscheduled services footer for services scaled to 0 replicas
- Contextual empty/error states with actionable guidance, doc links to
Dokploy docs and Docker Swarm guide, and links to Cluster Settings
## Edge Cases Handled
1. Swarm not initialized (tRPC error or undefined data)
2. Docker command failures (stderr / non-zero exit)
3. Swarm active but no services deployed
4. Services exist but no running containers
5. Containers with Docker errors (shown in tooltip + error alert)
6. Nodes down or drained (cross-referenced from node list)
7. Multi-node setups (metrics only from manager node)
8. Services scaled to 0 replicas (separated from running containers)
9. Empty stdout from docker commands (no JSON.parse crash)
This commit is contained in:
@@ -371,7 +371,11 @@ export const getSwarmNodes = async (serverId?: string) => {
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodesArray = stdout
|
||||
@@ -379,7 +383,10 @@ export const getSwarmNodes = async (serverId?: string) => {
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
return nodesArray;
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error("getSwarmNodes error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||
@@ -430,6 +437,10 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
@@ -437,7 +448,10 @@ export const getNodeApplications = async (serverId?: string) => {
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
|
||||
return appArray;
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error("getNodeApplications error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getApplicationInfo = async (
|
||||
@@ -464,11 +478,48 @@ export const getApplicationInfo = async (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
console.error("getApplicationInfo error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllContainerStats = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
const command =
|
||||
"docker stats --no-stream --format '{\"BlockIO\":\"{{.BlockIO}}\",\"CPUPerc\":\"{{.CPUPerc}}\",\"Container\":\"{{.Container}}\",\"ID\":\"{{.ID}}\",\"MemPerc\":\"{{.MemPerc}}\",\"MemUsage\":\"{{.MemUsage}}\",\"Name\":\"{{.Name}}\",\"NetIO\":\"{{.NetIO}}\"}'";
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
}
|
||||
|
||||
if (!stdout.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stats = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return stats;
|
||||
} catch (error) {
|
||||
console.error("getAllContainerStats error:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user