mirror of
https://github.com/louislam/dockge.git
synced 2024-11-27 13:14:03 +00:00
wip
This commit is contained in:
parent
a12c6dc033
commit
9b9234434e
16 changed files with 241 additions and 73 deletions
48
README.md
48
README.md
|
@ -4,7 +4,7 @@
|
|||
|
||||
# Dockge
|
||||
|
||||
A fancy, easy-to-use and reactive docker `compose.yaml` stack manager.
|
||||
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="" />
|
||||
|
||||
|
@ -21,23 +21,51 @@ A fancy, easy-to-use and reactive docker `compose.yaml` stack manager.
|
|||
|
||||
## 🔧 How to Install
|
||||
|
||||
1. Create a directory `dockge`
|
||||
2. Create or download [`compose.yaml`](https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml) and put it inside `dockge`:
|
||||
### Basic
|
||||
|
||||
Default stacks directory is `/opt/stacks`.
|
||||
|
||||
```
|
||||
# Create a directory that stores your stacks
|
||||
mkdir -p /opt/stacks
|
||||
|
||||
# Create a directory that stores dockge's compose.yaml
|
||||
mkdir -p /opt/dockge
|
||||
cd /opt/dockge
|
||||
|
||||
# Download the compose.yaml
|
||||
wget https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml
|
||||
|
||||
# Start Server
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 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:
|
||||
dockge:
|
||||
image: louislam/dockge:nightly
|
||||
image: louislam/dockge:1
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 5001:5001
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
```
|
||||
3. `docker-compose up -d`
|
||||
- ./data:/app/data
|
||||
|
||||
# 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=/my-stacks
|
||||
```
|
||||
|
||||
Dockge is now running on http://localhost:5001
|
||||
|
||||
## Motivations
|
||||
|
||||
|
@ -57,6 +85,10 @@ The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`.
|
|||
|
||||
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.
|
||||
|
||||
## More Ideas?
|
||||
|
||||
- Stats
|
||||
|
|
|
@ -134,7 +134,7 @@ export class Database {
|
|||
R.freeze(true);
|
||||
|
||||
if (autoloadModels) {
|
||||
await R.autoloadModels("./server/model");
|
||||
R.autoloadModels("./backend/models", "ts");
|
||||
}
|
||||
|
||||
if (dbConfig.type === "sqlite") {
|
||||
|
|
|
@ -28,6 +28,7 @@ import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler
|
|||
import { Stack } from "./stack";
|
||||
import { Cron } from "croner";
|
||||
import gracefulShutdown from "http-graceful-shutdown";
|
||||
import User from "./models/user";
|
||||
|
||||
export class DockgeServer {
|
||||
app : Express;
|
||||
|
@ -194,7 +195,7 @@ export class DockgeServer {
|
|||
cors,
|
||||
});
|
||||
|
||||
this.io.on("connection", (socket: Socket) => {
|
||||
this.io.on("connection", async (socket: Socket) => {
|
||||
log.info("server", "Socket connected!");
|
||||
|
||||
this.sendInfo(socket, true);
|
||||
|
@ -208,6 +209,20 @@ export class DockgeServer {
|
|||
for (const socketHandler of this.socketHandlerList) {
|
||||
socketHandler.create(socket as DockgeSocket, this);
|
||||
}
|
||||
|
||||
// ***************************
|
||||
// Better do anything after added all socket handlers here
|
||||
// ***************************
|
||||
|
||||
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"));
|
||||
socket.emit("autoLogin");
|
||||
} else {
|
||||
log.debug("auth", "need auth");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this.io.on("disconnect", () => {
|
||||
|
@ -216,8 +231,17 @@ export class DockgeServer {
|
|||
|
||||
}
|
||||
|
||||
prepareServer() {
|
||||
async afterLogin(socket : DockgeSocket, user : User) {
|
||||
socket.userID = user.id;
|
||||
socket.join(user.id.toString());
|
||||
|
||||
this.sendInfo(socket);
|
||||
|
||||
try {
|
||||
this.sendStackList();
|
||||
} catch (e) {
|
||||
log.error("server", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,7 +20,7 @@ export class User extends BeanModel {
|
|||
|
||||
/**
|
||||
* Reset this users password
|
||||
* @param {string} newPassword Users new password
|
||||
* @param {string} newPassword
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword : string) {
|
||||
|
@ -42,3 +42,5 @@ export class User extends BeanModel {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export default User;
|
||||
|
|
|
@ -183,6 +183,26 @@ export class DockerSocketHandler extends SocketHandler {
|
|||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
|
||||
// Services status
|
||||
socket.on("serviceStatusList", async (stackName : unknown, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (typeof(stackName) !== "string") {
|
||||
throw new ValidationError("Stack name must be a string");
|
||||
}
|
||||
|
||||
const stack = Stack.getStack(server, stackName);
|
||||
const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
|
||||
callback({
|
||||
ok: true,
|
||||
serviceStatusList,
|
||||
});
|
||||
} catch (e) {
|
||||
callbackError(e, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack {
|
||||
|
|
|
@ -72,7 +72,7 @@ export class MainSocketHandler extends SocketHandler {
|
|||
}
|
||||
|
||||
log.debug("auth", "afterLogin");
|
||||
await this.afterLogin(server, socket, user);
|
||||
await server.afterLogin(socket, user);
|
||||
log.debug("auth", "afterLogin ok");
|
||||
|
||||
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
|
||||
|
@ -129,7 +129,7 @@ export class MainSocketHandler extends SocketHandler {
|
|||
|
||||
if (user) {
|
||||
if (user.twofa_status === 0) {
|
||||
this.afterLogin(server, socket, user);
|
||||
server.afterLogin(socket, user);
|
||||
|
||||
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
|
||||
|
||||
|
@ -152,7 +152,7 @@ export class MainSocketHandler extends SocketHandler {
|
|||
const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
|
||||
|
||||
if (user.twofa_last_token !== data.token && verify) {
|
||||
this.afterLogin(server, socket, user);
|
||||
server.afterLogin(socket, user);
|
||||
|
||||
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
|
||||
data.token,
|
||||
|
@ -189,6 +189,35 @@ export class MainSocketHandler extends SocketHandler {
|
|||
|
||||
});
|
||||
|
||||
// Change Password
|
||||
socket.on("changePassword", async (password, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
if (! password.newPassword) {
|
||||
throw new Error("Invalid new password");
|
||||
}
|
||||
|
||||
if (passwordStrength(password.newPassword).value === "Too weak") {
|
||||
throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length.");
|
||||
}
|
||||
|
||||
let user = await doubleCheckPassword(socket, password.currentPassword);
|
||||
await user.resetPassword(password.newPassword);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Password has been updated successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getSettings", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
@ -221,6 +250,8 @@ export class MainSocketHandler extends SocketHandler {
|
|||
await doubleCheckPassword(socket, currentPassword);
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
await Settings.setSettings("general", data);
|
||||
|
||||
callback({
|
||||
|
@ -239,19 +270,6 @@ export class MainSocketHandler extends SocketHandler {
|
|||
});
|
||||
}
|
||||
|
||||
async afterLogin(server: DockgeServer, socket : DockgeSocket, user : User) {
|
||||
socket.userID = user.id;
|
||||
socket.join(user.id.toString());
|
||||
|
||||
server.sendInfo(socket);
|
||||
|
||||
try {
|
||||
server.sendStackList(socket);
|
||||
} catch (e) {
|
||||
log.error("server", e);
|
||||
}
|
||||
}
|
||||
|
||||
async login(username : string, password : string) {
|
||||
if (typeof username !== "string" || typeof password !== "string") {
|
||||
return null;
|
||||
|
|
|
@ -333,4 +333,26 @@ export class Stack {
|
|||
terminal.join(socket);
|
||||
terminal.start();
|
||||
}
|
||||
|
||||
async getServiceStatusList() {
|
||||
let statusList = new Map<string, number>();
|
||||
|
||||
let res = childProcess.execSync("docker compose ps --format json", {
|
||||
cwd: this.path,
|
||||
});
|
||||
|
||||
let lines = res.toString().split("\n");
|
||||
|
||||
console.log(lines);
|
||||
|
||||
for (let line of lines) {
|
||||
try {
|
||||
let obj = JSON.parse(line);
|
||||
statusList.set(obj.Service, obj.State);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
return statusList;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
dockge:
|
||||
image: louislam/dockge:nightly
|
||||
image: louislam/dockge:1
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Host Port : Container Port
|
||||
- 5001:5001
|
||||
volumes:
|
||||
# Docker Socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Dockge Config
|
||||
- ./data:/app/data
|
||||
# Your stacks directory in the host (The paths inside container must be the same as the host)
|
||||
- /opt/stacks:/opt/stacks
|
||||
environment:
|
||||
# Tell Dockge where is your stacks directory
|
||||
- DOCKGE_STACKS_DIR=/opt/stacks
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<span class="me-1">{{ imageName }}:</span><span class="tag">{{ imageTag }}</span>
|
||||
</div>
|
||||
<div v-if="!isEditMode">
|
||||
<span class="badge bg-primary me-1">Running</span>
|
||||
<span class="badge me-1" :class="bgStyle">{{ status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
|
@ -146,6 +146,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: "N/A",
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
],
|
||||
|
@ -156,6 +160,14 @@ export default defineComponent({
|
|||
},
|
||||
computed: {
|
||||
|
||||
bgStyle() {
|
||||
if (this.status === "running") {
|
||||
return "bg-primary";
|
||||
} else {
|
||||
return "bg-secondary";
|
||||
}
|
||||
},
|
||||
|
||||
terminalRouteLink() {
|
||||
return {
|
||||
name: "containerTerminal",
|
||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
|||
submit() {
|
||||
this.processing = true;
|
||||
|
||||
this.login(this.username, this.password, this.token, (res) => {
|
||||
this.$root.login(this.username, this.password, this.token, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.tokenRequired) {
|
||||
|
@ -82,40 +82,6 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Send request to log user in
|
||||
* @param {string} username Username to log in with
|
||||
* @param {string} password Password to log in with
|
||||
* @param {string} token User token
|
||||
* @param {loginCB} callback Callback to call with result
|
||||
* @returns {void}
|
||||
*/
|
||||
login(username, password, token, callback) {
|
||||
|
||||
this.$root.getSocket().emit("login", {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
}, (res) => {
|
||||
if (res.tokenRequired) {
|
||||
callback(res);
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.storage().token = res.token;
|
||||
this.$root.socketIO.token = res.token;
|
||||
this.$root.loggedIn = true;
|
||||
this.$root.username = this.$root.getJWTPayload()?.username;
|
||||
|
||||
this.$root.afterLogin();
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "");
|
||||
}
|
||||
|
||||
callback(res);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -64,7 +64,8 @@
|
|||
</form>
|
||||
</template>
|
||||
|
||||
<div v-if="! settings.disableAuth" class="mt-5 mb-3">
|
||||
<!-- TODO: Hidden for now -->
|
||||
<div v-if="! settings.disableAuth && false" class="mt-5 mb-3">
|
||||
<h5 class="my-4 settings-subheading">
|
||||
{{ $t("Two Factor Authentication") }}
|
||||
</h5>
|
||||
|
@ -182,7 +183,7 @@ export default {
|
|||
this.saveSettings(() => {
|
||||
this.password.currentPassword = "";
|
||||
this.$root.username = null;
|
||||
this.$root.socket.token = "autoLogin";
|
||||
this.$root.socketIO.token = "autoLogin";
|
||||
}, this.password.currentPassword);
|
||||
},
|
||||
|
||||
|
|
|
@ -43,5 +43,6 @@
|
|||
"addContainer": "Add Container",
|
||||
"addNetwork": "Add Network",
|
||||
"disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
|
||||
"disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms."
|
||||
"disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
|
||||
"passwordNotMatchMsg": "The repeat password does not match."
|
||||
}
|
||||
|
|
|
@ -220,6 +220,40 @@ export default defineComponent({
|
|||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send request to log user in
|
||||
* @param {string} username Username to log in with
|
||||
* @param {string} password Password to log in with
|
||||
* @param {string} token User token
|
||||
* @param {loginCB} callback Callback to call with result
|
||||
* @returns {void}
|
||||
*/
|
||||
login(username : string, password : string, token : string, callback) {
|
||||
this.getSocket().emit("login", {
|
||||
username,
|
||||
password,
|
||||
token,
|
||||
}, (res) => {
|
||||
if (res.tokenRequired) {
|
||||
callback(res);
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
this.storage().token = res.token;
|
||||
this.socketIO.token = res.token;
|
||||
this.loggedIn = true;
|
||||
this.username = this.getJWTPayload()?.username;
|
||||
|
||||
this.afterLogin();
|
||||
|
||||
// Trigger Chrome Save Password
|
||||
history.pushState({}, "");
|
||||
}
|
||||
|
||||
callback(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Log in using a token
|
||||
* @param {string} token Token to log in with
|
||||
|
|
|
@ -97,6 +97,7 @@
|
|||
:name="name"
|
||||
:is-edit-mode="isEditMode"
|
||||
:first="name === Object.keys(jsonConfig.services)[0]"
|
||||
:status="serviceStatusList[name]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -201,6 +202,8 @@ services:
|
|||
|
||||
let yamlErrorTimeout = null;
|
||||
|
||||
let serviceStatusTimeout = null;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NetworkInput,
|
||||
|
@ -228,10 +231,12 @@ export default {
|
|||
stack: {
|
||||
|
||||
},
|
||||
serviceStatusList: {},
|
||||
isEditMode: false,
|
||||
submitted: false,
|
||||
showDeleteDialog: false,
|
||||
newContainerName: "",
|
||||
stopServiceStatusTimeout: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -331,8 +336,33 @@ export default {
|
|||
this.stack.name = this.$route.params.stackName;
|
||||
this.loadStack();
|
||||
}
|
||||
|
||||
this.requestServiceStatus();
|
||||
},
|
||||
unmounted() {
|
||||
this.stopServiceStatusTimeout = true;
|
||||
clearTimeout(serviceStatusTimeout);
|
||||
},
|
||||
methods: {
|
||||
|
||||
startServiceStatusTimeout() {
|
||||
clearTimeout(serviceStatusTimeout);
|
||||
serviceStatusTimeout = setTimeout(async () => {
|
||||
this.requestServiceStatus();
|
||||
}, 2000);
|
||||
},
|
||||
|
||||
requestServiceStatus() {
|
||||
this.$root.getSocket().emit("serviceStatusList", this.stack.name, (res) => {
|
||||
if (res.ok) {
|
||||
this.serviceStatusList = res.serviceStatusList;
|
||||
}
|
||||
if (!this.stopServiceStatusTimeout) {
|
||||
this.startServiceStatusTimeout();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
exitConfirm(next) {
|
||||
if (this.isEditMode) {
|
||||
if (confirm("You are currently editing a stack. Are you sure you want to leave?")) {
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"knex": "~2.5.1",
|
||||
"limiter-es6-compat": "~2.1.2",
|
||||
"mysql2": "^3.6.3",
|
||||
"redbean-node": "0.3.1",
|
||||
"redbean-node": "0.3.2",
|
||||
"socket.io": "~4.7.2",
|
||||
"socket.io-client": "~4.7.2",
|
||||
"timezones-list": "~3.0.2",
|
||||
|
|
|
@ -57,8 +57,8 @@ dependencies:
|
|||
specifier: ^3.6.3
|
||||
version: 3.6.3
|
||||
redbean-node:
|
||||
specifier: 0.3.1
|
||||
version: 0.3.1(mysql2@3.6.3)
|
||||
specifier: 0.3.2
|
||||
version: 0.3.2(mysql2@3.6.3)
|
||||
socket.io:
|
||||
specifier: ~4.7.2
|
||||
version: 4.7.2
|
||||
|
@ -3491,8 +3491,8 @@ packages:
|
|||
resolve: 1.22.8
|
||||
dev: false
|
||||
|
||||
/redbean-node@0.3.1(mysql2@3.6.3):
|
||||
resolution: {integrity: sha512-rz71vF7UtJQ14ttZ9I0QuaJ9TOwBCnZb+qHUBiU05f2fLaiQC79liisL3xgkHI8uE9et6HAkG8Z8VPkZbhgxKw==}
|
||||
/redbean-node@0.3.2(mysql2@3.6.3):
|
||||
resolution: {integrity: sha512-39VMxPWPpPicRlU4FSJJnJuUMoxw5/4envFthHtKnLe+3qWTBje3RMrJTFZcQGLruWQ/s2LgeYzdd+d0O+p+uQ==}
|
||||
dependencies:
|
||||
'@types/node': 20.3.3
|
||||
await-lock: 2.2.2
|
||||
|
|
Loading…
Reference in a new issue