feat: add Docker service for container management and image pulls

This commit is contained in:
plenarius
2026-04-20 18:29:09 -04:00
parent ee7a0783ea
commit ec8aaf64a3
3 changed files with 210 additions and 0 deletions
@@ -0,0 +1,138 @@
import Docker from "dockerode";
import { broadcast } from "./ws.service.js";
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
export interface ContainerInfo {
id: string;
name: string;
image: string;
state: string;
status: string;
health?: string;
}
export async function listForgeContainers(): Promise<ContainerInfo[]> {
const containers = await docker.listContainers({ all: true });
return containers
.filter((c) => c.Names.some((n) => n.startsWith("/layonara-")))
.map((c) => ({
id: c.Id.slice(0, 12),
name: c.Names[0].replace(/^\//, ""),
image: c.Image,
state: c.State,
status: c.Status,
health: c.Status.includes("healthy")
? "healthy"
: c.Status.includes("unhealthy")
? "unhealthy"
: undefined,
}));
}
export async function pullImage(imageName: string): Promise<void> {
broadcast("docker", "pull:start", { image: imageName });
const stream = await docker.pull(imageName);
await new Promise<void>((resolve, reject) => {
docker.modem.followProgress(
stream,
(err: Error | null) => {
if (err) {
broadcast("docker", "pull:error", { image: imageName, error: err.message });
reject(err);
} else {
broadcast("docker", "pull:complete", { image: imageName });
resolve();
}
},
(event: { status: string; progress?: string }) => {
broadcast("docker", "pull:progress", {
image: imageName,
status: event.status,
progress: event.progress,
});
},
);
});
}
export async function startContainer(name: string): Promise<void> {
const container = docker.getContainer(name);
await container.start();
broadcast("docker", "container:started", { name });
}
export async function stopContainer(name: string): Promise<void> {
const container = docker.getContainer(name);
await container.stop();
broadcast("docker", "container:stopped", { name });
}
export async function restartContainer(name: string): Promise<void> {
const container = docker.getContainer(name);
await container.restart();
broadcast("docker", "container:restarted", { name });
}
export async function getContainerLogs(
name: string,
tail: number = 100,
): Promise<string> {
const container = docker.getContainer(name);
const logs = await container.logs({
stdout: true,
stderr: true,
tail,
timestamps: true,
});
return logs.toString();
}
export async function streamContainerLogs(name: string): Promise<NodeJS.ReadableStream> {
const container = docker.getContainer(name);
return container.logs({
follow: true,
stdout: true,
stderr: true,
tail: 50,
timestamps: true,
});
}
export async function runEphemeralContainer(opts: {
image: string;
cmd: string[];
binds: string[];
workingDir?: string;
env?: string[];
}): Promise<{ statusCode: number; output: string }> {
const container = await docker.createContainer({
Image: opts.image,
Cmd: opts.cmd,
WorkingDir: opts.workingDir,
Env: opts.env,
HostConfig: {
Binds: opts.binds,
AutoRemove: true,
},
AttachStdout: true,
AttachStderr: true,
});
const stream = await container.attach({ stream: true, stdout: true, stderr: true });
let output = "";
stream.on("data", (chunk: Buffer) => {
const text = chunk.toString();
output += text;
broadcast("build", "output", { text });
});
await container.start();
const { StatusCode } = await container.wait();
return { statusCode: StatusCode, output };
}
export function getDockerClient(): Docker {
return docker;
}