Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip #1

Merged
merged 27 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
  • Loading branch information
louislam committed Oct 26, 2023
commit 7d1da2ad99c3711baf1d6740cdeb476e91d1eab8
17 changes: 17 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Should be identical to .gitignore
.env
node_modules
dist
frontend-dist
.idea
data
tmp
/private

# Docker extra
docker
frontend
.editorconfig
.eslintrc.cjs
.gitignore
README.md
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,6 @@ module.exports = {
"one-var": [ "error", "never" ],
"max-statements-per-line": [ "error", { "max": 1 }],
"@typescript-eslint/ban-ts-comment": "off",
"prefer-const" : "off",
},
};
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# dotenv environment variable files
# Should update .dockerignore as well
.env
node_modules
dist
frontend-dist
.idea
data
tmp
/private

34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
<div align="center" width="100%">
<img src="./frontend/public/icon.svg" width="128" alt="" />
</div>

# Dockge

## Features
A fancy, easy-to-use and reactive docker stack (`docker-compose.yml`) manager.

## ⭐ Features

- Easy-to-use
- Fancy UI
- Focus on `docker-compose` stack management
- Focus on `docker-compose.yml` stack management
- Interactive editor for `docker-compose.yml` files
- Easy to expose your service to the internet with https
- Interactive web terminal for containers and any docker commands
- Reactive - Everything is just responsive. Progress and terminal output are in real-time
- Easy-to-use & fancy UI - If you love Uptime Kuma's UI, you will love this too

## Installation

## Motivations

- Want to build my dream web-based container manager
- Want to try next gen runtime like Deno or Bun, but I chose Deno because at this moment, Deno is more stable and Jetbrains IDE support is better.
- Full TypeScript and ES module
- Try DaisyUI + TailwindCSS
- Try ES Module and TypeScript in 2023
- I have been using Portainer for some time, but I am sometimes not satisfied with it. For example, sometimes when I deploy a stack, it keeps spinning the loading icon for a few minutes, and I don't know what's going on.

If you love this project, please consider giving this project a ⭐.

## Dockge?
## More Ideas?

Naming idea is coming from Twitch emotes. There are many emotes sound like this such as `bedge` and `sadge`.
- Container file manager
- App store for yaml templates
- Container stats
- Get app icons
- Switch Docker context
- Zero-config private docker registry
194 changes: 121 additions & 73 deletions backend/dockge-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,20 @@ import { R } from "redbean-node";
import { genSecret, isDev } from "./util-common";
import { generatePasswordHash } from "./password-hash";
import { Bean } from "redbean-node/dist/bean";
import { DockgeSocket } from "./util-server";
import { Arguments, Config, DockgeSocket } from "./util-server";
import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler";
import { Terminal } from "./terminal";

export interface Arguments {
sslKey? : string;
sslCert? : string;
sslKeyPassphrase? : string;
port? : number;
hostname? : string;
dataDir? : string;
}
import expressStaticGzip from "express-static-gzip";
import path from "path";
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
import { Stack } from "./stack";

export class DockgeServer {
app : Express;
httpServer : http.Server;
packageJSON : PackageJson;
io : socketIO.Server;
config : Arguments;
indexHTML : string;
terminal : Terminal;
config : Config;
indexHTML : string = "";

/**
* List of express routers
Expand All @@ -55,6 +48,7 @@ export class DockgeServer {
socketHandlerList : SocketHandler[] = [
new MainSocketHandler(),
new DockerSocketHandler(),
new TerminalSocketHandler(),
];

/**
Expand All @@ -64,19 +58,29 @@ export class DockgeServer {

jwtSecret? : string;

stacksDir : string = "";

/**
*
*/
constructor() {
// Catch unexpected errors here
let unexpectedErrorHandler = (error : unknown) => {
console.trace(error);
console.error("If you keep encountering errors, please report to https://github.com/louislam/uptime-kuma/issues");
};
process.addListener("unhandledRejection", unexpectedErrorHandler);
process.addListener("uncaughtException", unexpectedErrorHandler);

if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "production";
}

// Log NODE ENV
log.info("server", "NODE_ENV: " + process.env.NODE_ENV);

// Load arguments
const args = this.config = parse<Arguments>({
// Define all possible arguments
let args = parse<Arguments>({
sslKey: {
type: String,
optional: true,
Expand All @@ -103,63 +107,37 @@ export class DockgeServer {
}
});

this.config = args as Config;

// Load from environment variables or default values if args are not set
args.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
args.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
args.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
args.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
args.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
args.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined;
this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined;
this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001;
this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";

log.debug("server", args);
log.debug("server", this.config);

this.packageJSON = packageJSON as PackageJson;

this.initDataDir();

this.terminal = new Terminal(this);
}

/**
*
*/
async serve() {
// Connect to database
try {
await Database.init(this);
this.indexHTML = fs.readFileSync("./frontend-dist/index.html").toString();
} catch (e) {
log.error("server", "Failed to prepare your database: " + e.message);
process.exit(1);
}

// First time setup if needed
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);

if (! jwtSecretBean) {
log.info("server", "JWT secret is not found, generate one.");
jwtSecretBean = await this.initJWTSecret();
log.info("server", "Stored JWT secret into database");
} else {
log.debug("server", "Load JWT secret from database.");
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'frontend-dist/index.html', did you install correctly?");
process.exit(1);
}
}

this.jwtSecret = jwtSecretBean.value;

const userCount = (await R.knex("user").count("id as count").first()).count;

log.debug("server", "User count: " + userCount);

// If there is no record in user table, it is a new Dockge instance, need to setup
if (userCount == 0) {
log.info("server", "No user, need setup");
this.needSetup = true;
}
// Create all the necessary directories
this.initDataDir();

// Create express
this.app = express();

// Create HTTP server
if (this.config.sslKey && this.config.sslCert) {
log.info("server", "Server Type: HTTPS");
this.httpServer = https.createServer({
Expand All @@ -172,22 +150,23 @@ export class DockgeServer {
this.httpServer = http.createServer(this.app);
}

try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
log.error("server", "Error: Cannot find 'dist/index.html', did you install correctly?");
process.exit(1);
}
}

// Binding Routers
for (const router of this.routerList) {
this.app.use(router.create(this.app, this));
}

let cors = undefined;
// Static files
this.app.use("/", expressStaticGzip("frontend-dist", {
enableBrotli: true,
}));

// Universal Route Handler, must be at the end of all express routes.
this.app.get("*", async (_request, response) => {
response.send(this.indexHTML);
});

// Allow all CORS origins in development
let cors = undefined;
if (isDev) {
cors = {
origin: "*",
Expand Down Expand Up @@ -215,6 +194,53 @@ export class DockgeServer {
}
});

this.io.on("disconnect", () => {

});

}

prepareServer() {

}

/**
*
*/
async serve() {
// Connect to database
try {
await Database.init(this);
} catch (e) {
log.error("server", "Failed to prepare your database: " + e.message);
process.exit(1);
}

// First time setup if needed
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [
"jwtSecret",
]);

if (! jwtSecretBean) {
log.info("server", "JWT secret is not found, generate one.");
jwtSecretBean = await this.initJWTSecret();
log.info("server", "Stored JWT secret into database");
} else {
log.debug("server", "Load JWT secret from database.");
}

this.jwtSecret = jwtSecretBean.value;

const userCount = (await R.knex("user").count("id as count").first()).count;

log.debug("server", "User count: " + userCount);

// If there is no record in user table, it is a new Dockge instance, need to setup
if (userCount == 0) {
log.info("server", "No user, need setup");
this.needSetup = true;
}

// Listen
this.httpServer.listen(5001, this.config.hostname, () => {
if (this.config.hostname) {
Expand Down Expand Up @@ -349,14 +375,21 @@ export class DockgeServer {
* Initialize the data directory
*/
initDataDir() {
if (! fs.existsSync(this.config.dataDir)) {
fs.mkdirSync(this.config.dataDir, { recursive: true });
}

// Check if a directory
if (!fs.lstatSync(this.config.dataDir).isDirectory()) {
throw new Error(`Fatal error: ${this.config.dataDir} is not a directory`);
}

if (! fs.existsSync(this.config.dataDir)) {
fs.mkdirSync(this.config.dataDir, { recursive: true });
// Create data/stacks directory
this.stacksDir = path.join(this.config.dataDir, "stacks");
if (!fs.existsSync(this.stacksDir)) {
fs.mkdirSync(this.stacksDir, { recursive: true });
}

log.info("server", `Data Dir: ${this.config.dataDir}`);
}

Expand All @@ -378,4 +411,19 @@ export class DockgeServer {
await R.store(jwtSecretBean);
return jwtSecretBean;
}

sendStackList(socket : DockgeSocket) {
let room = socket.userID.toString();
let stackList = Stack.getStackList(this);
let list = {};

for (let stack of stackList) {
list[stack.name] = stack.toSimpleJSON();
}

this.io.to(room).emit("stackList", {
ok: true,
stackList: list,
});
}
}
2 changes: 1 addition & 1 deletion backend/routers/main-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class MainRouter extends Router {
const router = express.Router();

router.get("/", (req, res) => {

res.send(server.indexHTML);
});

return router;
Expand Down
Loading