diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx
index 6dc5cd90c..0864834b8 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx
@@ -1,103 +1,106 @@
import { format } from "date-fns";
+import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
import {
- Area,
- AreaChart,
- CartesianGrid,
- Legend,
- ResponsiveContainer,
- Tooltip,
- YAxis,
-} from "recharts";
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["block"];
}
+const chartConfig = {
+ readMb: {
+ label: "Read (MB)",
+ color: "hsl(var(--chart-1))",
+ },
+ writeMb: {
+ label: "Write (MB)",
+ color: "hsl(var(--chart-2))",
+ },
+} satisfies ChartConfig;
+
export const DockerBlockChart = ({ accumulativeData }: Props) => {
- const transformedData = accumulativeData.map((item, index) => {
- return {
- time: item.time,
- name: `Point ${index + 1}`,
- readMb: item.value.readMb,
- writeMb: item.value.writeMb,
- };
- });
+ const transformedData = accumulativeData.map((item, index) => ({
+ time: item.time,
+ name: `Point ${index + 1}`,
+ readMb: item.value.readMb,
+ writeMb: item.value.writeMb,
+ }));
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* @ts-ignore */}
- } />
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const time = payload?.[0]?.payload?.time;
+ return time
+ ? format(new Date(time), "PPpp")
+ : "";
+ }}
+ formatter={(value, name) => {
+ const label =
+ name === "readMb" ? "Read" : "Write";
+ return [`${value} MB`, label];
+ }}
+ />
+ }
+ />
+
+
+ } />
+
+
);
};
-interface CustomTooltipProps {
- active: boolean;
- payload?: {
- color?: string;
- dataKey?: string;
- value?: number;
- payload: {
- time: string;
- readMb: number;
- writeMb: number;
- };
- }[];
-}
-
-const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
- if (active && payload && payload.length && payload[0]) {
- return (
-
- {payload[0].payload.time && (
-
{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}
- )}
-
{`Read ${payload[0].payload.readMb} `}
-
{`Write: ${payload[0].payload.writeMb} `}
-
- );
- }
-
- return null;
-};
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx
index 67404268b..407eb6bf8 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx
@@ -1,87 +1,83 @@
import { format } from "date-fns";
+import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
import {
- Area,
- AreaChart,
- CartesianGrid,
- Legend,
- ResponsiveContainer,
- Tooltip,
- YAxis,
-} from "recharts";
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["cpu"];
}
+const chartConfig = {
+ usage: {
+ label: "CPU Usage",
+ color: "hsl(var(--chart-1))",
+ },
+} satisfies ChartConfig;
+
export const DockerCpuChart = ({ accumulativeData }: Props) => {
- const transformedData = accumulativeData.map((item, index) => {
- return {
- name: `Point ${index + 1}`,
- time: item.time,
- usage: item.value.toString().split("%")[0],
- };
- });
+ const transformedData = accumulativeData.map((item, index) => ({
+ name: `Point ${index + 1}`,
+ time: item.time,
+ usage: item.value.toString().split("%")[0],
+ }));
+
return (
-
-
-
-
-
-
-
-
-
-
-
- {/* @ts-ignore */}
- } />
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ `${value}%`}
+ domain={[0, 100]}
+ tickLine={false}
+ axisLine={false}
+ />
+ {
+ const time = payload?.[0]?.payload?.time;
+ return time
+ ? format(new Date(time), "PPpp")
+ : "";
+ }}
+ formatter={(value) => [`${value}%`, "CPU Usage"]}
+ />
+ }
+ />
+
+ } />
+
+
);
};
-
-interface CustomTooltipProps {
- active: boolean;
- payload?: {
- color?: string;
- dataKey?: string;
- value?: number;
- payload: {
- time: string;
- usage: number;
- };
- }[];
-}
-
-const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
- if (active && payload && payload.length && payload[0]) {
- return (
-
- {payload[0].payload.time && (
-
{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}
- )}
-
{`CPU Usage: ${payload[0].payload.usage}%`}
-
- );
- }
-
- return null;
-};
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx
index 58cefe6b3..4d17e7bde 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx
@@ -1,13 +1,13 @@
import { format } from "date-fns";
+import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
import {
- Area,
- AreaChart,
- CartesianGrid,
- Legend,
- ResponsiveContainer,
- Tooltip,
- YAxis,
-} from "recharts";
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
@@ -15,91 +15,99 @@ interface Props {
diskTotal: number;
}
+const chartConfig = {
+ usedGb: {
+ label: "Used (GB)",
+ color: "hsl(var(--chart-3))",
+ },
+ freeGb: {
+ label: "Free (GB)",
+ color: "hsl(var(--chart-4))",
+ },
+} satisfies ChartConfig;
+
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
- const transformedData = accumulativeData.map((item, index) => {
- return {
- time: item.time,
- name: `Point ${index + 1}`,
- usedGb: +item.value.diskUsage,
- totalGb: +item.value.diskTotal,
- freeGb: item.value.diskFree,
- };
- });
+ const transformedData = accumulativeData.map((item, index) => ({
+ time: item.time,
+ name: `Point ${index + 1}`,
+ usedGb: +item.value.diskUsage,
+ totalGb: +item.value.diskTotal,
+ freeGb: item.value.diskFree,
+ }));
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* @ts-ignore */}
- } />
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `${value} GB`}
+ />
+ {
+ const time = payload?.[0]?.payload?.time;
+ return time
+ ? format(new Date(time), "PPpp")
+ : "";
+ }}
+ formatter={(value, name) => {
+ const label =
+ name === "usedGb" ? "Used" : "Free";
+ return [`${value} GB`, label];
+ }}
+ />
+ }
+ />
+
+
+ } />
+
+
);
};
-interface CustomTooltipProps {
- active: boolean;
- payload?: {
- color?: string;
- dataKey?: string;
- value?: number;
- payload: {
- time: string;
- usedGb: number;
- freeGb: number;
- totalGb: number;
- };
- }[];
-}
-
-const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
- if (active && payload && payload.length && payload[0]) {
- return (
-
-
{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}
-
{`Disk usage: ${payload[0].payload.usedGb} GB`}
-
{`Disk free: ${payload[0].payload.freeGb} GB`}
-
{`Total disk: ${payload[0].payload.totalGb} GB`}
-
- );
- }
-
- return null;
-};
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-usage-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-usage-chart.tsx
new file mode 100644
index 000000000..dbd6b6b4c
--- /dev/null
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-usage-chart.tsx
@@ -0,0 +1,181 @@
+import { Loader2, RefreshCw } from "lucide-react";
+import { useMemo } from "react";
+import { Cell, Label, Pie, PieChart } from "recharts";
+import { Button } from "@/components/ui/button";
+import {
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
+import { api } from "@/utils/api";
+
+const TYPE_TO_KEY: Record = {
+ Images: "images",
+ Containers: "containers",
+ "Local Volumes": "volumes",
+ "Build Cache": "buildCache",
+};
+
+const chartConfig = {
+ value: {
+ label: "Size",
+ },
+ images: {
+ label: "Images",
+ color: "hsl(var(--chart-1))",
+ },
+ containers: {
+ label: "Containers",
+ color: "hsl(var(--chart-2))",
+ },
+ volumes: {
+ label: "Volumes",
+ color: "hsl(var(--chart-3))",
+ },
+ buildCache: {
+ label: "Build Cache",
+ color: "hsl(var(--chart-4))",
+ },
+} satisfies ChartConfig;
+
+const formatSize = (bytes: number): string => {
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
+ if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${bytes} B`;
+};
+
+export const DockerDiskUsageChart = () => {
+ const { data, isLoading, refetch, isRefetching } =
+ api.settings.getDockerDiskUsage.useQuery(undefined, {
+ refetchOnWindowFocus: false,
+ });
+
+ const { chartData, totalBytes } = useMemo(() => {
+ const items =
+ data
+ ?.filter((item) => item.sizeBytes > 0)
+ .map((item) => {
+ const key = TYPE_TO_KEY[item.type] ?? item.type;
+ return {
+ name: key,
+ value: item.sizeBytes,
+ size: item.size,
+ active: item.active,
+ totalCount: item.totalCount,
+ reclaimable: item.reclaimable,
+ fill: `var(--color-${key})`,
+ };
+ }) ?? [];
+ return {
+ chartData: items,
+ totalBytes: items.reduce((sum, item) => sum + item.value, 0),
+ };
+ }, [data]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (chartData.length === 0) {
+ return (
+
+ No Docker disk usage data available.
+
+ );
+ }
+
+ return (
+
+
+
+ Total: {formatSize(totalBytes)}
+
+
+
+
+
+ {
+ const item = chartData.find((d) => d.name === name);
+ if (!item) return [formatSize(value as number), name];
+ return [
+ `${item.size} — ${item.active} active / ${item.totalCount} total — Reclaimable: ${item.reclaimable}`,
+ chartConfig[name as keyof typeof chartConfig]?.label ?? name,
+ ];
+ }}
+ />
+ }
+ />
+
+ {chartData.map((entry) => (
+ |
+ ))}
+
+ } />
+
+
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx
index 226623fa2..4a6cf92ad 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx
@@ -1,13 +1,13 @@
import { format } from "date-fns";
+import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
import {
- Area,
- AreaChart,
- CartesianGrid,
- Legend,
- ResponsiveContainer,
- Tooltip,
- YAxis,
-} from "recharts";
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
import { convertMemoryToBytes } from "./show-free-container-monitoring";
@@ -16,78 +16,74 @@ interface Props {
memoryLimitGB: number;
}
+const chartConfig = {
+ usage: {
+ label: "Memory (GB)",
+ color: "hsl(var(--chart-2))",
+ },
+} satisfies ChartConfig;
+
export const DockerMemoryChart = ({
accumulativeData,
memoryLimitGB,
}: Props) => {
- const transformedData = accumulativeData.map((item, index) => {
- return {
- time: item.time,
- name: `Point ${index + 1}`,
- // @ts-ignore
- usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
- };
- });
+ const transformedData = accumulativeData.map((item, index) => ({
+ time: item.time,
+ name: `Point ${index + 1}`,
+ // @ts-ignore
+ usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
+ }));
+
return (
-
-
-
-
-
-
-
-
-
-
-
- {/* @ts-ignore */}
- } />
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ `${value} GB`}
+ domain={[0, +memoryLimitGB.toFixed(2)]}
+ tickLine={false}
+ axisLine={false}
+ />
+ {
+ const time = payload?.[0]?.payload?.time;
+ return time
+ ? format(new Date(time), "PPpp")
+ : "";
+ }}
+ formatter={(value) => [`${value} GB`, "Memory"]}
+ />
+ }
+ />
+
+ } />
+
+
);
};
-interface CustomTooltipProps {
- active: boolean;
- payload?: {
- color?: string;
- dataKey?: string;
- value?: number;
- payload: {
- time: string;
- usage: number;
- };
- }[];
-}
-
-const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
- if (active && payload && payload.length && payload[0] && payload[0].payload) {
- return (
-
- {payload[0].payload.time && (
-
{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}
- )}
-
-
{`Memory usage: ${payload[0].payload.usage} GB`}
-
- );
- }
-
- return null;
-};
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx
index 8dafcb465..9fee45adc 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx
@@ -1,99 +1,105 @@
import { format } from "date-fns";
+import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
import {
- Area,
- AreaChart,
- CartesianGrid,
- Legend,
- ResponsiveContainer,
- Tooltip,
- YAxis,
-} from "recharts";
+ type ChartConfig,
+ ChartContainer,
+ ChartLegend,
+ ChartLegendContent,
+ ChartTooltip,
+ ChartTooltipContent,
+} from "@/components/ui/chart";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["network"];
}
+const chartConfig = {
+ inMB: {
+ label: "In (MB)",
+ color: "hsl(var(--chart-1))",
+ },
+ outMB: {
+ label: "Out (MB)",
+ color: "hsl(var(--chart-2))",
+ },
+} satisfies ChartConfig;
+
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
- const transformedData = accumulativeData.map((item, index) => {
- return {
- time: item.time,
- name: `Point ${index + 1}`,
- inMB: item.value.inputMb,
- outMB: item.value.outputMb,
- };
- });
+ const transformedData = accumulativeData.map((item, index) => ({
+ time: item.time,
+ name: `Point ${index + 1}`,
+ inMB: item.value.inputMb,
+ outMB: item.value.outputMb,
+ }));
+
return (
-
-
-
-
-
-
-
-
-
-
-
- {/* @ts-ignore */}
- } />
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ const time = payload?.[0]?.payload?.time;
+ return time
+ ? format(new Date(time), "PPpp")
+ : "";
+ }}
+ formatter={(value, name) => {
+ const label = name === "inMB" ? "In" : "Out";
+ return [`${value} MB`, label];
+ }}
+ />
+ }
+ />
+
+
+ } />
+
+
);
};
-
-interface CustomTooltipProps {
- active: boolean;
- payload?: {
- color?: string;
- dataKey?: string;
- value?: number;
- payload: {
- time: string;
- inMB: number;
- outMB: number;
- };
- }[];
-}
-
-const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
- if (active && payload && payload.length && payload[0]) {
- return (
-
- {payload[0].payload.time && (
-
{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}
- )}
-
{`In Usage: ${payload[0].payload.inMB} `}
-
{`Out Usage: ${payload[0].payload.outMB} `}
-
- );
- }
-
- return null;
-};
diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx
index 6e572c224..fd666255c 100644
--- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx
+++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx
@@ -5,6 +5,7 @@ import { api } from "@/utils/api";
import { DockerBlockChart } from "./docker-block-chart";
import { DockerCpuChart } from "./docker-cpu-chart";
import { DockerDiskChart } from "./docker-disk-chart";
+import { DockerDiskUsageChart } from "./docker-disk-usage-chart";
import { DockerMemoryChart } from "./docker-memory-chart";
import { DockerNetworkChart } from "./docker-network-chart";
@@ -284,6 +285,18 @@ export const ContainerFreeMonitoring = ({
)}
+ {appName === "dokploy" && (
+
+
+
+ Docker Disk Usage
+
+
+
+
+
+
+ )}
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index 26c5d4822..9965643f7 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -15,6 +15,7 @@ import {
DEFAULT_UPDATE_DATA,
execAsync,
findServerById,
+ getDockerDiskUsage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
@@ -291,6 +292,12 @@ export const settingsRouter = createTRPCRouter({
});
return true;
}),
+ getDockerDiskUsage: adminProcedure.query(async () => {
+ if (IS_CLOUD) {
+ return [];
+ }
+ return getDockerDiskUsage();
+ }),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => {
diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts
index dd645cd1b..cfe9b95ac 100644
--- a/packages/server/src/utils/docker/utils.ts
+++ b/packages/server/src/utils/docker/utils.ts
@@ -259,6 +259,48 @@ export const cleanupSystem = async (serverId?: string) => {
}
};
+export interface DockerDiskUsageItem {
+ type: string;
+ totalCount: number;
+ active: number;
+ size: string;
+ reclaimable: string;
+ sizeBytes: number;
+}
+
+const parseSizeToBytes = (size: string): number => {
+ const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);
+ if (!match) return 0;
+ const value = Number.parseFloat(match[1] as string);
+ const unit = match[2].toUpperCase();
+ const multipliers: Record = {
+ B: 1,
+ KB: 1024,
+ MB: 1024 ** 2,
+ GB: 1024 ** 3,
+ TB: 1024 ** 4,
+ };
+ return value * (multipliers[unit] || 0);
+};
+
+export const getDockerDiskUsage = async (): Promise => {
+ const command = "docker system df --format '{{json .}}'";
+ const { stdout } = await execAsync(command);
+
+ const lines = stdout.trim().split("\n").filter(Boolean);
+ return lines.map((line) => {
+ const data = JSON.parse(line);
+ return {
+ type: data.Type,
+ totalCount: Number.parseInt(data.TotalCount, 10) || 0,
+ active: Number.parseInt(data.Active, 10) || 0,
+ size: data.Size,
+ reclaimable: data.Reclaimable,
+ sizeBytes: parseSizeToBytes(data.Size),
+ };
+ });
+};
+
/**
* Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation.
*