feat(system): Docker container management via Unix socket

This commit is contained in:
Zac Gaetano 2026-05-19 23:46:03 -04:00
parent 89771a2380
commit 910a906600

View file

@ -0,0 +1,101 @@
import express from 'express';
import http from 'node:http';
import { requireAuth } from '../middleware/auth.js';
const router = express.Router();
router.use(requireAuth);
const DOCKER_SOCKET = '/var/run/docker.sock';
const COMPOSE_PROJECT = (process.env.DOCKER_NETWORK || 'wild-dragon_wild-dragon').split('_')[0];
function dockerGet(path) {
return new Promise((resolve, reject) => {
const req = http.request(
{ socketPath: DOCKER_SOCKET, path, method: 'GET' },
(res) => {
let data = '';
res.on('data', chunk => { data += chunk; });
res.on('end', () => {
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, body: data }); }
});
}
);
req.on('error', reject);
req.end();
});
}
function dockerPost(path) {
return new Promise((resolve, reject) => {
const req = http.request(
{ socketPath: DOCKER_SOCKET, path, method: 'POST', headers: { 'Content-Length': '0' } },
(res) => {
res.resume();
res.on('end', () => resolve({ status: res.statusCode }));
}
);
req.on('error', reject);
req.end();
});
}
// GET /containers list containers for this compose project
router.get('/containers', async (req, res, next) => {
try {
const r = await dockerGet('/containers/json?all=1');
if (r.status !== 200) return res.status(502).json({ error: 'Docker API error', code: r.status });
const containers = r.body
.filter(c => {
const labels = c.Labels || {};
// Match by compose project label; fall back to network name
if (labels['com.docker.compose.project'] === COMPOSE_PROJECT) return true;
return Object.keys(c.NetworkSettings?.Networks || {}).some(n => n.includes(COMPOSE_PROJECT));
})
.map(c => ({
id: c.Id.slice(0, 12),
name: (c.Names[0] || '').replace(/^\//, ''),
image: c.Image,
state: c.State,
status: c.Status,
created: c.Created,
service: (c.Labels || {})['com.docker.compose.service'] || '',
ports: (c.Ports || [])
.filter(p => p.PublicPort)
.map(p => `${p.PublicPort}:${p.PrivatePort}/${p.Type}`)
.join(', '),
}));
res.json(containers);
} catch (err) { next(err); }
});
// POST /containers/:id/restart
router.post('/containers/:id/restart', async (req, res, next) => {
try {
const r = await dockerPost(`/containers/${req.params.id}/restart`);
if (r.status !== 204) return res.status(502).json({ error: 'Failed to restart', code: r.status });
res.json({ ok: true });
} catch (err) { next(err); }
});
// POST /containers/:id/stop
router.post('/containers/:id/stop', async (req, res, next) => {
try {
const r = await dockerPost(`/containers/${req.params.id}/stop`);
if (r.status !== 204 && r.status !== 304) return res.status(502).json({ error: 'Failed to stop', code: r.status });
res.json({ ok: true });
} catch (err) { next(err); }
});
// POST /containers/:id/start
router.post('/containers/:id/start', async (req, res, next) => {
try {
const r = await dockerPost(`/containers/${req.params.id}/start`);
if (r.status !== 204 && r.status !== 304) return res.status(502).json({ error: 'Failed to start', code: r.status });
res.json({ ok: true });
} catch (err) { next(err); }
});
export default router;