Compare commits

..

27 Commits

Author SHA1 Message Date
d8d25563ba wip 2023-11-11 22:10:22 +08:00
83d58cd363 Fix composerize 2023-11-11 21:29:09 +08:00
caa82bbad5 wip 2023-11-11 19:14:27 +08:00
2530cac989 wip 2023-11-11 11:17:26 +08:00
06307956ca wip 2023-11-10 22:06:58 +08:00
9b9234434e wip 2023-11-10 21:52:38 +08:00
a12c6dc033 wip 2023-11-10 18:16:52 +08:00
eb6db8b31e wip 2023-11-07 21:23:05 +08:00
015e4c21f9 wip 2023-11-07 21:23:05 +08:00
7a63d59ef8 Update README.md 2023-11-07 00:13:07 +08:00
ecbbdae7ab wip 2023-11-06 23:55:22 +08:00
e7abbcbefa Update README.md 2023-11-06 23:52:43 +08:00
59adcc148c wip 2023-11-06 23:51:08 +08:00
5e9065a4d6 wip 2023-11-06 23:25:54 +08:00
35a79ea8a6 wip 2023-11-06 23:21:18 +08:00
0c9fc4ead2 wip 2023-11-06 23:18:11 +08:00
1c5ff7914b wip 2023-11-06 23:16:08 +08:00
f7c66a476c wip 2023-11-06 23:14:02 +08:00
5daa6fd788 wip 2023-11-06 23:07:52 +08:00
c2ec9ac7f4 wip 2023-11-06 22:54:20 +08:00
21e736459e wip 2023-11-06 21:24:06 +08:00
d7f4873405 wip 2023-11-06 16:00:20 +08:00
2ed739b1b9 wip 2023-11-06 15:15:55 +08:00
314630724b wip 2023-11-06 01:18:02 +08:00
e67d08b7b3 wip 2023-10-29 15:25:52 +08:00
7d1da2ad99 wip 2023-10-26 13:23:45 +08:00
5f70fa6baf wip 2023-10-23 19:30:58 +08:00
35 changed files with 295 additions and 942 deletions

View File

@ -92,9 +92,6 @@ module.exports = {
"one-var": [ "error", "never" ],
"max-statements-per-line": [ "error", { "max": 1 }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [ "warn", {
"args": "none"
}],
"prefer-const" : "off",
},
};

View File

@ -1,60 +0,0 @@
name: Node.js CI - Dockge
on:
push:
branches: [master]
paths-ignore:
- '*.md'
pull_request:
branches: [master]
paths-ignore:
- '*.md'
jobs:
ci:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20.x] # Can be changed
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{matrix.node}}
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm run lint
- name: Check Typescript
run: pnpm run check-ts
# more things can be add later like tests etc..

View File

@ -4,19 +4,13 @@
# Dockge
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
![GitHub Repo stars](https://img.shields.io/github/stars/louislam/dockge?logo=github) ![GitHub issues](https://img.shields.io/github/issues/louislam/dockge?logo=github) ![GitHub pull requests](https://img.shields.io/github/issues-pr/louislam/dockge?logo=github) ![Docker Pulls](https://img.shields.io/docker/pulls/louislam/dockge?logo=docker) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/louislam/dockge?logo=docker) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/louislam/dockge/master?logo=github) ![GitHub](https://img.shields.io/github/license/louislam/dockge?logo=github)
A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager.
<img src="https://github.com/louislam/dockge/assets/1336778/26a583e1-ecb1-4a8d-aedf-76157d714ad7" width="900" alt="" />
View Video: https://youtu.be/AWAlOQeNpgU?t=48
## ⭐ Features
- Manage `compose.yaml`
- Create/Edit/Start/Stop/Restart/Delete
- Update Docker Images
- Interactive Editor for `compose.yaml`
- Interactive Web Terminal
- Reactive
@ -24,28 +18,20 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
- Easy-to-use & fancy UI
- If you love Uptime Kuma's UI/UX, you will love this too
- Convert `docker run ...` commands into `compose.yaml`
- File based structure
- Dockge won't kidnap your compose files, they stored on your drive as usual. You can interact with them using normal `docker compose` commands
<img src="https://github.com/louislam/dockge/assets/1336778/cc071864-592e-4909-b73a-343a57494002" width=300 />
![](https://github.com/louislam/dockge/assets/1336778/89fc1023-b069-42c0-a01c-918c495f1a6a)
## 🔧 How to Install
Requirements:
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman
- (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/)
- (Podman only) podman-docker (Debian: `apt install podman-docker`)
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended
- [Docker Compose V2](https://docs.docker.com/compose/install/linux/)
- OS:
- As long as you can run Docker CE / Podman, it should be fine, but:
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye or higher
- As long as you can run Docker CE, it should be fine, but:
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye
- Arch: armv7, arm64, amd64 (a.k.a x86_64)
### Basic
- Default Stacks Directory: `/opt/stacks`
- Default Port: 5001
Default stacks directory is `/opt/stacks`.
```
# Create a directory that stores your stacks and stores dockge's compose.yaml
@ -58,16 +44,16 @@ curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --out
# Start Server
docker compose up -d
# If you are using docker-compose V1 or Podman
# If you are using docker-compose V1
# docker-compose up -d
```
Dockge is now running on http://localhost:5001
### Advanced
If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes.
For exmaples, if you want to store your stacks in `/my-stacks`:
```yaml
version: "3.8"
services:
@ -75,45 +61,27 @@ services:
image: louislam/dockge:1
restart: unless-stopped
ports:
# Host Port:Container Port
- 5001:5001
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data
# If you want to use private registries, you need to share the auth file with Dockge:
# - /root/.docker/:/root/.docker
# Your stacks directory in the host (The paths inside container must be the same as the host)
# ⚠️⚠️ If you did it wrong, your data could end up be written into a wrong path.
# ✔️✔️✔️✔️ CORRECT EXAMPLE: - /my-stacks:/my-stacks (Both paths match)
# ❌❌❌❌ WRONG EXAMPLE: - /docker:/my-stacks (Both paths do not match)
- /opt/stacks:/opt/stacks
# Your stacks directory in the host
# (The paths inside container must be the same as the host)
- /my-stacks:/my-stacks
environment:
# Tell Dockge where is your stacks directory
- DOCKGE_STACKS_DIR=/opt/stacks
- DOCKGE_STACKS_DIR=/my-stacks
```
## How to Update
```bash
cd /opt/dockge
cd /opt/stacks
docker compose pull
docker compose up -d
```
## Screenshots
![](https://github.com/louislam/dockge/assets/1336778/e7ff0222-af2e-405c-b533-4eab04791b40)
![](https://github.com/louislam/dockge/assets/1336778/7139e88c-77ed-4d45-96e3-00b66d36d871)
![](https://github.com/louislam/dockge/assets/1336778/f019944c-0e87-405b-a1b8-625b35de1eeb)
![](https://github.com/louislam/dockge/assets/1336778/a4478d23-b1c4-4991-8768-1a7cad3472e3)
## Motivations
- I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear.
@ -122,22 +90,16 @@ docker compose up -d
If you love this project, please consider giving this project a ⭐.
## 🗣️
### Bug Report
https://github.com/louislam/dockge/issues
### Ask for Help / Discussions
https://github.com/louislam/dockge/discussions
## FAQ
#### "Dockge"?
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Dodge`.
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Badge` but replacing with `Dock` - `Dock-ge`.
The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`. They are all ending with `-ge`.
If you are not comfortable with the pronunciation, you can call it `Dockage`
#### Can I manage a single container without `compose.yaml`?
The main objective of Dockge is that try to use docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI.

View File

@ -5,7 +5,6 @@ import fs from "fs";
import path from "path";
import knex from "knex";
// @ts-ignore
import Dialect from "knex/lib/dialects/sqlite3/index.js";
import sqlite from "@louislam/sqlite3";
@ -13,11 +12,6 @@ import { sleep } from "./util-common";
interface DBConfig {
type?: "sqlite" | "mysql";
hostname?: string;
port?: string;
database?: string;
username?: string;
password?: string;
}
export class Database {
@ -25,7 +19,7 @@ export class Database {
* SQLite file path (Default: ./data/dockge.db)
* @type {string}
*/
static sqlitePath : string;
static sqlitePath;
static noReject = true;
@ -57,7 +51,7 @@ export class Database {
* @typedef {string|undefined} envString
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
*/
static readDBConfig() : DBConfig {
static readDBConfig() {
const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8");
const dbConfig = JSON.parse(dbConfigString);
@ -73,10 +67,10 @@ export class Database {
/**
* @typedef {string|undefined} envString
* @param dbConfig the database configuration that should be written
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
* @returns {void}
*/
static writeDBConfig(dbConfig : DBConfig) {
static writeDBConfig(dbConfig) {
fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
}
@ -86,17 +80,14 @@ export class Database {
* @param {boolean} noLog Should logs not be output?
* @returns {Promise<void>}
*/
static async connect(autoloadModels = true) {
static async connect(autoloadModels = true, noLog = false) {
const acquireConnectionTimeout = 120 * 1000;
let dbConfig : DBConfig;
let dbConfig;
try {
dbConfig = this.readDBConfig();
Database.dbConfig = dbConfig;
} catch (err) {
if (err instanceof Error) {
log.warn("db", err.message);
}
log.warn("db", err.message);
dbConfig = {
type: "sqlite",
};
@ -185,15 +176,13 @@ export class Database {
directory: Database.knexMigrationsPath,
});
} catch (e) {
if (e instanceof Error) {
// Allow missing patch files for downgrade or testing pr.
if (e.message.includes("the following files are missing:")) {
log.warn("db", e.message);
log.warn("db", "Database migration failed, you may be downgrading Dockge.");
} else {
log.error("db", "Database migration failed");
throw e;
}
// Allow missing patch files for downgrade or testing pr.
if (e.message.includes("the following files are missing:")) {
log.warn("db", e.message);
log.warn("db", "Database migration failed, you may be downgrading Dockge.");
} else {
log.error("db", "Database migration failed");
throw e;
}
}
}

View File

@ -60,7 +60,7 @@ export class DockgeServer {
*/
needSetup = false;
jwtSecret : string = "";
jwtSecret? : string;
stacksDir : string = "";
@ -129,7 +129,7 @@ export class DockgeServer {
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 || Number(process.env.DOCKGE_PORT) || 5001;
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/";
this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir;
@ -218,7 +218,7 @@ export class DockgeServer {
log.debug("auth", "check auto login");
if (await Settings.get("disableAuth")) {
log.info("auth", "Disabled Auth: auto login to admin");
this.afterLogin(socket as DockgeSocket, await R.findOne("user") as User);
this.afterLogin(socket as DockgeSocket, await R.findOne("user"));
socket.emit("autoLogin");
} else {
log.debug("auth", "need auth");
@ -253,9 +253,7 @@ export class DockgeServer {
try {
await Database.init(this);
} catch (e) {
if (e instanceof Error) {
log.error("server", "Failed to prepare your database: " + e.message);
}
log.error("server", "Failed to prepare your database: " + e.message);
process.exit(1);
}
@ -293,7 +291,7 @@ export class DockgeServer {
}
// Run every 5 seconds
Cron("*/2 * * * * *", {
const job = Cron("*/2 * * * * *", {
protect: true, // Enabled over-run protection.
}, () => {
log.debug("server", "Cron job running");
@ -378,9 +376,7 @@ export class DockgeServer {
return process.env.TZ;
}
} catch (e) {
if (e instanceof Error) {
log.warn("timezone", e.message + " in process.env.TZ");
}
log.warn("timezone", e.message + " in process.env.TZ");
}
const timezone = await Settings.get("serverTimezone");
@ -393,9 +389,7 @@ export class DockgeServer {
return timezone;
}
} catch (e) {
if (e instanceof Error) {
log.warn("timezone", e.message + " in settings");
}
log.warn("timezone", e.message + " in settings");
}
// Guess

View File

@ -17,7 +17,7 @@ export function generatePasswordHash(password : string) {
* @param {string} hash Hash to verify against
* @returns {boolean} Does the password match the hash?
*/
export function verifyPassword(password : string, hash : string) {
export function verifyPassword(password, hash) {
return bcrypt.compareSync(password, hash);
}
@ -37,7 +37,7 @@ export const SHAKE256_LENGTH = 16;
* @param {number} len Output length of the hash
* @returns {string} The hashed data in hex format
*/
export function shake256(data : string, len : number) {
export function shake256(data, len) {
if (!data) {
return "";
}

View File

@ -1,14 +1,8 @@
// "limit" is bugged in Typescript, use "limiter-es6-compat" instead
// See https://github.com/jhurliman/node-rate-limiter/issues/80
import { RateLimiter, RateLimiterOpts } from "limiter-es6-compat";
import { RateLimiter } from "limiter-es6-compat";
import { log } from "./log";
export interface KumaRateLimiterOpts extends RateLimiterOpts {
errorMessage : string;
}
export type KumaRateLimiterCallback = (err : object) => void;
class KumaRateLimiter {
errorMessage : string;
@ -17,7 +11,7 @@ class KumaRateLimiter {
/**
* @param {object} config Rate limiter configuration object
*/
constructor(config : KumaRateLimiterOpts) {
constructor(config) {
this.errorMessage = config.errorMessage;
this.rateLimiter = new RateLimiter(config);
}
@ -30,11 +24,11 @@ class KumaRateLimiter {
/**
* Should the request be passed through
* @param callback Callback function to call with decision
* @param {passCB} callback Callback function to call with decision
* @param {number} num Number of tokens to remove
* @returns {Promise<boolean>} Should the request be allowed?
*/
async pass(callback : KumaRateLimiterCallback, num = 1) {
async pass(callback, num = 1) {
const remainingRequests = await this.removeTokens(num);
log.info("rate-limit", "remaining requests: " + remainingRequests);
if (remainingRequests < 0) {

View File

@ -1,4 +1,4 @@
import { DockgeServer } from "../dockge-server";
import { DockgeServer } from "../dockgeServer";
import { Router } from "../router";
import express, { Express, Router as ExpressRouter } from "express";

View File

@ -1,6 +1,5 @@
import { R } from "redbean-node";
import { log } from "./log";
import { LooseObject } from "./util-common";
export class Settings {
@ -16,19 +15,20 @@ export class Settings {
* timestamp: 12345678
* },
* }
* @type {{}}
*/
static cacheList : LooseObject = {
static cacheList = {
};
static cacheCleaner? : NodeJS.Timeout;
static cacheCleaner = null;
/**
* Retrieve value of setting based on key
* @param key Key of setting to retrieve
* @returns Value
* @param {string} key Key of setting to retrieve
* @returns {Promise<any>} Value
*/
static async get(key : string) {
static async get(key) {
// Start cache clear if not started yet
if (!Settings.cacheCleaner) {
@ -72,12 +72,12 @@ export class Settings {
/**
* Sets the specified setting to specified value
* @param key Key of setting to set
* @param value Value to set to
* @param {string} key Key of setting to set
* @param {any} value Value to set to
* @param {?string} type Type of setting
* @returns {Promise<void>}
*/
static async set(key : string, value : object | string | number | boolean, type : string | null = null) {
static async set(key, value, type = null) {
let bean = await R.findOne("setting", " `key` = ? ", [
key,
@ -95,15 +95,15 @@ export class Settings {
/**
* Get settings based on type
* @param type The type of setting
* @returns Settings
* @param {string} type The type of setting
* @returns {Promise<Bean>} Settings
*/
static async getSettings(type : string) {
static async getSettings(type) {
const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
type,
]);
const result : LooseObject = {};
const result = {};
for (const row of list) {
try {
@ -118,11 +118,11 @@ export class Settings {
/**
* Set settings based on type
* @param type Type of settings to set
* @param data Values of settings
* @param {string} type Type of settings to set
* @param {object} data Values of settings
* @returns {Promise<void>}
*/
static async setSettings(type : string, data : LooseObject) {
static async setSettings(type, data) {
const keyList = Object.keys(data);
const promiseList = [];
@ -154,7 +154,7 @@ export class Settings {
* @param {string[]} keyList Keys to remove
* @returns {void}
*/
static deleteCache(keyList : string[]) {
static deleteCache(keyList) {
for (const key of keyList) {
delete Settings.cacheList[key];
}
@ -167,7 +167,7 @@ export class Settings {
static stopCacheCleaner() {
if (Settings.cacheCleaner) {
clearInterval(Settings.cacheCleaner);
Settings.cacheCleaner = undefined;
Settings.cacheCleaner = null;
}
}
}

View File

@ -1,11 +1,12 @@
import { SocketHandler } from "../socket-handler.js";
import { Socket } from "socket.io";
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
import { R } from "redbean-node";
import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
import { User } from "../models/user";
import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server";
import { checkLogin, DockgeSocket, doubleCheckPassword } from "../util-server";
import { passwordStrength } from "check-password-strength";
import jwt from "jsonwebtoken";
import { Settings } from "../settings";
@ -42,12 +43,10 @@ export class MainSocketHandler extends SocketHandler {
});
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
callback({
ok: false,
msg: e.message,
});
}
});
@ -58,7 +57,7 @@ export class MainSocketHandler extends SocketHandler {
log.info("auth", `Login by token. IP=${clientIP}`);
try {
const decoded = jwt.verify(token, server.jwtSecret) as JWTDecoded;
const decoded = jwt.verify(token, server.jwtSecret);
log.info("auth", "Username from JWT: " + decoded.username);
@ -92,13 +91,9 @@ export class MainSocketHandler extends SocketHandler {
});
}
} catch (error) {
if (!(error instanceof Error)) {
console.error("Unknown error:", error);
return;
}
log.error("auth", `Invalid token. IP=${clientIP}`);
if (error.message) {
log.error("auth", error.message + ` IP=${clientIP}`);
log.error("auth", error.message, `IP=${clientIP}`);
}
callback({
ok: false,
@ -154,7 +149,6 @@ export class MainSocketHandler extends SocketHandler {
}
if (data.token) {
// @ts-ignore
const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== data.token && verify) {
@ -217,12 +211,10 @@ export class MainSocketHandler extends SocketHandler {
});
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
callback({
ok: false,
msg: e.message,
});
}
});
@ -237,12 +229,10 @@ export class MainSocketHandler extends SocketHandler {
});
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
callback({
ok: false,
msg: e.message,
});
}
});
@ -272,24 +262,22 @@ export class MainSocketHandler extends SocketHandler {
server.sendInfo(socket);
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
callback({
ok: false,
msg: e.message,
});
}
});
}
async login(username : string, password : string) : Promise<User | null> {
async login(username : string, password : string) {
if (typeof username !== "string" || typeof password !== "string") {
return null;
}
const user = await R.findOne("user", " username = ? AND active = 1 ", [
username,
]) as User;
]);
if (user && verifyPassword(password, user.password)) {
// Upgrade the hash to bcrypt

View File

@ -38,12 +38,10 @@ export class TerminalSocketHandler extends SocketHandler {
throw new Error("Terminal not found or it is not a Interactive Terminal.");
}
} catch (e) {
if (e instanceof Error) {
errorCallback({
ok: false,
msg: e.message,
});
}
errorCallback({
ok: false,
msg: e.message,
});
}
});

View File

@ -24,7 +24,6 @@ export class Stack {
protected _status: number = UNKNOWN;
protected _composeYAML?: string;
protected _configFilePath?: string;
protected _composeFileName: string = "compose.yaml";
protected server: DockgeServer;
protected combinedTerminal? : Terminal;
@ -35,15 +34,6 @@ export class Stack {
this.name = name;
this.server = server;
this._composeYAML = composeYAML;
// Check if compose file name is different from compose.yaml
const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ];
for (const filename of supportedFileNames) {
if (fs.existsSync(path.join(this.path, filename))) {
this._composeFileName = filename;
break;
}
}
}
toJSON() : object {
@ -60,7 +50,6 @@ export class Stack {
status: this._status,
tags: [],
isManagedByDockge: this.isManagedByDockge,
composeFileName: this._composeFileName,
};
}
@ -83,9 +72,9 @@ export class Stack {
}
validate() {
// Check name, allows [a-z][0-9] _ - only
if (!this.name.match(/^[a-z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][0-9] _ - only");
// Check name, allows [a-z][A-Z][0-9] _ - only
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
}
// Check YAML format
@ -95,7 +84,7 @@ export class Stack {
get composeYAML() : string {
if (this._composeYAML === undefined) {
try {
this._composeYAML = fs.readFileSync(path.join(this.path, this._composeFileName), "utf-8");
this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
} catch (e) {
this._composeYAML = "";
}
@ -146,7 +135,7 @@ export class Stack {
}
// Write or overwrite the compose.yaml
fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML);
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
}
async deploy(socket? : DockgeSocket) : Promise<number> {
@ -160,7 +149,7 @@ export class Stack {
async delete(socket?: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans", "--rmi", "all" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to delete, please check the terminal output for more information.");
}
@ -188,18 +177,11 @@ export class Stack {
for (let filename of filenameList) {
try {
// Check if it is a directory
let stat = fs.statSync(path.join(stacksDir, filename));
if (!stat.isDirectory()) {
continue;
}
let stack = this.getStack(server, filename);
stack._status = CREATED_FILE;
stackList.set(filename, stack);
} catch (e) {
if (e instanceof Error) {
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
}
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
}
}
@ -364,11 +346,7 @@ export class Stack {
for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
statusList.set(obj.Service, obj.State);
} catch (e) {
}
}

View File

@ -54,9 +54,7 @@ export class Terminal {
try {
this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) {
if (e instanceof Error) {
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
}
@ -69,9 +67,7 @@ export class Terminal {
try {
this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) {
if (e instanceof Error) {
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
}
@ -89,7 +85,7 @@ export class Terminal {
// On Data
this._ptyProcess.onData((data) => {
this.buffer.pushItem(data);
this.buffer.push(data);
if (this.server.io) {
this.server.io.to(this.name).emit("terminalWrite", this.name, data);
}

View File

@ -12,11 +12,6 @@ dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export interface LooseObject {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
let randomBytes : (numBytes: number) => Uint8Array;
initRandomBytes();
@ -91,8 +86,8 @@ export const TERMINAL_COLS = 105;
export const TERMINAL_ROWS = 10;
export const PROGRESS_TERMINAL_ROWS = 8;
export const COMBINED_TERMINAL_COLS = 58;
export const COMBINED_TERMINAL_ROWS = 20;
export const COMBINED_TERMINAL_COLS = 56;
export const COMBINED_TERMINAL_ROWS = 15;
export const ERROR_TYPE_VALIDATION = 1;

View File

@ -6,11 +6,6 @@ import { ERROR_TYPE_VALIDATION } from "./util-common";
import { R } from "redbean-node";
import { verifyPassword } from "./password-hash";
export interface JWTDecoded {
username : string;
h? : string;
}
export interface DockgeSocket extends Socket {
userID: number;
consoleTerminal? : Terminal;

View File

@ -4,14 +4,14 @@
*/
export class LimitQueue<T> extends Array<T> {
__limit;
__onExceed? : (item : T | undefined) => void;
__onExceed = null;
constructor(limit: number) {
super();
this.__limit = limit;
}
pushItem(value : T) {
push(value : T) {
super.push(value);
if (this.length > this.__limit) {
const item = this.shift();

View File

@ -7,16 +7,11 @@ services:
# Host Port : Container Port
- 5001:5001
volumes:
# Docker Socket
- /var/run/docker.sock:/var/run/docker.sock
# Dockge Config
- ./data:/app/data
# If you want to use private registries, you need to share the auth file with Dockge:
# - /root/.docker/:/root/.docker
# Your stacks directory in the host (The paths inside container must be the same as the host)
# ⚠️⚠️ If you did it wrong, your data could end up be written into a wrong path.
# ✔️✔️✔️✔️ CORRECT: - /my-stacks:/my-stacks (Both paths match)
# ❌❌❌❌ WRONG: - /docker:/my-stacks (Both paths do not match)
- /opt/stacks:/opt/stacks
environment:
# Tell Dockge where is your stacks directory

View File

@ -1,20 +0,0 @@
#!/usr/bin/env node
import childProcess from "child_process";
let env = process.env;
let cmd = process.argv[2];
let args = process.argv.slice(3);
let replacedArgs = [];
for (let arg of args) {
for (let key in env) {
arg = arg.replaceAll(`$${key}`, env[key]);
}
replacedArgs.push(arg);
}
let child = childProcess.spawn(cmd, replacedArgs);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

View File

@ -1,9 +0,0 @@
// Check if docker is running
import { exec } from "child_process";
exec("docker ps", (err, stdout, stderr) => {
if (err) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
});

View File

@ -1,64 +0,0 @@
import pkg from "../package.json";
import childProcess from "child_process";
import fs from "fs";
const newVersion = process.env.VERSION;
console.log("New Version: " + newVersion);
if (! newVersion) {
console.error("invalid version");
process.exit(1);
}
const exists = tagExists(newVersion);
if (! exists) {
// Process package.json
pkg.version = newVersion;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(newVersion);
tag(newVersion);
} else {
console.log("version exists");
}
/**
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) {
let msg = "Update to " + version;
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error");
}
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
}
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
return res.stdout.toString().trim() === version;
}

View File

@ -5,7 +5,7 @@
<li v-for="(value, index) in array" :key="index" class="list-group-item">
<select v-model="array[index]" class="no-bg domain-input">
<option value="">Select a network...</option>
<option v-for="option in options" :key="option" :value="option">{{ option }}</option>
<option v-for="option in options" :value="option">{{ option }}</option>
</select>
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />

View File

@ -9,7 +9,7 @@
<div v-if="!isEditMode">
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
<a v-for="port in service.ports" :key="port" :href="parsePort(port).url" target="_blank">
<a v-for="port in service.ports" :href="parsePort(port).url" target="_blank">
<span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
</a>
</div>
@ -115,7 +115,7 @@
{{ $tc("network", 2) }}
</label>
<div v-if="networkList.length === 0 && service.networks && service.networks.length > 0" class="text-warning mb-3">
<div v-if="networkList.length === 0 && service.networks.length > 0" class="text-warning mb-3">
No networks available. You need to add internal networks or enable external networks in the right side first.
</div>
@ -179,10 +179,8 @@ export default defineComponent({
},
bgStyle() {
if (this.status === "running" || this.status === "healthy") {
if (this.status === "running") {
return "bg-primary";
} else if (this.status === "unhealthy") {
return "bg-danger";
} else {
return "bg-secondary";
}

View File

@ -32,7 +32,7 @@
class="form-control"
@keyup.enter="createExternelNetwork"
/>
<button class="btn btn-normal btn-sm me-2" type="button">
<button class="btn btn-normal btn-sm me-2" type="button" @click="">
{{ $t("createExternalNetwork") }}
</button>
</div>

View File

@ -77,8 +77,8 @@ export default {
}
this.terminal = new Terminal({
fontSize: 14,
fontFamily: "'JetBrains Mono', monospace",
fontSize: 16,
fontFamily: "monospace",
cursorBlink,
cols: this.cols,
rows: this.rows,

View File

@ -19,6 +19,7 @@ export default {
computed: {
uptime() {
return "0.00%";
return this.$t("notAvailableShort");
},

View File

@ -68,13 +68,13 @@
</template>
<script>
import HiddenInput from "../../components/HiddenInput.vue";
import dayjs from "dayjs";
import { timezoneList } from "../../util-frontend";
export default {
components: {
HiddenInput,
},
data() {

View File

@ -13,7 +13,6 @@ import Toast, { POSITION, useToast } from "vue-toastification";
import "xterm/lib/xterm.js";
// CSS
import "@fontsource/jetbrains-mono";
import "vue-toastification/dist/index.css";
import "xterm/css/xterm.css";
import "./styles/main.scss";
@ -23,9 +22,6 @@ import socket from "./mixins/socket";
import lang from "./mixins/lang";
import theme from "./mixins/theme";
// Set Title
document.title = document.title + " - " + location.host;
const app = createApp(rootApp());
app.use(Toast, {

View File

@ -68,10 +68,9 @@
<h4 class="mb-3">{{ $t("general") }}</h4>
<div class="shadow-box big-padding mb-3">
<!-- Stack Name -->
<div>
<div class="mb-3">
<label for="name" class="form-label">{{ $t("stackName") }}</label>
<input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
<div class="form-text">Lowercase only</div>
<input id="name" v-model="stack.name" type="text" class="form-control" required>
</div>
</div>
</div>
@ -118,7 +117,7 @@
</div>
</div>
<div class="col-lg-6">
<h4 class="mb-3">{{ stack.composeFileName }}</h4>
<h4 class="mb-3">compose.yaml</h4>
<!-- YAML editor -->
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
@ -583,10 +582,6 @@ export default {
});
},
stackNameToLowercase() {
this.stack.name = this.stack?.name?.toLowerCase();
},
}
};
</script>
@ -597,8 +592,6 @@ export default {
}
.editor-box {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
&.edit-mode {
background-color: #2c2f38 !important;
}

View File

@ -227,7 +227,5 @@ table {
.docker-run {
background-color: $dark-bg !important;
border: none;
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
}
</style>

View File

@ -680,10 +680,6 @@ code {
}
}
.form-text {
color: $dark-font-color3;
}
// Vue Prism Editor bug - workaround
// https://github.com/koca/vue-prism-editor/issues/87
/*

View File

@ -10,7 +10,7 @@ import { POSITION } from "vue-toastification";
*
* Generated by Trelent
*/
function getTimezoneOffset(timeZone : string) {
function getTimezoneOffset(timeZone) {
const now = new Date();
const tzString = now.toLocaleString("en-US", {
timeZone,
@ -124,6 +124,33 @@ export function hostNameRegexPattern(mqtt = false) {
return `${ipRegexPattern}|${hostNameRegexPattern}`;
}
/**
* Get the tag color options
* Shared between components
* @param {any} self Component
* @returns {object[]} Colour options
*/
export function colorOptions(self) {
return [
{ name: self.$t("Gray"),
color: "#4B5563" },
{ name: self.$t("Red"),
color: "#DC2626" },
{ name: self.$t("Orange"),
color: "#D97706" },
{ name: self.$t("Green"),
color: "#059669" },
{ name: self.$t("Blue"),
color: "#2563EB" },
{ name: self.$t("Indigo"),
color: "#4F46E5" },
{ name: self.$t("Purple"),
color: "#7C3AED" },
{ name: self.$t("Pink"),
color: "#DB2777" },
];
}
/**
* Loads the toast timeout settings from storage.
* @returns {object} The toast plugin options object.

View File

@ -1,4 +1,3 @@
/* eslint-disable */
/// <reference types="vite/client" />
declare module "*.vue" {

View File

@ -1,31 +1,29 @@
{
"name": "dockge",
"version": "1.0.4",
"version": "1.0.0",
"type": "module",
"scripts": {
"fmt": "eslint \"**/*.{ts,vue}\" --fix",
"lint": "eslint \"**/*.{ts,vue}\"",
"check-ts": "tsc --noEmit",
"start": "tsx ./backend/index.ts",
"dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts",
"dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
"build:frontend": "vite build --config ./frontend/vite.config.ts",
"build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push",
"build:docker": "node ./extra/env2arg.js docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:$VERSION --target release -f ./docker/Dockerfile . --push",
"build:docker": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:1.0.0 -f ./docker/Dockerfile . --push",
"build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push",
"start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest",
"mark-as-nightly": "tsx ./extra/mark-as-nightly.ts"
},
"dependencies": {
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.11",
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.10",
"@louislam/sqlite3": "~15.1.6",
"bcryptjs": "~2.4.3",
"check-password-strength": "~2.0.7",
"command-exists": "~1.2.9",
"compare-versions": "~6.1.0",
"composerize": "~1.4.1",
"croner": "~7.0.5",
"croner": "~7.0.4",
"dayjs": "~1.11.10",
"express": "~4.18.2",
"express-static-gzip": "~2.1.7",
@ -34,8 +32,8 @@
"jwt-decode": "~3.1.2",
"knex": "~2.5.1",
"limiter-es6-compat": "~2.1.2",
"mysql2": "~3.6.3",
"redbean-node": "~0.3.3",
"mysql2": "^3.6.3",
"redbean-node": "0.3.2",
"socket.io": "~4.7.2",
"socket.io-client": "~4.7.2",
"timezones-list": "~3.0.2",
@ -45,19 +43,17 @@
"yaml": "~2.3.4"
},
"devDependencies": {
"@fontsource/jetbrains-mono": "^5.0.17",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"@types/bcryptjs": "^2.4.6",
"@types/bootstrap": "~5.2.9",
"@types/bootstrap": "~5.2.8",
"@types/command-exists": "~1.2.3",
"@types/express": "~4.17.21",
"@types/jsonwebtoken": "~9.0.5",
"@types/jsonwebtoken": "~9.0.4",
"@typescript-eslint/eslint-plugin": "~6.8.0",
"@typescript-eslint/parser": "~6.8.0",
"@vitejs/plugin-vue": "~4.5.0",
"@vitejs/plugin-vue": "~4.3.4",
"bootstrap": "5.3.2",
"bootstrap-vue-next": "~0.14.10",
"cross-env": "~7.0.3",
@ -68,7 +64,7 @@
"sass": "~1.68.0",
"typescript": "~5.2.2",
"unplugin-vue-components": "~0.25.2",
"vite": "~5.0.0",
"vite": "~4.5.0",
"vite-plugin-compression": "~0.5.1",
"vue": "~3.3.8",
"vue-eslint-parser": "~9.3.2",
@ -77,7 +73,7 @@
"vue-qrcode": "~2.2.0",
"vue-router": "~4.2.5",
"vue-toastification": "2.0.0-rc.5",
"xterm": "5.4.0-beta.37",
"xterm": "~5.4.0-beta.37",
"xterm-addon-web-links": "~0.9.0"
}
}

645
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,6 @@
"module": "ESNext",
"target": "ESNext",
"strict": true,
"moduleResolution": "bundler",
"skipLibCheck": true
},
"include": [
"backend/**/*"
],
"moduleResolution": "bundler"
}
}