feat: init

This commit is contained in:
2026-03-27 12:18:36 +01:00
commit cc59a91fea
55 changed files with 21768 additions and 0 deletions

20
backend/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
# Use an official Node.js runtime as a parent image
FROM node:18-alpine
# Set the working directory in the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install any needed packages
RUN npm install
# Bundle app source
COPY . .
# Make port 3001 available to the world outside this container
EXPOSE 3001
# Define the command to run the app
CMD [ "node", "index.js" ]

114
backend/docker.js Normal file
View File

@@ -0,0 +1,114 @@
const Docker = require('dockerode');
const os = require('os');
const ip = require('ip');
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
function getHostIpAddresses() {
const interfaces = os.networkInterfaces();
const ipAddresses = [];
for (const interfaceName in interfaces) {
for (const iface of interfaces[interfaceName]) {
// Only consider IPv4 and non-internal addresses
if (iface.family === 'IPv4' && !iface.internal) {
ipAddresses.push({
address: iface.address,
netmask: iface.netmask,
cidr: iface.cidr,
});
}
}
}
return ipAddresses;
}
function isIpInSubnet(checkIp, subnetCidr) {
try {
return ip.cidrSubnet(subnetCidr).contains(checkIp);
} catch (e) {
console.error(`Error checking IP in subnet: ${e.message}`);
return false;
}
}
function getTraefikUrl(container) {
const ruleLabel = Object.keys(container.Labels).find(label => label.endsWith('.rule'));
if (ruleLabel) {
const rule = container.Labels[ruleLabel];
const match = rule.match(/Host\(`(.*?)`\)/);
if (match) {
return `http://${match[1]}`;
}
}
return null;
}
async function listContainers() {
const containers = await docker.listContainers();
const hostIpAddresses = getHostIpAddresses();
return containers
.filter(container => container.Ports.length > 0)
.map(container => {
const traefikUrl = getTraefikUrl(container);
const containerName = container.Names[0].substring(1);
const accessibleUrls = [];
let preferredLocalUrl = null;
// Get all container IPs
const containerIps = [];
for (const networkName in container.NetworkSettings.Networks) {
const network = container.NetworkSettings.Networks[networkName];
if (network.IPAddress) {
containerIps.push(network.IPAddress);
}
}
// Determine accessible URLs and preferred local URL
container.Ports.forEach(port => {
const publicPort = port.PublicPort;
const privatePort = port.PrivatePort;
// Add accessible URLs based on container IPs
containerIps.forEach(containerIp => {
const url = `http://${containerIp}:${privatePort}`;
accessibleUrls.push({ type: 'container', url: url, description: `Container IP: ${containerIp}` });
// Check for preferred local URL
if (!preferredLocalUrl) {
for (const hostIp of hostIpAddresses) {
if (isIpInSubnet(containerIp, hostIp.cidr)) {
preferredLocalUrl = `http://${hostIp.address}:${publicPort || privatePort}`;
break;
}
}
}
});
// Add accessible URLs based on host exposed ports
if (publicPort) {
hostIpAddresses.forEach(hostIp => {
const url = `http://${hostIp.address}:${publicPort}`;
accessibleUrls.push({ type: 'host', url: url, description: `Host IP: ${hostIp.address}` });
});
}
});
return {
id: container.Id,
name: containerName,
ports: container.Ports,
traefikUrl: traefikUrl,
preferredLocalUrl: preferredLocalUrl,
accessibleUrls: accessibleUrls,
};
});
}
module.exports = {
docker,
listContainers,
getHostIpAddresses,
};

49
backend/index.js Normal file
View File

@@ -0,0 +1,49 @@
const express = require('express');
const cors = require('cors');
const expressWs = require('express-ws');
const { listContainers, docker } = require('./docker');
const app = express();
const wsInstance = expressWs(app);
app.use(cors());
app.get('/api/services', async (req, res) => {
const services = await listContainers();
res.json(services);
});
app.ws('/api/events', (ws, req) => {
docker.getEvents((err, stream) => {
if (err) {
console.error(err);
ws.close();
return;
}
stream.on('data', (chunk) => {
const event = JSON.parse(chunk.toString());
if (event.Type === 'container' && (event.Action === 'start' || event.Action === 'stop')) {
ws.send(JSON.stringify({ type: 'container', action: event.Action }));
}
});
stream.on('end', () => {
ws.close();
});
ws.on('close', () => {
stream.destroy();
});
});
});
app.get('/', (req, res) => {
res.send('Hello, World!');
});
const port = process.env.PORT || 3001;
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});

1569
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
backend/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"express": "*",
"dockerode": "*",
"cors": "*",
"express-ws": "*",
"ip": "*"
},
"keywords": [],
"author": "",
"license": "ISC"
}