Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions backend/agent-socket-handlers/docker-socket-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DockgeServer } from "../dockge-server";
import { callbackError, callbackResult, checkLogin, DockgeSocket, ValidationError } from "../util-server";
import { Stack } from "../stack";
import { AgentSocket } from "../../common/agent-socket";
import childProcessAsync from "promisify-child-process";

export class DockerSocketHandler extends AgentSocketHandler {
create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
Expand Down Expand Up @@ -251,6 +252,107 @@ export class DockerSocketHandler extends AgentSocketHandler {
callbackError(e, callback);
}
});

// getDockerImageList
agentSocket.on("getDockerImageList", async (callback) => {
try {
checkLogin(socket);
const imageList = await this.getDockerImageList();
callbackResult({
ok: true,
imageList,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// deleteDockerImage
agentSocket.on("deleteDockerImage", async (imageId : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(imageId) !== "string") {
throw new ValidationError("Image ID must be a string");
}
await this.deleteDockerImage(imageId);
callbackResult({
ok: true,
msg: "Image Deleted",
msgi18n: true,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// getDockerDiskUsage
agentSocket.on("getDockerDiskUsage", async (callback) => {
try {
checkLogin(socket);
const diskUsage = await this.getDockerDiskUsage();
callbackResult({
ok: true,
diskUsage,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// pullDockerImage
agentSocket.on("pullDockerImage", async (imageName : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(imageName) !== "string") {
throw new ValidationError("Image name must be a string");
}
await this.pullDockerImage(imageName);
callbackResult({
ok: true,
msg: "Image Pulled",
msgi18n: true,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// buildDockerImage
agentSocket.on("buildDockerImage", async (imageName : unknown, dockerfileContent : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(imageName) !== "string") {
throw new ValidationError("Image name must be a string");
}
if (typeof(dockerfileContent) !== "string") {
throw new ValidationError("Dockerfile content must be a string");
}
await this.buildDockerImage(imageName, dockerfileContent);
callbackResult({
ok: true,
msg: "Image Built",
msgi18n: true,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});

// pruneDockerImages
agentSocket.on("pruneDockerImages", async (callback) => {
try {
checkLogin(socket);
const result = await this.pruneDockerImages();
callbackResult({
ok: true,
msg: "Images Pruned",
msgi18n: true,
result,
}, callback);
} catch (e) {
callbackError(e, callback);
}
});
}

async saveStack(server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise<Stack> {
Expand All @@ -273,5 +375,151 @@ export class DockerSocketHandler extends AgentSocketHandler {
return stack;
}

/**
* Get the list of Docker images
* @returns List of Docker images with their details
*/
async getDockerImageList() {
const res = await childProcessAsync.spawn("docker", [ "images", "--format", "json" ], {
encoding: "utf-8",
});

if (!res.stdout) {
return [];
}

const output = res.stdout.toString().trim();
if (!output) {
return [];
}

const lines = output.split("\n");
const imageList = lines.map((line : string) => {
try {
return JSON.parse(line);
} catch (e) {
return null;
}
}).filter((img : unknown) => img !== null);

return imageList;
}

/**
* Delete a Docker image by ID or name
* @param imageId - The image ID or name to delete
* @throws Error with Docker error message if deletion fails
*/
async deleteDockerImage(imageId : string) {
try {
await childProcessAsync.spawn("docker", [ "rmi", imageId ], {
encoding: "utf-8",
});
} catch (error : any) {
// Extract meaningful error message from Docker
const stderr = error.stderr?.toString() || "";
const stdout = error.stdout?.toString() || "";
const errorMessage = stderr || stdout || error.message || "Failed to delete image";

// Throw error with the actual Docker message
throw new Error(errorMessage);
}
}

/**
* Get Docker disk usage information
* @returns Docker disk usage statistics
*/
async getDockerDiskUsage() {
const res = await childProcessAsync.spawn("docker", [ "system", "df", "--format", "json" ], {
encoding: "utf-8",
});

if (!res.stdout) {
return {};
}

const output = res.stdout.toString().trim();
if (!output) {
return {};
}

return JSON.parse(output);
}

/**
* Pull a Docker image from registry
* @param imageName - The image name to pull (e.g., "nginx:latest")
* @throws Error with Docker error message if pull fails
*/
async pullDockerImage(imageName : string) {
try {
await childProcessAsync.spawn("docker", [ "pull", imageName ], {
encoding: "utf-8",
});
} catch (error : any) {
const stderr = error.stderr?.toString() || "";
const stdout = error.stdout?.toString() || "";
const errorMessage = stderr || stdout || error.message || "Failed to pull image";
throw new Error(errorMessage);
}
}

/**
* Build a Docker image from Dockerfile content
* @param imageName - The name/tag for the built image (e.g., "myapp:latest")
* @param dockerfileContent - The content of the Dockerfile
* @throws Error with Docker error message if build fails
*/
async buildDockerImage(imageName : string, dockerfileContent : string) {
const fs = await import("fs");
const os = await import("os");
const path = await import("path");

// Create a temporary directory for the build context
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "dockge-build-"));
const dockerfilePath = path.join(tempDir, "Dockerfile");

try {
// Write Dockerfile content to temp directory
fs.writeFileSync(dockerfilePath, dockerfileContent);

// Build the image
try {
await childProcessAsync.spawn("docker", [ "build", "-t", imageName, tempDir ], {
encoding: "utf-8",
});
} catch (error : any) {
const stderr = error.stderr?.toString() || "";
const stdout = error.stdout?.toString() || "";
const errorMessage = stderr || stdout || error.message || "Failed to build image";
throw new Error(errorMessage);
}
} finally {
// Clean up temp directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (e) {
// Ignore cleanup errors
}
}
}

/**
* Prune unused Docker images
* @returns Result of the prune operation
*/
async pruneDockerImages() {
const res = await childProcessAsync.spawn("docker", [ "image", "prune", "-a", "-f" ], {
encoding: "utf-8",
});

return {
stdout: res.stdout?.toString() || "",
stderr: res.stderr?.toString() || "",
};
}

}


41 changes: 40 additions & 1 deletion frontend/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,44 @@
"ConsoleNotEnabledMSG1": "Console is a powerful tool that allows you to execute any commands such as <code>docker</code>, <code>rm</code> within the Dockge's container in this Web UI.",
"ConsoleNotEnabledMSG2": "It might be dangerous since this Dockge container is connecting to the host's Docker daemon. Also Dockge could be possibly taken down by commands like <code>rm -rf</code>" ,
"ConsoleNotEnabledMSG3": "If you understand the risk, you can enable it by setting <code>DOCKGE_ENABLE_CONSOLE=true</code> in the environment variables.",
"confirmLeaveStack": "You are currently editing a stack. Are you sure you want to leave?"
"confirmLeaveStack": "You are currently editing a stack. Are you sure you want to leave?",
"dockerImages": "Docker Images",
"diskUsage": "Disk Usage",
"images": "Images",
"containers": "Containers",
"volumes": "Volumes",
"buildCache": "Build Cache",
"refresh": "Refresh",
"pruneImages": "Prune Images",
"loading": "Loading",
"noImagesFound": "No images found",
"repository": "Repository",
"tag": "Tag",
"imageId": "Image ID",
"created": "Created",
"size": "Size",
"actions": "Actions",
"deleteImage": "Delete Image",
"deleteImageMsg": "Are you sure you want to delete this image?",
"pruneImagesMsg": "This will permanently delete ALL unused images (not just dangling ones).",
"pruneImagesWarning": "This action cannot be undone and may free up significant disk space.",
"warning": "Warning",
"pullImage": "Pull Image",
"imageName": "Image Name",
"imageNamePlaceholder": "nginx:latest",
"imageNameExample": "Example: nginx:latest, redis:alpine, mysql:8.0",
"buildImage": "Build Image",
"buildImageNamePlaceholder": "myapp:latest",
"buildImageNameExample": "Example: myapp:latest, myapp:1.0.0",
"dockerfile": "Dockerfile",
"dockerfilePlaceholder": "FROM alpine:latest\nRUN apk add --no-cache curl\nCMD [\"/bin/sh\"]",
"dockerfileExample": "Paste your Dockerfile content here",
"searchImages": "Search images...",
"pullingImage": "Pulling {0}...",
"buildingImage": "Building {0}...",
"imageInUseError": "This image is currently being used by one or more containers. Please stop and remove the containers first.",
"Image Deleted": "Image Deleted",
"Image Pulled": "Image Pulled",
"Image Built": "Image Built",
"Images Pruned": "Images Pruned"
}
6 changes: 6 additions & 0 deletions frontend/src/layouts/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
</router-link>
</li>

<li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/images" class="nav-link">
<font-awesome-icon icon="images" /> {{ $t("dockerImages") }}
</router-link>
</li>

<li v-if="$root.loggedIn" class="nav-item me-2">
<router-link to="/console" class="nav-link">
<font-awesome-icon icon="terminal" /> {{ $t("console") }}
Expand Down
Loading
Loading