* Environment Variables and Labels are now unchecked by default.
* Support for Docker volumes.
* Fixed app uninstall.
* Fixed Proxy Manager.
* Updated functions to ignore the three DweebUI containers: DweebUI, DweebCache(redis), and DweebProxy(caddy).
* Visual updates: Tabs for networks, images, and volumes. Added 'update' option in container drop-down.
* Updated main.js to prevent javascript errors.
* Fix for templates using 'set' instead of 'default' in environment variables.
* Fixes for templates with no volumes or no labels.
* New README.md.
* New screenshots.
* Automatically persists data in docker volumes if there is no bind mount.
This commit is contained in:
lllllllillllllillll 2023-11-16 18:01:24 -08:00 committed by GitHub
parent 4cf7919056
commit c2c4fcca89
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 230 additions and 92 deletions

2
app.js
View file

@ -5,8 +5,10 @@ const app = express();
const routes = require("./routes");
const { serverStats, containerList, containerStats, containerAction } = require('./functions/system_information');
const { RefreshSites } = require('./controllers/site_actions');
let sent_list, clicked;
app.locals.site_list = '';
const redisClient = require('redis').createClient({
url: 'redis://DweebCache:6379',

1
caddyfiles/Caddyfile Normal file
View file

@ -0,0 +1 @@
import ./sites/*

View file

@ -139,11 +139,11 @@ function appCard(data) {
try {
let volumes = data.volumes[i];
let volume_check = volumes ? "checked" : "";
let volume_bind = volumes.bind.split(":")[0] ? volumes.bind.split(":")[0] : "";
let volume_container = volumes.container.split(":")[0] ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw"
let volume_bind = volumes.bind ? volumes.bind : "";
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
let volume_readwrite = "rw";
if ((volumes.readonly == true) || (volumes.container.endsWith(":ro"))) {
if (volumes.readonly == true) {
volume_readwrite = "ro";
}
@ -168,6 +168,7 @@ function appCard(data) {
let env = data.env[i];
let env_check = "";
let env_default = env.default ? env.default : "";
if (env.set) { env_default = env.set;}
let env_description = env.description ? env.description : "";
let env_label = env.label ? env.label : "";
let env_name = env.name ? env.name : "";

View file

@ -1,10 +1,8 @@
services:
dweebui:
container_name: DweebUI
build:
context: .
environment:
NODE_ENV: production
image: lllllllillllllillll/dweebui:v0.05
restart: unless-stopped
ports:
- 8000:8000
depends_on:
@ -13,6 +11,8 @@ services:
- cache
volumes:
- dweebui:/app
- ./caddyfiles/Caddyfile:/app/caddyfiles/Caddyfile
- ./caddyfiles/sites:/app/caddyfiles/sites
- /var/run/docker.sock:/var/run/docker.sock
cache:
container_name: DweebCache
@ -21,7 +21,20 @@ services:
command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
volumes:
- cache:/data
proxy:
container_name: DweebProxy
image: caddy:2.4.5-alpine
depends_on:
- dweebui
restart: unless-stopped
network_mode: host
volumes:
- caddy:/data
- caddy:/config
- ./caddyfiles/Caddyfile:/etc/caddy/Caddyfile
- ./caddyfiles/sites:/etc/caddy/sites
volumes:
dweebui:
cache:
cache:
caddy:

View file

@ -144,6 +144,8 @@ exports.Install = async function (req, res) {
if (req.session.role == "admin") {
console.log(`Starting install for: ${req.body.name}`)
install(req.body);
let container_info = {

View file

@ -13,7 +13,8 @@ exports.Dashboard = async function (req, res) {
name: user.first_name + ' ' + user.last_name,
role: user.role,
avatar: user.avatar,
isLoggedIn: true
isLoggedIn: true,
site_list: req.app.locals.site_list,
});
} else {
// Redirect to the login page

View file

@ -1,7 +1,7 @@
const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
const { execSync } = require("child_process");
const { siteCard } = require('../components/siteCard');
const { containerExec } = require('../functions/system_information')
exports.AddSite = async function (req, res) {
@ -20,31 +20,54 @@ exports.AddSite = async function (req, res) {
caddyfile += `\n\t}`
caddyfile += `\n}`
// save caddyfile
writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
// format caddyfile
execSync(`docker exec caddy caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
if (stdout) { console.log(`stdout:\n${stdout}`); return; }
console.log(`Formatted ${domain}.Caddyfile`)
let format = {
container: 'DweebProxy',
command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
}
await containerExec(format, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Formatted ${domain}.Caddyfile`);
});
///////////////// convert caddyfile to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
}
await containerExec(convert, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Converted ${domain}.Caddyfile to JSON`);
});
////////////// reload caddy
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Reloaded Caddy Config`);
});
let site = siteCard(type, domain, host, port, 0);
// reload caddy config to enable new site
execSync(`docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
if (stdout) { console.log(`stdout:\n${stdout}`); return; }
console.log(`reloaded caddy config`)
});
req.app.locals.site_list += site;
// append the site to site_list.ejs
appendFileSync('./views/partials/site_list.ejs', site, function (err) { console.log(err) });
res.redirect("/");
} else {
@ -61,22 +84,20 @@ exports.RemoveSite = async function (req, res) {
for (const [key, value] of Object.entries(req.body)) {
console.log(`${key}: ${value}`);
execSync(`rm /home/docker/caddy/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${value}.Caddyfile`);
});
}
// reload caddy config to disable sites
try {
execSync(`docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`reloaded caddy config`)
}); } catch (error) { console.log("No sites to reload") }
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload);
console.log('Removed Site(s)')
@ -98,21 +119,15 @@ exports.RefreshSites = async function (req, res) {
// Clear site_list.ejs
writeFileSync('./views/partials/site_list.ejs', '', function (err) {
if (err) {
console.log(err);
} else {
console.log('site_list.ejs has been cleared');
}
});
req.app.locals.site_list = "";
// check if /home/docker/caddy/sites/ contains any .json files, then delete them
// check if ./caddyfiles/sites contains any .json files, then delete them
try {
let files = readdirSync('/home/docker/caddy/sites/');
let files = readdirSync('./caddyfiles/sites/');
files.forEach(file => {
if (file.includes(".json")) {
execSync(`rm /home/docker/caddy/sites/${file}`, (err, stdout, stderr) => {
execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${file}`);
@ -122,23 +137,25 @@ exports.RefreshSites = async function (req, res) {
} catch (error) { console.log("No .json files to delete") }
// get list of Caddyfiles
let sites = readdirSync('/home/docker/caddy/sites/');
let sites = readdirSync('./caddyfiles/sites/');
sites.forEach(site_name => {
// convert the caddyfile of each site to json
execSync(`docker exec caddy caddy adapt --config /etc/caddy/sites/${site_name} --pretty >> /home/docker/caddy/sites/${site_name}.json`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`stdout:\n${stdout}`);
});
// read the json file
let site_file = readFileSync(`/home/docker/caddy/sites/${site_name}.json`, 'utf8');
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
}
containerExec(convert);
try {
// read the json file
let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
// fix whitespace and parse the json file
site_file = site_file.replace(/ /g, " ");
site_file = JSON.parse(site_file);
} catch (error) { console.log("No .json file to read") }
// get the domain, type, host, and port from the json file
try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
@ -149,13 +166,13 @@ exports.RefreshSites = async function (req, res) {
// build the site card
let site = siteCard(type, domain, host, port, id);
// append the site card to site_list.ejs
appendFileSync('./views/partials/site_list.ejs', site, function (err) { console.log(err) });
// append the site card to site_list
req.app.locals.site_list += site;
id++;
});
res.redirect("/");
} else {
// Redirect to the login page

View file

@ -9,6 +9,7 @@ var DockerodeCompose = require('dockerode-compose');
module.exports.install = async function (data) {
console.log(`[Start of install function]`);
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
let { port0, port1, port2, port3, port4, port5 } = data;
@ -16,6 +17,8 @@ module.exports.install = async function (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;
let docker_volumes = [];
if (image.startsWith('https://')){
mkdirSync(`./appdata/${name}`, { recursive: true });
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
@ -71,9 +74,18 @@ module.exports.install = async function (data) {
compose_file += `\n volumes:`
for (let i = 0; i < 6; i++) {
if (data[`volume${i}`] == 'on') {
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
}
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
docker_volumes.push(`${name}_${volume_name}`);
}
}
}
@ -122,6 +134,22 @@ module.exports.install = async function (data) {
}
}
// add any docker volumes to the docker-compose file
if ( docker_volumes.length > 0 ) {
compose_file += `\n`
compose_file += `\nvolumes:`
// check docker_volumes for duplicates and remove them completely
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
for (let i = 0; i < docker_volumes.length; i++) {
if ( docker_volumes[i] != '') {
compose_file += `\n ${docker_volumes[i]}:`
}
}
}
try {
mkdirSync(`./appdata/${name}`, { recursive: true });
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });

View file

@ -32,7 +32,7 @@ module.exports.containerList = async function () {
for (const container of data) {
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache') && (container.Names[0].slice(1) != 'DweebProxy')) {
let imageVersion = container.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
@ -42,15 +42,18 @@ module.exports.containerList = async function () {
// Get ports //////////////////////////
let ports_list = [];
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check : 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
try {
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
let ports = {
check : 'checked',
external: value[0].HostPort,
internal: key.split('/')[0],
protocol: key.split('/')[1]
}
ports_list.push(ports);
}
ports_list.push(ports);
}
} catch { console.log('no ports') }
for (let i = 0; i < 12; i++) {
if (ports_list[i] == undefined) {
let ports = {
@ -66,15 +69,15 @@ module.exports.containerList = async function () {
// Get volumes ////////////////////////
let volumes_list = [];
for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
let volumes = {
check : 'checked',
bind: value.split(':')[0],
container: value.split(':')[1],
readwrite: value.split(':')[2]
}
volumes_list.push(volumes);
}
try { for (const [key, value] of Object.entries(containerInfo.HostConfig.Binds)) {
let volumes = {
check : 'checked',
bind: value.split(':')[0],
container: value.split(':')[1],
readwrite: value.split(':')[2]
}
volumes_list.push(volumes);
}} catch { console.log('no volumes') }
for (let i = 0; i < 12; i++) {
if (volumes_list[i] == undefined) {
let volumes = {
@ -90,14 +93,14 @@ module.exports.containerList = async function () {
// Get environment variables.
let environment_variables = [];
for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
try { for (const [key, value] of Object.entries(containerInfo.Config.Env)) {
let env = {
check : 'checked',
name: value.split('=')[0],
default: value.split('=')[1]
}
environment_variables.push(env);
}
}} catch { console.log('no env') }
for (let i = 0; i < 12; i++) {
if (environment_variables[i] == undefined) {
let env = {
@ -169,7 +172,7 @@ module.exports.containerStats = async function () {
for (const container of data) {
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache')) {
if ((container.Names[0].slice(1) != 'DweebUI') && (container.Names[0].slice(1) != 'DweebCache') && (container.Names[0].slice(1) != 'DweebProxy')) {
const stats = await dockerContainerStats(container.Id);
let container_stat = {
name: container.Names[0].slice(1),
@ -218,5 +221,36 @@ module.exports.containerAction = async function (data) {
module.exports.containerExec = async function (data) {
let { container, command } = data;
var containerName = docker.getContainer(container);
var options = {
Cmd: ['/bin/sh', '-c', command],
AttachStdout: true,
AttachStderr: true,
Tty: true
};
containerName.exec(options, function (err, exec) {
if (err) return;
exec.start(function (err, stream) {
if (err) return;
containerName.modem.demuxStream(stream, process.stdout, process.stderr);
exec.inspect(function (err, data) {
if (err) return;
});
});
});
}

View file

@ -910,18 +910,13 @@
"name": "PGID",
"set": "1000"
},
{
"label": "UMASK_SET",
"name": "UMASK_SET",
"set": "000"
},
{
"label": "TZ",
"name": "TZ",
"set": "America/Chicago"
}
],
"image": "linuxserver/deluge:latest",
"image": "lscr.io/linuxserver/deluge:latest",
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/deluge.png",
"platform": "linux",
"title": "Deluge",
@ -934,6 +929,11 @@
{
"container": "/downloads"
}
],
"ports": [
"8112/tcp",
"6881/tcp",
"6881/udp"
]
},
{
@ -3705,11 +3705,11 @@
"container": "/data"
},
{
"bind": "/home/docker/caddy/Caddyfile",
"bind": "caddy/Caddyfile",
"container": "/etc/caddy/Caddyfile"
},
{
"bind": "/home/docker/caddy/sites",
"bind": "caddy/sites",
"container": "/etc/caddy/sites"
}
]

View file

@ -224,7 +224,7 @@
</thead>
<tbody>
<%- include('../partials/site_list.ejs') %>
<%- site_list %>
</tbody>
</table>
@ -248,7 +248,7 @@
</form>
<p class="m-0 text-muted ms-auto">Imported: /home/docker/caddy/Caddyfile</p>
<p class="m-0 text-muted ms-auto">./caddyfiles/Caddyfile</p>
</div>
</div>

View file

@ -206,6 +206,45 @@
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-world" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M3.6 9h16.8" /><path d="M3.6 15h16.8" /><path d="M11.5 3a17 17 0 0 0 0 18" /><path d="M12.5 3a17 17 0 0 1 0 18" /></svg>
</span>
<span class="nav-link-title">
Networks
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-augmented-reality" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M12 12.5l4 -2.5" /><path d="M8 10l4 2.5v4.5l4 -2.5v-4.5l-4 -2.5z" /><path d="M8 10v4.5l4 2.5" /></svg>
</span>
<span class="nav-link-title">
Images
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span
class="nav-link-icon d-md-none d-lg-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
</span>
<span class="nav-link-title">
Volumes
</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#navbar-help" data-bs-toggle="dropdown"
data-bs-auto-close="outside" role="button" aria-expanded="false">