feat: add Images and Disk Usage tabs to docker dashboard

- Images tab: list, inspect and delete docker images, with a
  force-delete fallback when an image is in use.
- Disk Usage tab: docker system df summary as stat cards plus a
  Build Cache table (docker system df -v) with a prune action.
This commit is contained in:
Mauricio Siu
2026-08-12 04:30:19 -06:00
parent 9412b52172
commit c2071bbbfd
9 changed files with 1179 additions and 3 deletions
+2 -1
View File
@@ -18,6 +18,7 @@ export * from "./services/deployment";
export * from "./services/destination";
export * from "./services/dns-provider";
export * from "./services/docker";
export * from "./services/docker-image";
export * from "./services/docker-volume";
export * from "./services/domain";
export * from "./services/environment";
@@ -104,7 +105,6 @@ export * from "./utils/docker/compose/volume";
export * from "./utils/docker/domain";
export * from "./utils/docker/types";
export * from "./utils/docker/utils";
export * from "./utils/vault";
export * from "./utils/filesystem/directory";
export * from "./utils/filesystem/ssh";
export * from "./utils/git-branch-validation";
@@ -141,6 +141,7 @@ export * from "./utils/traefik/redirect";
export * from "./utils/traefik/security";
export * from "./utils/traefik/types";
export * from "./utils/traefik/web-server";
export * from "./utils/vault";
export * from "./utils/volume-backups/index";
export * from "./utils/watch-paths/should-deploy";
export * from "./verification/send-verification-email";
@@ -0,0 +1,68 @@
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { quote } from "shell-quote";
export interface DockerImage {
Repository: string;
Tag: string;
ID: string;
Digest?: string;
CreatedAt: string;
CreatedSince: string;
Size: string;
SharedSize?: string;
UniqueSize?: string;
VirtualSize?: string;
}
export const getImages = async (serverId?: string) => {
try {
const command = "docker images --format '{{json .}}'";
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
return stdout
.trim()
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line) as DockerImage);
} catch (error) {
console.error(error);
return [];
}
};
export const getImageConfig = async (imageRef: string, serverId?: string) => {
const command = `docker image inspect ${quote([String(imageRef ?? "")])}`;
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
return JSON.parse(stdout.trim())[0];
};
interface RemoveImageParams {
repository: string;
tag: string;
id: string;
force?: boolean;
}
export const removeImage = async (
{ repository, tag, id, force }: RemoveImageParams,
serverId?: string,
) => {
const hasTaggedReference =
repository && tag && repository !== "<none>" && tag !== "<none>";
const reference = hasTaggedReference ? `${repository}:${tag}` : id;
const command = `docker rmi ${force ? "-f " : ""}${quote([String(reference ?? "")])}`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
};
+49 -2
View File
@@ -291,9 +291,13 @@ const parseSizeToBytes = (size: string): number => {
return value * (multipliers[unit] || 0);
};
export const getDockerDiskUsage = async (): Promise<DockerDiskUsageItem[]> => {
export const getDockerDiskUsage = async (
serverId?: string,
): Promise<DockerDiskUsageItem[]> => {
const command = "docker system df --format '{{json .}}'";
const { stdout } = await execAsync(command);
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
const lines = stdout.trim().split("\n").filter(Boolean);
return lines.map((line) => {
@@ -309,6 +313,49 @@ export const getDockerDiskUsage = async (): Promise<DockerDiskUsageItem[]> => {
});
};
export interface DockerBuildCacheItem {
id: string;
type: string;
description: string;
size: string;
sizeBytes: number;
createdSince: string;
lastUsedSince: string;
usageCount: number;
shared: boolean;
inUse: boolean;
}
export const getBuildCache = async (
serverId?: string,
): Promise<DockerBuildCacheItem[]> => {
try {
const command = "docker system df -v --format '{{json .}}'";
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
const diskUsage = JSON.parse(stdout.trim());
return ((diskUsage?.BuildCache ?? []) as Record<string, string>[]).map(
(entry) => ({
id: entry.ID ?? "",
type: entry.CacheType ?? "",
description: entry.Description ?? "",
size: entry.Size ?? "",
sizeBytes: parseSizeToBytes(entry.Size ?? ""),
createdSince: entry.CreatedSince ?? "",
lastUsedSince: entry.LastUsedSince ?? "",
usageCount: Number.parseInt(entry.UsageCount ?? "0", 10) || 0,
shared: entry.Shared === "true",
inUse: entry.InUse === "true",
}),
);
} catch (error) {
console.error(error);
return [];
}
};
/**
* 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.
*