Add files via upload
This commit is contained in:
parent
0626792b0d
commit
9597df2524
6 changed files with 405 additions and 51 deletions
30
app.js
30
app.js
|
@ -4,7 +4,7 @@ const redis = require('connect-redis');
|
|||
const app = express();
|
||||
const routes = require("./routes");
|
||||
|
||||
const { serverStats, containerList, containerStats, containerAction } = require('./functions/systeminformation');
|
||||
const { serverStats, containerList, containerStats, containerAction } = require('./functions/system_information');
|
||||
|
||||
let sent_list, clicked;
|
||||
|
||||
|
@ -43,8 +43,6 @@ const io = require('socket.io')(server);
|
|||
io.engine.use(sessionMiddleware);
|
||||
|
||||
|
||||
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
// set user session
|
||||
const user_session = socket.request.session;
|
||||
|
@ -54,22 +52,16 @@ io.on('connection', (socket) => {
|
|||
if (sent_list != null) { socket.emit('cards', sent_list); }
|
||||
|
||||
// check if an install card has to be sent
|
||||
if((app.locals.install != '') && (app.locals.install != null)){
|
||||
socket.emit('install', app.locals.install);
|
||||
}
|
||||
if((app.locals.install != '') && (app.locals.install != null)){ socket.emit('install', app.locals.install); }
|
||||
|
||||
// send server metrics
|
||||
let ServerStats = setInterval(async () => {
|
||||
|
||||
socket.emit('metrics', await serverStats());
|
||||
|
||||
}, 1000);
|
||||
|
||||
// send container metrics
|
||||
// send container list
|
||||
let ContainerList = setInterval(async () => {
|
||||
|
||||
let card_list = await containerList();
|
||||
|
||||
if (sent_list !== card_list) {
|
||||
sent_list = card_list;
|
||||
app.locals.install = '';
|
||||
|
@ -80,20 +72,14 @@ io.on('connection', (socket) => {
|
|||
// send container metrics
|
||||
let ContainerStats = setInterval(async () => {
|
||||
let container_stats = await containerStats();
|
||||
|
||||
|
||||
//split up the array to display the name and stats
|
||||
for (let i = 0; i < container_stats.length; i++) {
|
||||
socket.emit('container_stats', container_stats[i]);
|
||||
}
|
||||
|
||||
}, 1000);
|
||||
|
||||
|
||||
// play/pause/stop/restart container
|
||||
socket.on('clicked', (data) => {
|
||||
// Prevent multiple clicks
|
||||
if (clicked == true) { return; } clicked = true;
|
||||
|
||||
let buttonPress = {
|
||||
user: socket.request.session.user,
|
||||
role: socket.request.session.role,
|
||||
|
@ -101,22 +87,14 @@ io.on('connection', (socket) => {
|
|||
container: data.container,
|
||||
state: data.state
|
||||
}
|
||||
|
||||
console.log(buttonPress)
|
||||
|
||||
containerAction(buttonPress);
|
||||
|
||||
clicked = false;
|
||||
});
|
||||
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
clearInterval(ServerStats);
|
||||
clearInterval(ContainerList);
|
||||
clearInterval(ContainerStats);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
|
@ -1,5 +1,12 @@
|
|||
const User = require('../database/UserModel');
|
||||
const { appCard } = require('../components/appCard')
|
||||
const { exec, execSync } = require("child_process");
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const { install } = require('../functions/package_manager');
|
||||
|
||||
|
||||
const templates_json = require('../templates.json');
|
||||
let templates = templates_json.templates;
|
||||
|
||||
|
@ -60,7 +67,7 @@ exports.Apps = async function(req, res) {
|
|||
|
||||
|
||||
|
||||
exports.processApps = async function(req, res) {
|
||||
exports.searchApps = async function(req, res) {
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
// Get the user.
|
||||
|
@ -127,4 +134,63 @@ exports.processApps = async function(req, res) {
|
|||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.Install = async function (req, res) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
install(req.body);
|
||||
|
||||
let container_info = {
|
||||
name: req.body.name,
|
||||
service: req.body.service_name,
|
||||
state: 'installing',
|
||||
image: req.body.image,
|
||||
restart_policy: req.body.restart_policy
|
||||
}
|
||||
|
||||
let installCard = dashCard(container_info);
|
||||
|
||||
req.app.locals.install = installCard;
|
||||
|
||||
|
||||
// Redirect to the home page
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.Uninstall = async function (req, res) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
if (req.body.confirm == 'Yes') {
|
||||
exec(`docker compose -f ./appdata/${req.body.service_name}/docker-compose.yml down`, (error, stdout, stderr) => {
|
||||
if (error) { console.error(`error: ${error.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Redirect to the home page
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
161
functions/package_manager.js
Normal file
161
functions/package_manager.js
Normal file
|
@ -0,0 +1,161 @@
|
|||
const { writeFileSync, mkdirSync, readFileSync } = require("fs");
|
||||
const { exec, execSync } = require("child_process");
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
|
||||
|
||||
module.exports.install = async function (data) {
|
||||
|
||||
|
||||
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
|
||||
let { port0, port1, port2, port3, port4, port5 } = data;
|
||||
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
|
||||
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
|
||||
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
|
||||
|
||||
if (image.startsWith('https://')){
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
|
||||
console.log(`Downloaded stackfile: ${image}`);
|
||||
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
|
||||
let services = Object.keys(stackfile.services);
|
||||
|
||||
for ( let i = 0; i < services.length; i++ ) {
|
||||
try {
|
||||
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
|
||||
} catch { console.log('no env') }
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
let compose_file = `version: '3'`;
|
||||
compose_file += `\nservices:`
|
||||
compose_file += `\n ${service_name}:`
|
||||
compose_file += `\n container_name: ${name}`;
|
||||
compose_file += `\n image: ${image}`;
|
||||
|
||||
// Command
|
||||
if (command_check == 'on') {
|
||||
compose_file += `\n command: ${command}`
|
||||
}
|
||||
|
||||
// Network mode
|
||||
if (net_mode == 'host') {
|
||||
compose_file += `\n network_mode: 'host'`
|
||||
}
|
||||
else if (net_mode != 'host' && net_mode != 'docker') {
|
||||
compose_file += `\n network_mode: '${net_mode}'`
|
||||
}
|
||||
|
||||
// Restart policy
|
||||
if (restart_policy != '') {
|
||||
compose_file += `\n restart: ${restart_policy}`
|
||||
}
|
||||
|
||||
// Ports
|
||||
if ((port0 == 'on' || port1 == 'on' || port2 == 'on' || port3 == 'on' || port4 == 'on' || port5 == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n ports:`
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (data[`port${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Volumes
|
||||
if (volume0 == 'on' || volume1 == 'on' || volume2 == 'on' || volume3 == 'on' || volume4 == 'on' || volume5 == 'on') {
|
||||
compose_file += `\n volumes:`
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (data[`volume${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
compose_file += `\n environment:`
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`env${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Add labels
|
||||
if (label0 == 'on' || label1 == 'on' || label2 == 'on' || label3 == 'on' || label4 == 'on' || label5 == 'on' || label6 == 'on' || label7 == 'on' || label8 == 'on' || label9 == 'on' || label10 == 'on' || label11 == 'on') {
|
||||
compose_file += `\n labels:`
|
||||
}
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`label${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Add privileged mode
|
||||
|
||||
if (data.privileged == 'on') {
|
||||
compose_file += `\n privileged: true`
|
||||
}
|
||||
|
||||
|
||||
// Add hardware acceleration to the docker-compose file if one of the environment variables has the label DRINODE
|
||||
if (env0 == 'on' || env1 == 'on' || env2 == 'on' || env3 == 'on' || env4 == 'on' || env5 == 'on' || env6 == 'on' || env7 == 'on' || env8 == 'on' || env9 == 'on' || env10 == 'on' || env11 == 'on') {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`env${i}`] == 'on') {
|
||||
if (data[`env_${i}_name`] == 'DRINODE') {
|
||||
compose_file += `\n deploy:`
|
||||
compose_file += `\n resources:`
|
||||
compose_file += `\n reservations:`
|
||||
compose_file += `\n devices:`
|
||||
compose_file += `\n - driver: nvidia`
|
||||
compose_file += `\n count: 1`
|
||||
compose_file += `\n capabilities: [gpu]`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
|
||||
exec(`docker compose -f ./appdata/${name}/docker-compose.yml up -d`, (error, stdout, stderr) => {
|
||||
if (error) { console.error(`error: ${error.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
} catch { console.log('error creating directory or compose file') }
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.uninstall = async function (data) {
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
|
||||
|
||||
if (data.confirm == 'Yes') {
|
||||
exec(`docker compose -f ./appdata/${data.service_name}/docker-compose.yml down`, (error, stdout, stderr) => {
|
||||
if (error) { console.error(`error: ${error.message}`); return; }
|
||||
if (stderr) { console.error(`stderr: ${stderr}`); return; }
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Redirect to the home page
|
||||
res.redirect("/");
|
||||
} else {
|
||||
// Redirect to the login page
|
||||
res.redirect("/login");
|
||||
}
|
||||
}
|
150
functions/system_information.js
Normal file
150
functions/system_information.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
const { currentLoad, mem, networkStats, fsSize, dockerContainerStats } = require('systeminformation');
|
||||
var Docker = require('dockerode');
|
||||
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
|
||||
const { dashCard } = require('../components/dashCard');
|
||||
|
||||
|
||||
|
||||
module.exports.serverStats = async function () {
|
||||
const cpuUsage = await currentLoad();
|
||||
const ramUsage = await mem();
|
||||
const netUsage = await networkStats();
|
||||
const diskUsage = await fsSize();
|
||||
|
||||
const info = {
|
||||
cpu: Math.round(cpuUsage.currentLoad),
|
||||
ram: Math.round((ramUsage.active / ramUsage.total) * 100),
|
||||
tx: netUsage[0].tx_bytes,
|
||||
rx: netUsage[0].rx_bytes,
|
||||
disk: diskUsage[0].use,
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports.containerList = async function () {
|
||||
let card_list = '';
|
||||
|
||||
const data = await docker.listContainers({ all: true });
|
||||
for (const container of data) {
|
||||
let imageVersion = container.Image.split('/');
|
||||
let service = imageVersion[imageVersion.length - 1].split(':')[0];
|
||||
|
||||
let containerId = docker.getContainer(container.Id);
|
||||
let containerInfo = await containerId.inspect();
|
||||
|
||||
let open_ports = [];
|
||||
let external_port = 0;
|
||||
let internal_port = 0;
|
||||
|
||||
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
|
||||
open_ports.push(`${value[0].HostPort}`);
|
||||
external_port = value[0].HostPort;
|
||||
internal_port = key;
|
||||
|
||||
if ((external_port == undefined) || (internal_port == undefined)) {
|
||||
external_port = 0;
|
||||
internal_port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
let volumes = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Mounts)) {
|
||||
volumes.push(`${value.Source}: ${value.Destination}: ${value.RW}`);
|
||||
}
|
||||
|
||||
let environment_variables = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
|
||||
environment_variables.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
let labels = [];
|
||||
for (const [key, value] of Object.entries(containerInfo.Config.Labels)) {
|
||||
labels.push(`${key}: ${value}`);
|
||||
}
|
||||
|
||||
|
||||
let container_info = {
|
||||
name: container.Names[0].slice(1),
|
||||
service: service,
|
||||
id: container.Id,
|
||||
state: container.State,
|
||||
image: container.Image,
|
||||
external_port: external_port,
|
||||
internal_port: internal_port
|
||||
}
|
||||
|
||||
let dockerCard = dashCard(container_info);
|
||||
|
||||
card_list += dockerCard;
|
||||
|
||||
}
|
||||
|
||||
return card_list;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports.containerStats = async function () {
|
||||
|
||||
let container_stats = [];
|
||||
|
||||
const data = await docker.listContainers({ all: true });
|
||||
|
||||
for (const container of data) {
|
||||
|
||||
const stats = await dockerContainerStats(container.Id);
|
||||
let container_stat = {
|
||||
name: container.Names[0].slice(1),
|
||||
cpu: Math.round(stats[0].cpuPercent),
|
||||
ram: Math.round(stats[0].memPercent)
|
||||
}
|
||||
|
||||
//push stats to an array
|
||||
container_stats.push(container_stat);
|
||||
}
|
||||
return container_stats;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports.containerAction = async function (data) {
|
||||
|
||||
let { user, role, action, container, state } = data;
|
||||
|
||||
console.log(`${user} wants to: ${action} ${container}`);
|
||||
|
||||
if (role == 'admin') {
|
||||
var containerName = docker.getContainer(container);
|
||||
|
||||
if ((action == 'start') && (state == 'stopped')) {
|
||||
containerName.start();
|
||||
} else if ((action == 'start') && (state == 'paused')) {
|
||||
containerName.unpause();
|
||||
} else if ((action == 'stop') && (state != 'stopped')) {
|
||||
containerName.stop();
|
||||
} else if ((action == 'pause') && (state == 'running')) {
|
||||
containerName.pause();
|
||||
} else if ((action == 'pause') && (state == 'paused')) {
|
||||
containerName.unpause();
|
||||
} else if (action == 'restart') {
|
||||
containerName.restart();
|
||||
}
|
||||
} else {
|
||||
console.log('User is not an admin');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -4,15 +4,14 @@ const router = express.Router();
|
|||
const { Dashboard } = require("../controllers/dashboard");
|
||||
|
||||
const { AddSite, RemoveSite, RefreshSites, DisableSite, EnableSite } = require("../controllers/site_actions");
|
||||
const { Install, Uninstall } = require("../controllers/app_actions");
|
||||
|
||||
const {Apps, processApps} = require("../controllers/apps");
|
||||
const { Apps, searchApps, Install, Uninstall } = require("../controllers/apps");
|
||||
const { Users } = require("../controllers/users");
|
||||
const {Account} = require("../controllers/account");
|
||||
const {Settings} = require("../controllers/settings");
|
||||
const {Logout} = require("../controllers/logout");
|
||||
const {Login, processLogin} = require("../controllers/login");
|
||||
const {Register, processRegister} = require("../controllers/register");
|
||||
const { Account } = require("../controllers/account");
|
||||
const { Settings } = require("../controllers/settings");
|
||||
const { Logout } = require("../controllers/logout");
|
||||
const { Login, processLogin } = require("../controllers/login");
|
||||
const { Register, processRegister } = require("../controllers/register");
|
||||
|
||||
|
||||
|
||||
|
@ -31,7 +30,7 @@ router.post("/enablesite", EnableSite)
|
|||
router.get("/users", Users);
|
||||
|
||||
router.get("/apps", Apps);
|
||||
router.post("/apps", processApps);
|
||||
router.post("/apps", searchApps);
|
||||
|
||||
router.get("/settings", Settings);
|
||||
router.get("/account", Account);
|
||||
|
|
|
@ -2196,20 +2196,20 @@
|
|||
],
|
||||
"volumes": [
|
||||
{
|
||||
"container": "/opt",
|
||||
"bind": "/home/docker/kasm/opt"
|
||||
"bind": "/home/docker/kasm/opt",
|
||||
"container": "/opt"
|
||||
},
|
||||
{
|
||||
"container": "/profiles",
|
||||
"bind": "/home/docker/kasm/profiles"
|
||||
"bind": "/home/docker/kasm/profiles",
|
||||
"container": "/profiles"
|
||||
},
|
||||
{
|
||||
"container": "/dev/input",
|
||||
"bind": "/dev/input"
|
||||
"bind": "/dev/input",
|
||||
"container": "/dev/input"
|
||||
},
|
||||
{
|
||||
"container": "/run/udev/data",
|
||||
"bind": "/run/udev/data"
|
||||
"bind": "/run/udev/data",
|
||||
"container": "/run/udev/data"
|
||||
}
|
||||
],
|
||||
"restart_policy": "unless-stopped"
|
||||
|
@ -2225,7 +2225,7 @@
|
|||
"Tools"
|
||||
],
|
||||
"platform": "linux",
|
||||
"logo": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/libreoffice-logo.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/libreoffice.png",
|
||||
"image": "linuxserver/libreoffice:latest",
|
||||
"env": [
|
||||
{
|
||||
|
@ -2511,7 +2511,7 @@
|
|||
"Tools"
|
||||
],
|
||||
"platform": "linux",
|
||||
"logo": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/phpmyadmin-logo.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/phpmyadmin.png",
|
||||
"image": "linuxserver/phpmyadmin:latest",
|
||||
"env": [
|
||||
{
|
||||
|
@ -2561,9 +2561,9 @@
|
|||
"title": "Pidgin",
|
||||
"name": "pidgin",
|
||||
"note": "",
|
||||
"description": "Pidgin (https://pidgin.im/) is a chat program which lets you log into accounts on multiple chat networks simultaneously. This means that you can be chatting with friends on XMPP and sitting in an IRC channel at the same time.",
|
||||
"description": "Pidgin is a chat program which lets you log into accounts on multiple chat networks simultaneously. This means that you can be chatting with friends on XMPP and sitting in an IRC channel at the same time.",
|
||||
"platform": "linux",
|
||||
"logo": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/pidgin-logo.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/pidgin.png",
|
||||
"image": "linuxserver/pidgin:latest",
|
||||
"env": [
|
||||
{
|
||||
|
@ -2591,8 +2591,8 @@
|
|||
],
|
||||
"volumes": [
|
||||
{
|
||||
"container": "/config",
|
||||
"bind": "/home/docker/pidgin/config"
|
||||
"bind": "/home/docker/pidgin",
|
||||
"container": "/config"
|
||||
}
|
||||
],
|
||||
"restart_policy": "unless-stopped"
|
||||
|
@ -2602,9 +2602,9 @@
|
|||
"title": "Remmina",
|
||||
"name": "remmina",
|
||||
"note": "",
|
||||
"description": "[Remmina](https://remmina.org/) is a remote desktop client written in GTK, aiming to be useful for system administrators and travellers, who need to work with lots of remote computers in front of either large or tiny screens. Remmina supports multiple network protocols, in an integrated and consistent user interface. Currently RDP, VNC, SPICE, NX, XDMCP, SSH and EXEC are supported.",
|
||||
"description": "Remmina is a remote desktop client written in GTK, aiming to be useful for system administrators and travellers, who need to work with lots of remote computers in front of either large or tiny screens. Remmina supports multiple network protocols, in an integrated and consistent user interface. Currently RDP, VNC, SPICE, NX, XDMCP, SSH and EXEC are supported.",
|
||||
"platform": "linux",
|
||||
"logo": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/remmina-icon.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/remmina.png",
|
||||
"image": "linuxserver/remmina:latest",
|
||||
"env": [
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue