feat: live Docker data — containers, services, logs, metrics via dockerode
All checks were successful
continuous-integration/drone/push Build is passing

- Backend: rewrote containers/services/logs/metrics routes to use dockerode
- docker-compose: mount /var/run/docker.sock, run backend as root
- .drone.yml: sync docker-compose.yml from Gitea on deploy
- Frontend: Containers page shows real data with wired start/stop/restart
- Frontend: Services page shows Docker Compose stacks with health status
- Frontend: Metrics page adds disk (docker df) and containers cards + chart legend
- Frontend: Logs page replaces text input with container dropdown + auto-refresh
This commit is contained in:
Ernie Butcher
2026-03-18 18:33:25 -04:00
parent 36c76edb29
commit 4233734759
20 changed files with 1146 additions and 172 deletions

View File

@@ -1,55 +1,66 @@
import { Router, Request, Response } from 'express';
import Docker from 'dockerode';
const router = Router();
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
// GET /api/services - List all services
router.get('/', async (req: Request, res: Response) => {
try {
// TODO: Implement service listing (systemd, docker compose, etc.)
res.json({ services: [] });
const list = await docker.listContainers({ all: true });
const projects = new Map<string, any[]>();
const standalone: any[] = [];
for (const c of list) {
const project = c.Labels?.['com.docker.compose.project'];
const service = c.Labels?.['com.docker.compose.service'];
if (project && service) {
if (!projects.has(project)) projects.set(project, []);
projects.get(project)!.push({
name: service,
container: c.Names[0]?.replace(/^\//, ''),
state: c.State,
status: c.Status,
image: c.Image,
});
} else {
standalone.push({
name: c.Names[0]?.replace(/^\//, '') ?? c.Id.slice(0, 12),
container: c.Names[0]?.replace(/^\//, ''),
state: c.State,
status: c.Status,
image: c.Image,
});
}
}
const stacks = [
...Array.from(projects.entries()).map(([project, svcs]) => ({
id: project,
name: project,
type: 'compose',
services: svcs,
running: svcs.filter(s => s.state === 'running').length,
total: svcs.length,
status: svcs.every(s => s.state === 'running') ? 'healthy'
: svcs.some(s => s.state === 'running') ? 'degraded' : 'stopped',
})),
...(standalone.length > 0 ? [{
id: 'standalone',
name: 'standalone',
type: 'standalone',
services: standalone,
running: standalone.filter(s => s.state === 'running').length,
total: standalone.length,
status: 'unknown',
}] : []),
];
res.json({ services: stacks });
} catch (error) {
console.error('Services error:', error);
res.status(500).json({ error: 'Failed to fetch services' });
}
});
// GET /api/services/:name - Get service status
router.get('/:name', async (req: Request, res: Response) => {
try {
// TODO: Implement service status check
res.json({ name: req.params.name, status: 'unknown' });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch service status' });
}
});
// POST /api/services/:name/start - Start service
router.post('/:name/start', async (req: Request, res: Response) => {
try {
// TODO: Implement service start
res.json({ status: 'started' });
} catch (error) {
res.status(500).json({ error: 'Failed to start service' });
}
});
// POST /api/services/:name/stop - Stop service
router.post('/:name/stop', async (req: Request, res: Response) => {
try {
// TODO: Implement service stop
res.json({ status: 'stopped' });
} catch (error) {
res.status(500).json({ error: 'Failed to stop service' });
}
});
// POST /api/services/:name/restart - Restart service
router.post('/:name/restart', async (req: Request, res: Response) => {
try {
// TODO: Implement service restart
res.json({ status: 'restarted' });
} catch (error) {
res.status(500).json({ error: 'Failed to restart service' });
}
});
export default router;