feat: Implement web-based Docker Compose stack management with file editing, deployment, container monitoring, and terminal access.

This commit is contained in:
SaelixCode
2026-02-21 15:05:05 -05:00
parent e94b706a9f
commit 18c51f083e
4 changed files with 187 additions and 96 deletions
+66 -15
View File
@@ -16,18 +16,18 @@ export class ComposeService {
*/
runCommand(stackName: string, action: 'up' | 'down', ws?: WebSocket) {
const stackDir = path.join(this.baseDir, stackName);
// Run docker compose from within the stack directory
// This ensures relative paths (e.g., ./data:/config) resolve correctly
const args = action === 'up'
? ['compose', 'up', '-d']
const args = action === 'up'
? ['compose', 'up', '-d']
: ['compose', 'down'];
const child = spawn('docker', args, {
const child = spawn('docker', args, {
cwd: stackDir, // CRITICAL: Set working directory to stack folder
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
}
});
@@ -51,6 +51,57 @@ export class ComposeService {
}
}
/**
* Stream docker compose logs for a stack via WebSocket.
* Spawns `docker compose logs -f --tail 100` and pipes stdout/stderr to ws.send().
* Kills the child process when the WebSocket closes.
*/
streamLogs(stackName: string, ws: WebSocket) {
const stackDir = path.join(this.baseDir, stackName);
const child = spawn('docker', ['compose', 'logs', '-f', '--tail', '100'], {
cwd: stackDir,
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
}
});
child.stdout.on('data', (data: Buffer) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data.toString());
}
});
child.stderr.on('data', (data: Buffer) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data.toString());
}
});
child.on('error', (error: Error) => {
console.error(`Docker Compose Logs Error for ${stackName}:`, error.message);
if (ws.readyState === WebSocket.OPEN) {
ws.send(`Error: ${error.message}\n`);
}
});
child.on('close', (code: number | null) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(`\r\nLog stream ended (code ${code})\r\n`);
}
});
// Kill the logs process when the WebSocket is closed
ws.on('close', () => {
try {
child.kill();
} catch {
// Ignore kill errors
}
});
}
/**
* Update stack: pull images first, then recreate containers
* CRITICAL: cwd is set to the stack directory so relative paths resolve correctly
@@ -67,11 +118,11 @@ export class ComposeService {
// Step 1: Pull images
sendOutput('=== Pulling latest images ===\n');
await new Promise<void>((resolve, reject) => {
const pullProcess = spawn('docker', ['compose', 'pull'], {
const pullProcess = spawn('docker', ['compose', 'pull'], {
cwd: stackDir,
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
}
});
@@ -103,11 +154,11 @@ export class ComposeService {
// Step 2: Recreate containers with new images
sendOutput('=== Recreating containers ===\n');
await new Promise<void>((resolve, reject) => {
const upProcess = spawn('docker', ['compose', 'up', '-d'], {
const upProcess = spawn('docker', ['compose', 'up', '-d'], {
cwd: stackDir,
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
env: {
...process.env,
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
}
});