mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/stack-env-support
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import { webcrypto } from "node:crypto";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { findAdminByAuthId } from "@dokploy/server/services/admin";
|
||||
import { findUserByAuthId } from "@dokploy/server/services/user";
|
||||
@@ -9,7 +8,6 @@ import type { Session, User } from "lucia/dist/core.js";
|
||||
import { db } from "../db";
|
||||
import { type DatabaseUser, auth, sessionTable } from "../db/schema";
|
||||
|
||||
globalThis.crypto = webcrypto as Crypto;
|
||||
export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth);
|
||||
|
||||
export const lucia = new Lucia(adapter, {
|
||||
|
||||
@@ -5,34 +5,22 @@ export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
export const docker = new Docker();
|
||||
|
||||
export const paths = (isServer = false) => {
|
||||
if (isServer) {
|
||||
const BASE_PATH = "/etc/dokploy";
|
||||
return {
|
||||
BASE_PATH,
|
||||
MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
|
||||
DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
|
||||
LOGS_PATH: `${BASE_PATH}/logs`,
|
||||
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
||||
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
||||
SSH_PATH: `${BASE_PATH}/ssh`,
|
||||
CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
|
||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
};
|
||||
}
|
||||
const BASE_PATH =
|
||||
process.env.NODE_ENV === "production"
|
||||
isServer || process.env.NODE_ENV === "production"
|
||||
? "/etc/dokploy"
|
||||
: path.join(process.cwd(), ".docker");
|
||||
const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
|
||||
const DYNAMIC_TRAEFIK_PATH = `${MAIN_TRAEFIK_PATH}/dynamic`;
|
||||
|
||||
return {
|
||||
BASE_PATH,
|
||||
MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
|
||||
DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
|
||||
MAIN_TRAEFIK_PATH,
|
||||
DYNAMIC_TRAEFIK_PATH,
|
||||
LOGS_PATH: `${BASE_PATH}/logs`,
|
||||
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
||||
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
||||
SSH_PATH: `${BASE_PATH}/ssh`,
|
||||
CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
|
||||
CERTIFICATES_PATH: `${DYNAMIC_TRAEFIK_PATH}/certificates`,
|
||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ const clearDb = async (): Promise<void> => {
|
||||
console.log(tables);
|
||||
await pg.end();
|
||||
} catch (error) {
|
||||
console.error("Error to clean database", error);
|
||||
console.error("Error cleaning database", error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,6 +31,15 @@ export const admins = pgTable("admin", {
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
||||
.notNull()
|
||||
.default(false),
|
||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||
.notNull()
|
||||
.default(false),
|
||||
});
|
||||
|
||||
export const adminsRelations = relations(admins, ({ one, many }) => ({
|
||||
|
||||
@@ -17,6 +17,7 @@ import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { mounts } from "./mount";
|
||||
import { ports } from "./port";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { projects } from "./project";
|
||||
import { redirects } from "./redirects";
|
||||
import { registry } from "./registry";
|
||||
@@ -25,7 +26,6 @@ import { server } from "./server";
|
||||
import { applicationStatus, certificateType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
@@ -129,10 +129,10 @@ export const applications = pgTable("application", {
|
||||
false,
|
||||
),
|
||||
buildArgs: text("buildArgs"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
title: text("title"),
|
||||
enabled: boolean("enabled"),
|
||||
subtitle: text("subtitle"),
|
||||
@@ -355,10 +355,10 @@ const createSchema = createInsertSchema(applications, {
|
||||
buildArgs: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
subtitle: z.string().optional(),
|
||||
|
||||
@@ -92,6 +92,7 @@ export const apiUpdateAuth = createSchema.partial().extend({
|
||||
email: z.string().nullable(),
|
||||
password: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
currentPassword: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const apiUpdateAuthByAdmin = createSchema.partial().extend({
|
||||
|
||||
@@ -155,6 +155,11 @@ export const apiFindCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiDeleteCompose = z.object({
|
||||
composeId: z.string().min(1),
|
||||
deleteVolumes: z.boolean(),
|
||||
});
|
||||
|
||||
export const apiFetchServices = z.object({
|
||||
composeId: z.string().min(1),
|
||||
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
||||
|
||||
@@ -11,8 +11,8 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { server } from "./server";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { server } from "./server";
|
||||
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
@@ -47,6 +47,7 @@ export const deployments = pgTable("deployment", {
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
errorMessage: text("errorMessage"),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
|
||||
@@ -82,6 +82,7 @@ export const apiUpdateDestination = createSchema
|
||||
endpoint: true,
|
||||
secretAccessKey: true,
|
||||
destinationId: true,
|
||||
provider: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
|
||||
@@ -14,8 +14,8 @@ import { z } from "zod";
|
||||
import { domain } from "../validations/domain";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { certificateType } from "./shared";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { certificateType } from "./shared";
|
||||
|
||||
export const domainType = pgEnum("domainType", [
|
||||
"compose",
|
||||
|
||||
@@ -10,6 +10,7 @@ export const gitlab = pgTable("gitlab", {
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
gitlabUrl: text("gitlabUrl").default("https://gitlab.com").notNull(),
|
||||
applicationId: text("application_id"),
|
||||
redirectUri: text("redirect_uri"),
|
||||
secret: text("secret"),
|
||||
@@ -39,6 +40,7 @@ export const apiCreateGitlab = createSchema.extend({
|
||||
redirectUri: z.string().optional(),
|
||||
authId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
gitlabUrl: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneGitlab = createSchema
|
||||
@@ -67,4 +69,5 @@ export const apiUpdateGitlab = createSchema.extend({
|
||||
redirectUri: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
gitlabId: z.string().min(1),
|
||||
gitlabUrl: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -29,4 +29,4 @@ export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./server";
|
||||
export * from "./utils";
|
||||
export * from "./preview-deployments";
|
||||
export * from "./preview-deployments";
|
||||
|
||||
@@ -29,10 +29,10 @@ export const mariadb = pgTable("mariadb", {
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
//
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
@@ -74,10 +74,10 @@ const createSchema = createInsertSchema(mariadb, {
|
||||
dockerImage: z.string().default("mariadb:6"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -26,10 +26,10 @@ export const mongo = pgTable("mongo", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
@@ -43,6 +43,7 @@ export const mongo = pgTable("mongo", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
replicaSets: boolean("replicaSets").default(false),
|
||||
});
|
||||
|
||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
@@ -68,15 +69,16 @@ const createSchema = createInsertSchema(mongo, {
|
||||
dockerImage: z.string().default("mongo:15"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
replicaSets: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export const apiCreateMongo = createSchema
|
||||
@@ -89,6 +91,7 @@ export const apiCreateMongo = createSchema
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
serverId: true,
|
||||
replicaSets: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
@@ -28,10 +28,10 @@ export const mysql = pgTable("mysql", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
@@ -72,10 +72,10 @@ const createSchema = createInsertSchema(mysql, {
|
||||
dockerImage: z.string().default("mysql:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
|
||||
@@ -10,6 +10,7 @@ export const notificationType = pgEnum("notificationType", [
|
||||
"telegram",
|
||||
"discord",
|
||||
"email",
|
||||
"gotify",
|
||||
]);
|
||||
|
||||
export const notifications = pgTable("notification", {
|
||||
@@ -39,6 +40,9 @@ export const notifications = pgTable("notification", {
|
||||
emailId: text("emailId").references(() => email.emailId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
adminId: text("adminId").references(() => admins.adminId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -68,6 +72,7 @@ export const discord = pgTable("discord", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
webhookUrl: text("webhookUrl").notNull(),
|
||||
decoration: boolean("decoration"),
|
||||
});
|
||||
|
||||
export const email = pgTable("email", {
|
||||
@@ -83,6 +88,17 @@ export const email = pgTable("email", {
|
||||
toAddresses: text("toAddress").array().notNull(),
|
||||
});
|
||||
|
||||
export const gotify = pgTable("gotify", {
|
||||
gotifyId: text("gotifyId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
serverUrl: text("serverUrl").notNull(),
|
||||
appToken: text("appToken").notNull(),
|
||||
priority: integer("priority").notNull().default(5),
|
||||
decoration: boolean("decoration"),
|
||||
});
|
||||
|
||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
slack: one(slack, {
|
||||
fields: [notifications.slackId],
|
||||
@@ -100,6 +116,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||
fields: [notifications.emailId],
|
||||
references: [email.emailId],
|
||||
}),
|
||||
gotify: one(gotify, {
|
||||
fields: [notifications.gotifyId],
|
||||
references: [gotify.gotifyId],
|
||||
}),
|
||||
admin: one(admins, {
|
||||
fields: [notifications.adminId],
|
||||
references: [admins.adminId],
|
||||
@@ -171,6 +191,7 @@ export const apiCreateDiscord = notificationsSchema
|
||||
})
|
||||
.extend({
|
||||
webhookUrl: z.string().min(1),
|
||||
decoration: z.boolean(),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -180,9 +201,13 @@ export const apiUpdateDiscord = apiCreateDiscord.partial().extend({
|
||||
adminId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestDiscordConnection = apiCreateDiscord.pick({
|
||||
webhookUrl: true,
|
||||
});
|
||||
export const apiTestDiscordConnection = apiCreateDiscord
|
||||
.pick({
|
||||
webhookUrl: true,
|
||||
})
|
||||
.extend({
|
||||
decoration: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
@@ -218,6 +243,39 @@ export const apiTestEmailConnection = apiCreateEmail.pick({
|
||||
fromAddress: true,
|
||||
});
|
||||
|
||||
export const apiCreateGotify = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
dockerCleanup: true,
|
||||
})
|
||||
.extend({
|
||||
serverUrl: z.string().min(1),
|
||||
appToken: z.string().min(1),
|
||||
priority: z.number().min(1),
|
||||
decoration: z.boolean(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateGotify = apiCreateGotify.partial().extend({
|
||||
notificationId: z.string().min(1),
|
||||
gotifyId: z.string().min(1),
|
||||
adminId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const apiTestGotifyConnection = apiCreateGotify
|
||||
.pick({
|
||||
serverUrl: true,
|
||||
appToken: true,
|
||||
priority: true,
|
||||
})
|
||||
.extend({
|
||||
decoration: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const apiFindOneNotification = notificationsSchema
|
||||
.pick({
|
||||
notificationId: true,
|
||||
@@ -236,5 +294,8 @@ export const apiSendTest = notificationsSchema
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
toAddresses: z.array(z.string()),
|
||||
serverUrl: z.string(),
|
||||
appToken: z.string(),
|
||||
priority: z.number(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
@@ -27,11 +27,11 @@ export const postgres = pgTable("postgres", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
externalPort: integer("externalPort"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
@@ -68,10 +68,10 @@ const createSchema = createInsertSchema(postgres, {
|
||||
dockerImage: z.string().default("postgres:15"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { applications } from "./application";
|
||||
import { domains } from "./domain";
|
||||
import { deployments } from "./deployment";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { generateAppName } from "./utils";
|
||||
import { applications } from "./application";
|
||||
import { deployments } from "./deployment";
|
||||
import { domains } from "./domain";
|
||||
import { applicationStatus } from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const previewDeployments = pgTable("preview_deployments", {
|
||||
previewDeploymentId: text("previewDeploymentId")
|
||||
|
||||
@@ -24,10 +24,10 @@ export const redis = pgTable("redis", {
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
memoryReservation: integer("memoryReservation"),
|
||||
memoryLimit: integer("memoryLimit"),
|
||||
cpuReservation: integer("cpuReservation"),
|
||||
cpuLimit: integer("cpuLimit"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
externalPort: integer("externalPort"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
@@ -64,10 +64,10 @@ const createSchema = createInsertSchema(redis, {
|
||||
dockerImage: z.string().default("redis:8"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.number().optional(),
|
||||
memoryLimit: z.number().optional(),
|
||||
cpuReservation: z.number().optional(),
|
||||
cpuLimit: z.number().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
projectId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
|
||||
@@ -67,7 +67,7 @@ export const apiCreateRegistry = createSchema
|
||||
});
|
||||
|
||||
export const apiTestRegistry = createSchema.pick({}).extend({
|
||||
registryName: z.string().min(1),
|
||||
registryName: z.string().optional(),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
registryUrl: z.string(),
|
||||
|
||||
@@ -40,7 +40,7 @@ export const server = pgTable("server", {
|
||||
.notNull()
|
||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
||||
|
||||
command: text("command").notNull().default(""),
|
||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
@@ -105,4 +105,7 @@ export const apiUpdateServer = createSchema
|
||||
username: true,
|
||||
sshKeyId: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.extend({
|
||||
command: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -40,11 +40,11 @@ export const users = pgTable("user", {
|
||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
||||
.notNull()
|
||||
.default(false),
|
||||
accesedProjects: text("accesedProjects")
|
||||
accessedProjects: text("accesedProjects")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
accesedServices: text("accesedServices")
|
||||
accessedServices: text("accesedServices")
|
||||
.array()
|
||||
.notNull()
|
||||
.default(sql`ARRAY[]::text[]`),
|
||||
@@ -73,8 +73,8 @@ const createSchema = createInsertSchema(users, {
|
||||
token: z.string().min(1),
|
||||
isRegistered: z.boolean().optional(),
|
||||
adminId: z.string(),
|
||||
accesedProjects: z.array(z.string()).optional(),
|
||||
accesedServices: z.array(z.string()).optional(),
|
||||
accessedProjects: z.array(z.string()).optional(),
|
||||
accessedServices: z.array(z.string()).optional(),
|
||||
canCreateProjects: z.boolean().optional(),
|
||||
canCreateServices: z.boolean().optional(),
|
||||
canDeleteProjects: z.boolean().optional(),
|
||||
@@ -106,8 +106,8 @@ export const apiAssignPermissions = createSchema
|
||||
canCreateServices: true,
|
||||
canDeleteProjects: true,
|
||||
canDeleteServices: true,
|
||||
accesedProjects: true,
|
||||
accesedServices: true,
|
||||
accessedProjects: true,
|
||||
accessedServices: true,
|
||||
canAccessToTraefikFiles: true,
|
||||
canAccessToDocker: true,
|
||||
canAccessToAPI: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
@@ -13,3 +14,17 @@ export const generateAppName = (type: string) => {
|
||||
const nanoidPart = customNanoid();
|
||||
return `${type}-${randomFakerElement}-${nanoidPart}`;
|
||||
};
|
||||
|
||||
export const cleanAppName = (appName?: string) => {
|
||||
if (!appName) {
|
||||
return appName?.toLowerCase();
|
||||
}
|
||||
return appName.trim().replace(/ /g, "-").toLowerCase();
|
||||
};
|
||||
|
||||
export const buildAppName = (type: string, baseAppName?: string) => {
|
||||
if (baseAppName) {
|
||||
return `${cleanAppName(baseAppName)}-${generatePassword(6)}`;
|
||||
}
|
||||
return generateAppName(type);
|
||||
};
|
||||
|
||||
@@ -41,6 +41,8 @@ export * from "./setup/redis-setup";
|
||||
export * from "./setup/server-setup";
|
||||
export * from "./setup/setup";
|
||||
export * from "./setup/traefik-setup";
|
||||
export * from "./setup/server-validate";
|
||||
export * from "./setup/server-audit";
|
||||
|
||||
export * from "./utils/backups/index";
|
||||
export * from "./utils/backups/mariadb";
|
||||
@@ -101,11 +103,6 @@ export * from "./utils/traefik/security";
|
||||
export * from "./utils/traefik/types";
|
||||
export * from "./utils/traefik/web-server";
|
||||
|
||||
export * from "./wss/docker-container-logs";
|
||||
export * from "./wss/docker-container-terminal";
|
||||
export * from "./wss/docker-stats";
|
||||
export * from "./wss/listen-deployment";
|
||||
export * from "./wss/terminal";
|
||||
export * from "./wss/utils";
|
||||
|
||||
export * from "./utils/access-log/handler";
|
||||
|
||||
@@ -30,7 +30,7 @@ export const createInvitation = async (
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the user",
|
||||
message: "Error creating the user",
|
||||
});
|
||||
}
|
||||
const expiresIn24Hours = new Date();
|
||||
@@ -88,6 +88,9 @@ export const isAdminPresent = async () => {
|
||||
export const findAdminByAuthId = async (authId: string) => {
|
||||
const admin = await db.query.admins.findFirst({
|
||||
where: eq(admins.authId, authId),
|
||||
with: {
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
if (!admin) {
|
||||
throw new TRPCError({
|
||||
@@ -141,6 +144,24 @@ export const removeUserByAuthId = async (authId: string) => {
|
||||
.then((res) => res[0]);
|
||||
};
|
||||
|
||||
export const removeAdminByAuthId = async (authId: string) => {
|
||||
const admin = await findAdminByAuthId(authId);
|
||||
if (!admin) return null;
|
||||
|
||||
// First delete all associated users
|
||||
const users = admin.users;
|
||||
|
||||
for (const user of users) {
|
||||
await removeUserByAuthId(user.authId);
|
||||
}
|
||||
// Then delete the auth record which will cascade delete the admin
|
||||
return await db
|
||||
.delete(auth)
|
||||
.where(eq(auth.id, authId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
};
|
||||
|
||||
export const getDokployUrl = async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "https://app.dokploy.com";
|
||||
|
||||
@@ -3,10 +3,10 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateApplication,
|
||||
applications,
|
||||
buildAppName,
|
||||
cleanAppName,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import {
|
||||
buildApplication,
|
||||
getBuildCommand,
|
||||
@@ -40,40 +40,38 @@ import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { findAdminById, getDokployUrl } from "./admin";
|
||||
import {
|
||||
createDeployment,
|
||||
createDeploymentPreview,
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
import {
|
||||
createPreviewDeploymentComment,
|
||||
getIssueComment,
|
||||
issueCommentExists,
|
||||
updateIssueComment,
|
||||
} from "./github";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { cleanupFullDocker } from "./settings";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
input: typeof apiCreateApplication._type,
|
||||
) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("app", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Application with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Application with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
@@ -81,6 +79,7 @@ export const createApplication = async (
|
||||
.insert(applications)
|
||||
.values({
|
||||
...input,
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -88,7 +87,7 @@ export const createApplication = async (
|
||||
if (!newApplication) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the application",
|
||||
message: "Error creating the application",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,10 +139,11 @@ export const updateApplication = async (
|
||||
applicationId: string,
|
||||
applicationData: Partial<Application>,
|
||||
) => {
|
||||
const { appName, ...rest } = applicationData;
|
||||
const application = await db
|
||||
.update(applications)
|
||||
.set({
|
||||
...applicationData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(applications.applicationId, applicationId))
|
||||
.returning();
|
||||
@@ -176,6 +176,7 @@ export const deployApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
@@ -184,6 +185,12 @@ export const deployApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await findAdminById(application.project.adminId);
|
||||
|
||||
if (admin.cleanupCacheApplications) {
|
||||
await cleanupFullDocker(application?.serverId);
|
||||
}
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
@@ -214,6 +221,7 @@ export const deployApplication = async ({
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
domains: application.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -223,7 +231,7 @@ export const deployApplication = async ({
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
@@ -244,6 +252,7 @@ export const rebuildApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
@@ -251,6 +260,11 @@ export const rebuildApplication = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await findAdminById(application.project.adminId);
|
||||
|
||||
if (admin.cleanupCacheApplications) {
|
||||
await cleanupFullDocker(application?.serverId);
|
||||
}
|
||||
if (application.sourceType === "github") {
|
||||
await buildApplication(application, deployment.logPath);
|
||||
} else if (application.sourceType === "gitlab") {
|
||||
@@ -285,6 +299,7 @@ export const deployRemoteApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`;
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
@@ -294,6 +309,11 @@ export const deployRemoteApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
const admin = await findAdminById(application.project.adminId);
|
||||
|
||||
if (admin.cleanupCacheApplications) {
|
||||
await cleanupFullDocker(application?.serverId);
|
||||
}
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
@@ -333,6 +353,7 @@ export const deployRemoteApplication = async ({
|
||||
applicationType: "application",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
domains: application.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -353,19 +374,11 @@ export const deployRemoteApplication = async ({
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: application.project.adminId,
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Error on ",
|
||||
application.buildType,
|
||||
"/",
|
||||
application.sourceType,
|
||||
error,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -384,6 +397,7 @@ export const deployPreviewApplication = async ({
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
@@ -437,9 +451,15 @@ export const deployPreviewApplication = async ({
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
const admin = await findAdminById(application.project.adminId);
|
||||
|
||||
if (admin.cleanupCacheOnPreviews) {
|
||||
await cleanupFullDocker(application?.serverId);
|
||||
}
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...application,
|
||||
@@ -449,7 +469,6 @@ export const deployPreviewApplication = async ({
|
||||
});
|
||||
await buildApplication(application, deployment.logPath);
|
||||
}
|
||||
// 4eef09efc46009187d668cf1c25f768d0bde4f91
|
||||
const successComment = getIssueComment(
|
||||
application.name,
|
||||
"success",
|
||||
@@ -491,6 +510,7 @@ export const deployRemotePreviewApplication = async ({
|
||||
previewDeploymentId: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeploymentPreview({
|
||||
title: titleLog,
|
||||
description: descriptionLog,
|
||||
@@ -544,14 +564,21 @@ export const deployRemotePreviewApplication = async ({
|
||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||
});
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = application.previewEnv;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
|
||||
if (application.serverId) {
|
||||
const admin = await findAdminById(application.project.adminId);
|
||||
|
||||
if (admin.cleanupCacheOnPreviews) {
|
||||
await cleanupFullDocker(application?.serverId);
|
||||
}
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
command += await getGithubCloneCommand({
|
||||
...application,
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
serverId: application.serverId,
|
||||
logPath: deployment.logPath,
|
||||
});
|
||||
@@ -601,6 +628,7 @@ export const rebuildRemoteApplication = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const application = await findApplicationById(applicationId);
|
||||
|
||||
const deployment = await createDeployment({
|
||||
applicationId: applicationId,
|
||||
title: titleLog,
|
||||
@@ -609,6 +637,11 @@ export const rebuildRemoteApplication = async ({
|
||||
|
||||
try {
|
||||
if (application.serverId) {
|
||||
const admin = await findAdminById(application.project.adminId);
|
||||
|
||||
if (admin.cleanupCacheApplications) {
|
||||
await cleanupFullDocker(application?.serverId);
|
||||
}
|
||||
if (application.sourceType !== "docker") {
|
||||
let command = "set -e;";
|
||||
command += getBuildCommand(application, deployment.logPath);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
auth,
|
||||
users,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal";
|
||||
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -34,7 +34,7 @@ export const createAdmin = async (input: typeof apiCreateAdmin._type) => {
|
||||
if (!newAuth) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the user",
|
||||
message: "Error creating the user",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const createUser = async (input: typeof apiCreateUser._type) => {
|
||||
if (!res) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the user",
|
||||
message: "Error creating the user",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateBackup, backups } from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { removeScheduleBackup, scheduleBackup } from "../utils/backups/utils";
|
||||
|
||||
export type Backup = typeof backups.$inferSelect;
|
||||
|
||||
export type BackupSchedule = Awaited<ReturnType<typeof findBackupById>>;
|
||||
|
||||
export type BackupScheduleList = Awaited<ReturnType<typeof findBackupsByDbId>>;
|
||||
export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
||||
const newBackup = await db
|
||||
.insert(backups)
|
||||
@@ -19,7 +21,7 @@ export const createBackup = async (input: typeof apiCreateBackup._type) => {
|
||||
if (!newBackup) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the Backup",
|
||||
message: "Error creating the Backup",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -69,3 +71,20 @@ export const removeBackupById = async (backupId: string) => {
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findBackupsByDbId = async (
|
||||
id: string,
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo",
|
||||
) => {
|
||||
const result = await db.query.backups.findMany({
|
||||
where: eq(backups[`${type}Id`], id),
|
||||
with: {
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
return result || [];
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const createBitbucket = async (
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
message: "Error creating the Bitbucket provider",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
buildCompose,
|
||||
getBuildComposeCommand,
|
||||
@@ -45,24 +44,22 @@ import {
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { getDokployUrl } from "./admin";
|
||||
import { findAdminById, getDokployUrl } from "./admin";
|
||||
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { cleanupFullDocker } from "./settings";
|
||||
|
||||
export type Compose = typeof compose.$inferSelect;
|
||||
|
||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("compose", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await db
|
||||
@@ -70,6 +67,7 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
.values({
|
||||
...input,
|
||||
composeFile: "",
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -87,8 +85,9 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||
export const createComposeByTemplate = async (
|
||||
input: typeof compose.$inferInsert,
|
||||
) => {
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = cleanAppName(input.appName);
|
||||
if (appName) {
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
@@ -101,6 +100,7 @@ export const createComposeByTemplate = async (
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -184,10 +184,11 @@ export const updateCompose = async (
|
||||
composeId: string,
|
||||
composeData: Partial<Compose>,
|
||||
) => {
|
||||
const { appName, ...rest } = composeData;
|
||||
const composeResult = await db
|
||||
.update(compose)
|
||||
.set({
|
||||
...composeData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(compose.composeId, composeId))
|
||||
.returning();
|
||||
@@ -205,7 +206,10 @@ export const deployCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -213,6 +217,10 @@ export const deployCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await findAdminById(compose.project.adminId);
|
||||
if (admin.cleanupCacheOnCompose) {
|
||||
await cleanupFullDocker(compose?.serverId);
|
||||
}
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
...compose,
|
||||
@@ -240,6 +248,7 @@ export const deployCompose = async ({
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
domains: compose.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
@@ -251,7 +260,7 @@ export const deployCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
@@ -269,6 +278,7 @@ export const rebuildCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -276,6 +286,10 @@ export const rebuildCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await findAdminById(compose.project.adminId);
|
||||
if (admin.cleanupCacheOnCompose) {
|
||||
await cleanupFullDocker(compose?.serverId);
|
||||
}
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
} else {
|
||||
@@ -307,7 +321,10 @@ export const deployRemoteCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
|
||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||
compose.projectId
|
||||
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -315,6 +332,10 @@ export const deployRemoteCompose = async ({
|
||||
});
|
||||
try {
|
||||
if (compose.serverId) {
|
||||
const admin = await findAdminById(compose.project.adminId);
|
||||
if (admin.cleanupCacheOnCompose) {
|
||||
await cleanupFullDocker(compose?.serverId);
|
||||
}
|
||||
let command = "set -e;";
|
||||
|
||||
if (compose.sourceType === "github") {
|
||||
@@ -361,6 +382,7 @@ export const deployRemoteCompose = async ({
|
||||
applicationType: "compose",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
domains: compose.domains,
|
||||
});
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
@@ -382,7 +404,7 @@ export const deployRemoteCompose = async ({
|
||||
applicationName: compose.name,
|
||||
applicationType: "compose",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error to build",
|
||||
errorMessage: error?.message || "Error building",
|
||||
buildLink,
|
||||
adminId: compose.project.adminId,
|
||||
});
|
||||
@@ -400,6 +422,7 @@ export const rebuildRemoteCompose = async ({
|
||||
descriptionLog: string;
|
||||
}) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
|
||||
const deployment = await createDeploymentCompose({
|
||||
composeId: composeId,
|
||||
title: titleLog,
|
||||
@@ -407,6 +430,10 @@ export const rebuildRemoteCompose = async ({
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await findAdminById(compose.project.adminId);
|
||||
if (admin.cleanupCacheOnCompose) {
|
||||
await cleanupFullDocker(compose?.serverId);
|
||||
}
|
||||
if (compose.serverId) {
|
||||
await getBuildComposeCommand(compose, deployment.logPath);
|
||||
}
|
||||
@@ -436,13 +463,17 @@ export const rebuildRemoteCompose = async ({
|
||||
return true;
|
||||
};
|
||||
|
||||
export const removeCompose = async (compose: Compose) => {
|
||||
export const removeCompose = async (
|
||||
compose: Compose,
|
||||
deleteVolumes: boolean,
|
||||
) => {
|
||||
try {
|
||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
@@ -452,7 +483,13 @@ export const removeCompose = async (compose: Compose) => {
|
||||
cwd: projectPath,
|
||||
});
|
||||
} else {
|
||||
const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
||||
let command: string;
|
||||
if (deleteVolumes) {
|
||||
command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`;
|
||||
} else {
|
||||
command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
||||
}
|
||||
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(compose.serverId, command);
|
||||
} else {
|
||||
@@ -476,7 +513,11 @@ export const startCompose = async (composeId: string) => {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
|
||||
`cd ${join(
|
||||
COMPOSE_PATH,
|
||||
compose.appName,
|
||||
"code",
|
||||
)} && docker compose -p ${compose.appName} up -d`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
||||
@@ -506,7 +547,9 @@ export const stopCompose = async (composeId: string) => {
|
||||
if (compose.serverId) {
|
||||
await execAsyncRemote(
|
||||
compose.serverId,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
|
||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
|
||||
compose.appName
|
||||
} stop`,
|
||||
);
|
||||
} else {
|
||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||
|
||||
@@ -23,8 +23,8 @@ import { type Server, findServerById } from "./server";
|
||||
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
findPreviewDeploymentById,
|
||||
type PreviewDeployment,
|
||||
findPreviewDeploymentById,
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
|
||||
@@ -93,16 +93,27 @@ export const createDeployment = async (
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
applicationId: deployment.applicationId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
})
|
||||
.returning();
|
||||
await updateApplicationStatus(application.applicationId, "error");
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -159,18 +170,29 @@ export const createDeploymentPreview = async (
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
previewDeploymentId: deployment.previewDeploymentId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
})
|
||||
.returning();
|
||||
await updatePreviewDeployment(deployment.previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -221,18 +243,29 @@ echo "Initializing deployment" >> ${logFilePath};
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
await db
|
||||
.insert(deployments)
|
||||
.values({
|
||||
composeId: deployment.composeId,
|
||||
title: deployment.title || "Deployment",
|
||||
status: "error",
|
||||
logPath: "",
|
||||
description: deployment.description || "",
|
||||
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
|
||||
})
|
||||
.returning();
|
||||
await updateCompose(compose.composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -247,7 +280,7 @@ export const removeDeployment = async (deploymentId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this deployment",
|
||||
message: "Error deleting this deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -497,14 +530,14 @@ export const createServerDeployment = async (
|
||||
if (deploymentCreate.length === 0 || !deploymentCreate[0]) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
return deploymentCreate[0];
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the deployment",
|
||||
message: "Error creating the deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -157,6 +157,124 @@ export const getContainersByAppNameMatch = async (
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getStackContainersByAppName = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
result = stdout.trim().split("\n");
|
||||
} else {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
result = stdout.trim().split("\n");
|
||||
}
|
||||
|
||||
const containers = result.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||
: "No state";
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
node,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getServiceContainersByAppName = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let result: string[] = [];
|
||||
|
||||
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||
|
||||
if (serverId) {
|
||||
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
result = stdout.trim().split("\n");
|
||||
} else {
|
||||
const { stdout, stderr } = await execAsync(command);
|
||||
|
||||
if (stderr) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!stdout) return [];
|
||||
|
||||
result = stdout.trim().split("\n");
|
||||
}
|
||||
|
||||
const containers = result.map((line) => {
|
||||
const parts = line.split(" | ");
|
||||
const containerId = parts[0]
|
||||
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||
: "No container id";
|
||||
const name = parts[1]
|
||||
? parts[1].replace("Name: ", "").trim()
|
||||
: "No container name";
|
||||
|
||||
const state = parts[2]
|
||||
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||
: "No state";
|
||||
|
||||
const node = parts[3]
|
||||
? parts[3].replace("Node: ", "").trim()
|
||||
: "No specific node";
|
||||
return {
|
||||
containerId,
|
||||
name,
|
||||
state,
|
||||
node,
|
||||
};
|
||||
});
|
||||
|
||||
return containers || [];
|
||||
} catch (error) {}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getContainersByAppLabel = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
@@ -224,3 +342,123 @@ export const containerRestart = async (containerId: string) => {
|
||||
return config;
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getSwarmNodes = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = "docker node ls --format '{{json .}}'";
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
return nodesArray;
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||
try {
|
||||
const command = `docker node inspect ${nodeId} --format '{{json .}}'`;
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeInfo = JSON.parse(stdout);
|
||||
|
||||
return nodeInfo;
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getNodeApplications = async (serverId?: string) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = `docker service ls --format '{{json .}}'`;
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line))
|
||||
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||
|
||||
return appArray;
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
export const getApplicationInfo = async (
|
||||
appName: string,
|
||||
serverId?: string,
|
||||
) => {
|
||||
try {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`;
|
||||
|
||||
if (serverId) {
|
||||
const result = await execAsyncRemote(serverId, command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
} else {
|
||||
const result = await execAsync(command);
|
||||
stdout = result.stdout;
|
||||
stderr = result.stderr;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`Error: ${stderr}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const appArray = stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
return appArray;
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const createGithub = async (
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export const createGitlab = async (
|
||||
if (!newGitProvider) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the git provider",
|
||||
message: "Error creating the Git provider",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
backups,
|
||||
mariadb,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type Mariadb = typeof mariadb.$inferSelect;
|
||||
|
||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("mariadb", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newMariadb = await db
|
||||
@@ -40,6 +37,7 @@ export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||
databaseRootPassword: input.databaseRootPassword
|
||||
? input.databaseRootPassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -82,10 +80,11 @@ export const updateMariadbById = async (
|
||||
mariadbId: string,
|
||||
mariadbData: Partial<Mariadb>,
|
||||
) => {
|
||||
const { appName, ...rest } = mariadbData;
|
||||
const result = await db
|
||||
.update(mariadb)
|
||||
.set({
|
||||
...mariadbData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(mariadb.mariadbId, mariadbId))
|
||||
.returning();
|
||||
@@ -121,23 +120,33 @@ export const findMariadbByBackupId = async (backupId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMariadb = async (mariadbId: string) => {
|
||||
export const deployMariadb = async (
|
||||
mariadbId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const mariadb = await findMariadbById(mariadbId);
|
||||
try {
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting mariadb deployment...");
|
||||
if (mariadb.serverId) {
|
||||
await execAsyncRemote(
|
||||
mariadb.serverId,
|
||||
`docker pull ${mariadb.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(mariadb.dockerImage);
|
||||
await pullImage(mariadb.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildMariadb(mariadb);
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateMariadbById(mariadbId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
@@ -13,17 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type Mongo = typeof mongo.$inferSelect;
|
||||
|
||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("mongo", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newMongo = await db
|
||||
@@ -33,6 +30,7 @@ export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -74,10 +72,11 @@ export const updateMongoById = async (
|
||||
mongoId: string,
|
||||
mongoData: Partial<Mongo>,
|
||||
) => {
|
||||
const { appName, ...rest } = mongoData;
|
||||
const result = await db
|
||||
.update(mongo)
|
||||
.set({
|
||||
...mongoData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(mongo.mongoId, mongoId))
|
||||
.returning();
|
||||
@@ -113,20 +112,34 @@ export const removeMongoById = async (mongoId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMongo = async (mongoId: string) => {
|
||||
export const deployMongo = async (
|
||||
mongoId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const mongo = await findMongoById(mongoId);
|
||||
try {
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
|
||||
onData?.("Starting mongo deployment...");
|
||||
if (mongo.serverId) {
|
||||
await execAsyncRemote(mongo.serverId, `docker pull ${mongo.dockerImage}`);
|
||||
await execAsyncRemote(
|
||||
mongo.serverId,
|
||||
`docker pull ${mongo.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(mongo.dockerImage);
|
||||
await pullImage(mongo.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildMongo(mongo);
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateMongoById(mongoId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
if (!value) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting mount",
|
||||
message: "Error inserting mount",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const createMount = async (input: typeof apiCreateMount._type) => {
|
||||
console.log(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the mount",
|
||||
message: `Error ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -88,10 +88,10 @@ export const createFileMount = async (mountId: string) => {
|
||||
await createFile(baseFilePath, mount.filePath || "", mount.content || "");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error to create the file mount: ${error}`);
|
||||
console.log(`Error creating the file mount: ${error}`);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the mount",
|
||||
message: `Error creating the mount ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { buildAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
@@ -13,18 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type MySql = typeof mysql.$inferSelect;
|
||||
|
||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
|
||||
const appName = buildAppName("mysql", input.appName);
|
||||
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newMysql = await db
|
||||
@@ -37,6 +33,7 @@ export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||
databaseRootPassword: input.databaseRootPassword
|
||||
? input.databaseRootPassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -79,10 +76,11 @@ export const updateMySqlById = async (
|
||||
mysqlId: string,
|
||||
mysqlData: Partial<MySql>,
|
||||
) => {
|
||||
const { appName, ...rest } = mysqlData;
|
||||
const result = await db
|
||||
.update(mysql)
|
||||
.set({
|
||||
...mysqlData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(mysql.mysqlId, mysqlId))
|
||||
.returning();
|
||||
@@ -118,20 +116,33 @@ export const removeMySqlById = async (mysqlId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployMySql = async (mysqlId: string) => {
|
||||
export const deployMySql = async (
|
||||
mysqlId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const mysql = await findMySqlById(mysqlId);
|
||||
try {
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting mysql deployment...");
|
||||
if (mysql.serverId) {
|
||||
await execAsyncRemote(mysql.serverId, `docker pull ${mysql.dockerImage}`);
|
||||
await execAsyncRemote(
|
||||
mysql.serverId,
|
||||
`docker pull ${mysql.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(mysql.dockerImage);
|
||||
await pullImage(mysql.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildMysql(mysql);
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateMySqlById(mysqlId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -2,14 +2,17 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateDiscord,
|
||||
type apiCreateEmail,
|
||||
type apiCreateGotify,
|
||||
type apiCreateSlack,
|
||||
type apiCreateTelegram,
|
||||
type apiUpdateDiscord,
|
||||
type apiUpdateEmail,
|
||||
type apiUpdateGotify,
|
||||
type apiUpdateSlack,
|
||||
type apiUpdateTelegram,
|
||||
discord,
|
||||
email,
|
||||
gotify,
|
||||
notifications,
|
||||
slack,
|
||||
telegram,
|
||||
@@ -204,6 +207,7 @@ export const createDiscordNotification = async (
|
||||
.insert(discord)
|
||||
.values({
|
||||
webhookUrl: input.webhookUrl,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -272,6 +276,7 @@ export const updateDiscordNotification = async (
|
||||
.update(discord)
|
||||
.set({
|
||||
webhookUrl: input.webhookUrl,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.where(eq(discord.discordId, input.discordId))
|
||||
.returning()
|
||||
@@ -377,6 +382,96 @@ export const updateEmailNotification = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const createGotifyNotification = async (
|
||||
input: typeof apiCreateGotify._type,
|
||||
adminId: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newGotify = await tx
|
||||
.insert(gotify)
|
||||
.values({
|
||||
serverUrl: input.serverUrl,
|
||||
appToken: input.appToken,
|
||||
priority: input.priority,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newGotify) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting gotify",
|
||||
});
|
||||
}
|
||||
|
||||
const newDestination = await tx
|
||||
.insert(notifications)
|
||||
.values({
|
||||
gotifyId: newGotify.gotifyId,
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "gotify",
|
||||
adminId: adminId,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting notification",
|
||||
});
|
||||
}
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const updateGotifyNotification = async (
|
||||
input: typeof apiUpdateGotify._type,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const newDestination = await tx
|
||||
.update(notifications)
|
||||
.set({
|
||||
name: input.name,
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
adminId: input.adminId,
|
||||
})
|
||||
.where(eq(notifications.notificationId, input.notificationId))
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newDestination) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error Updating notification",
|
||||
});
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(gotify)
|
||||
.set({
|
||||
serverUrl: input.serverUrl,
|
||||
appToken: input.appToken,
|
||||
priority: input.priority,
|
||||
decoration: input.decoration,
|
||||
})
|
||||
.where(eq(gotify.gotifyId, input.gotifyId));
|
||||
|
||||
return newDestination;
|
||||
});
|
||||
};
|
||||
|
||||
export const findNotificationById = async (notificationId: string) => {
|
||||
const notification = await db.query.notifications.findFirst({
|
||||
where: eq(notifications.notificationId, notificationId),
|
||||
@@ -385,6 +480,7 @@ export const findNotificationById = async (notificationId: string) => {
|
||||
telegram: true,
|
||||
discord: true,
|
||||
email: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
backups,
|
||||
postgres,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
export type Postgres = typeof postgres.$inferSelect;
|
||||
|
||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("postgres", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newPostgres = await db
|
||||
@@ -37,6 +34,7 @@ export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -96,10 +94,11 @@ export const updatePostgresById = async (
|
||||
postgresId: string,
|
||||
postgresData: Partial<Postgres>,
|
||||
) => {
|
||||
const { appName, ...rest } = postgresData;
|
||||
const result = await db
|
||||
.update(postgres)
|
||||
.set({
|
||||
...postgresData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(postgres.postgresId, postgresId))
|
||||
.returning();
|
||||
@@ -116,24 +115,37 @@ export const removePostgresById = async (postgresId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployPostgres = async (postgresId: string) => {
|
||||
export const deployPostgres = async (
|
||||
postgresId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const postgres = await findPostgresById(postgresId);
|
||||
try {
|
||||
const promises = [];
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
|
||||
onData?.("Starting postgres deployment...");
|
||||
|
||||
if (postgres.serverId) {
|
||||
const result = await execAsyncRemote(
|
||||
await execAsyncRemote(
|
||||
postgres.serverId,
|
||||
`docker pull ${postgres.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(postgres.dockerImage);
|
||||
await pullImage(postgres.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildPostgres(postgres);
|
||||
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updatePostgresById(postgresId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -7,20 +7,20 @@ import {
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
import { slugify } from "../setup/server-setup";
|
||||
import { findApplicationById } from "./application";
|
||||
import { createDomain } from "./domain";
|
||||
import { generatePassword, generateRandomDomain } from "../templates/utils";
|
||||
import { removeService } from "../utils/docker/utils";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { manageDomain } from "../utils/traefik/domain";
|
||||
import { findAdminById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import {
|
||||
removeDeployments,
|
||||
removeDeploymentsByPreviewDeploymentId,
|
||||
} from "./deployment";
|
||||
import { removeDirectoryCode } from "../utils/filesystem/directory";
|
||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||
import { removeService } from "../utils/docker/utils";
|
||||
import { authGithub } from "../utils/providers/github";
|
||||
import { getIssueComment, type Github } from "./github";
|
||||
import { findAdminById } from "./admin";
|
||||
import { createDomain } from "./domain";
|
||||
import { type Github, getIssueComment } from "./github";
|
||||
|
||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||
|
||||
@@ -112,7 +112,7 @@ export const removePreviewDeployment = async (previewDeploymentId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this preview deployment",
|
||||
message: "Error deleting this preview deployment",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -189,7 +189,7 @@ export const createPreviewDeployment = async (
|
||||
if (!previewDeployment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the preview deployment",
|
||||
message: "Error creating the preview deployment",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export const createProject = async (
|
||||
if (!newProject) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the project",
|
||||
message: "Error creating the project",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export const createRedirect = async (
|
||||
if (!redirect) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the redirect",
|
||||
message: "Error creating the redirect",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const createRedirect = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this redirect",
|
||||
message: "Error creating this redirect",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -82,7 +82,7 @@ export const removeRedirectById = async (redirectId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this redirect",
|
||||
message: "Error removing this redirect",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export const updateRedirectById = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this redirect",
|
||||
message: "Error updating this redirect",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { type apiCreateRedis, redis } from "@dokploy/server/db/schema";
|
||||
import { generateAppName } from "@dokploy/server/db/schema";
|
||||
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||
import { buildRedis } from "@dokploy/server/utils/databases/redis";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
@@ -14,17 +14,14 @@ export type Redis = typeof redis.$inferSelect;
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
input.appName =
|
||||
`${input.appName}-${generatePassword(6)}` || generateAppName("redis");
|
||||
if (input.appName) {
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
const appName = buildAppName("redis", input.appName);
|
||||
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
const valid = await validUniqueServerAppName(appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newRedis = await db
|
||||
@@ -34,6 +31,7 @@ export const createRedis = async (input: typeof apiCreateRedis._type) => {
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -70,10 +68,11 @@ export const updateRedisById = async (
|
||||
redisId: string,
|
||||
redisData: Partial<Redis>,
|
||||
) => {
|
||||
const { appName, ...rest } = redisData;
|
||||
const result = await db
|
||||
.update(redis)
|
||||
.set({
|
||||
...redisData,
|
||||
...rest,
|
||||
})
|
||||
.where(eq(redis.redisId, redisId))
|
||||
.returning();
|
||||
@@ -90,20 +89,34 @@ export const removeRedisById = async (redisId: string) => {
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployRedis = async (redisId: string) => {
|
||||
export const deployRedis = async (
|
||||
redisId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const redis = await findRedisById(redisId);
|
||||
try {
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
|
||||
onData?.("Starting redis deployment...");
|
||||
if (redis.serverId) {
|
||||
await execAsyncRemote(redis.serverId, `docker pull ${redis.dockerImage}`);
|
||||
await execAsyncRemote(
|
||||
redis.serverId,
|
||||
`docker pull ${redis.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(redis.dockerImage);
|
||||
await pullImage(redis.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildRedis(redis);
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateRedisById(redisId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export const removeRegistry = async (registryId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this registry",
|
||||
message: "Error removing this registry",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export const updateRegistry = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this registry",
|
||||
message: "Error updating this registry",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const createSecurity = async (
|
||||
if (!securityResponse) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the security",
|
||||
message: "Error creating the security",
|
||||
});
|
||||
}
|
||||
await createSecurityMiddleware(application, securityResponse);
|
||||
@@ -50,7 +50,7 @@ export const createSecurity = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create this security",
|
||||
message: "Error creating this security",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export const deleteSecurityById = async (securityId: string) => {
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to remove this security",
|
||||
message: "Error removing this security",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -100,7 +100,7 @@ export const updateSecurityById = async (
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this security",
|
||||
message: "Error updating this security",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const createServer = async (
|
||||
if (!newServer) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the server",
|
||||
message: "Error creating the server",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +1,109 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { docker } from "@dokploy/server/constants";
|
||||
import { getServiceContainer } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { findAdminById } from "./admin";
|
||||
// import packageInfo from "../../../package.json";
|
||||
|
||||
const updateIsAvailable = async () => {
|
||||
try {
|
||||
const service = await getServiceContainer("dokploy");
|
||||
export interface IUpdateData {
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
const localImage = await docker.getImage(getDokployImage()).inspect();
|
||||
return localImage.Id !== service?.ImageID;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
export const DEFAULT_UPDATE_DATA: IUpdateData = {
|
||||
latestVersion: null,
|
||||
updateAvailable: false,
|
||||
};
|
||||
|
||||
/** Returns current Dokploy docker image tag or `latest` by default. */
|
||||
export const getDokployImageTag = () => {
|
||||
return process.env.RELEASE_TAG || "latest";
|
||||
};
|
||||
|
||||
export const getDokployImage = () => {
|
||||
return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`;
|
||||
return `dokploy/dokploy:${getDokployImageTag()}`;
|
||||
};
|
||||
|
||||
export const pullLatestRelease = async () => {
|
||||
try {
|
||||
const stream = await docker.pull(getDokployImage(), {});
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
const newUpdateIsAvailable = await updateIsAvailable();
|
||||
return newUpdateIsAvailable;
|
||||
} catch (error) {}
|
||||
|
||||
return false;
|
||||
const stream = await docker.pull(getDokployImage());
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.modem.followProgress(stream, (err, res) =>
|
||||
err ? reject(err) : resolve(res),
|
||||
);
|
||||
});
|
||||
};
|
||||
export const getDokployVersion = () => {
|
||||
// return packageInfo.version;
|
||||
|
||||
/** Returns Dokploy docker service image digest */
|
||||
export const getServiceImageDigest = async () => {
|
||||
const { stdout } = await execAsync(
|
||||
"docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'",
|
||||
);
|
||||
|
||||
const currentDigest = stdout.trim().split("@")[1];
|
||||
|
||||
if (!currentDigest) {
|
||||
throw new Error("Could not get current service image digest");
|
||||
}
|
||||
|
||||
return currentDigest;
|
||||
};
|
||||
|
||||
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
|
||||
export const getUpdateData = async (): Promise<IUpdateData> => {
|
||||
let currentDigest: string;
|
||||
try {
|
||||
currentDigest = await getServiceImageDigest();
|
||||
} catch {
|
||||
// Docker service might not exist locally
|
||||
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
|
||||
// https://docs.dokploy.com/docs/core/manual-installation
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
|
||||
let url: string | null = `${baseUrl}?page_size=100`;
|
||||
let allResults: { digest: string; name: string }[] = [];
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const data = (await response.json()) as {
|
||||
next: string | null;
|
||||
results: { digest: string; name: string }[];
|
||||
};
|
||||
|
||||
allResults = allResults.concat(data.results);
|
||||
url = data?.next;
|
||||
}
|
||||
|
||||
const imageTag = getDokployImageTag();
|
||||
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
|
||||
|
||||
if (!searchedDigest) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
if (imageTag === "latest") {
|
||||
const versionedTag = allResults.find(
|
||||
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
|
||||
);
|
||||
|
||||
if (!versionedTag) {
|
||||
return DEFAULT_UPDATE_DATA;
|
||||
}
|
||||
|
||||
const { name: latestVersion, digest } = versionedTag;
|
||||
const updateAvailable = digest !== currentDigest;
|
||||
|
||||
return { latestVersion, updateAvailable };
|
||||
}
|
||||
const updateAvailable = searchedDigest !== currentDigest;
|
||||
return { latestVersion: imageTag, updateAvailable };
|
||||
};
|
||||
|
||||
interface TreeDataItem {
|
||||
@@ -146,3 +214,35 @@ echo "$json_output"
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const cleanupFullDocker = async (serverId?: string | null) => {
|
||||
const cleanupImages = "docker image prune --force";
|
||||
const cleanupVolumes = "docker volume prune --force";
|
||||
const cleanupContainers = "docker container prune --force";
|
||||
const cleanupSystem = "docker system prune --all --force --volumes";
|
||||
const cleanupBuilder = "docker builder prune --all --force";
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`,
|
||||
);
|
||||
}
|
||||
await execAsync(`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export const createSshKey = async (input: typeof apiCreateSshKey._type) => {
|
||||
if (!sshKey) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to create the ssh key",
|
||||
message: "Error creating the SSH Key",
|
||||
});
|
||||
}
|
||||
return sshKey;
|
||||
|
||||
@@ -54,7 +54,7 @@ export const addNewProject = async (authId: string, projectId: string) => {
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
accesedProjects: [...user.accesedProjects, projectId],
|
||||
accessedProjects: [...user.accessedProjects, projectId],
|
||||
})
|
||||
.where(eq(users.authId, authId));
|
||||
};
|
||||
@@ -64,7 +64,7 @@ export const addNewService = async (authId: string, serviceId: string) => {
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
accesedServices: [...user.accesedServices, serviceId],
|
||||
accessedServices: [...user.accessedServices, serviceId],
|
||||
})
|
||||
.where(eq(users.authId, authId));
|
||||
};
|
||||
@@ -73,8 +73,9 @@ export const canPerformCreationService = async (
|
||||
userId: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
const { accesedProjects, canCreateServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
||||
const { accessedProjects, canCreateServices } =
|
||||
await findUserByAuthId(userId);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (canCreateServices && haveAccessToProject) {
|
||||
return true;
|
||||
@@ -87,8 +88,8 @@ export const canPerformAccessService = async (
|
||||
userId: string,
|
||||
serviceId: string,
|
||||
) => {
|
||||
const { accesedServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToService = accesedServices.includes(serviceId);
|
||||
const { accessedServices } = await findUserByAuthId(userId);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (haveAccessToService) {
|
||||
return true;
|
||||
@@ -101,8 +102,9 @@ export const canPeformDeleteService = async (
|
||||
authId: string,
|
||||
serviceId: string,
|
||||
) => {
|
||||
const { accesedServices, canDeleteServices } = await findUserByAuthId(authId);
|
||||
const haveAccessToService = accesedServices.includes(serviceId);
|
||||
const { accessedServices, canDeleteServices } =
|
||||
await findUserByAuthId(authId);
|
||||
const haveAccessToService = accessedServices.includes(serviceId);
|
||||
|
||||
if (canDeleteServices && haveAccessToService) {
|
||||
return true;
|
||||
@@ -135,9 +137,9 @@ export const canPerformAccessProject = async (
|
||||
authId: string,
|
||||
projectId: string,
|
||||
) => {
|
||||
const { accesedProjects } = await findUserByAuthId(authId);
|
||||
const { accessedProjects } = await findUserByAuthId(authId);
|
||||
|
||||
const haveAccessToProject = accesedProjects.includes(projectId);
|
||||
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||
|
||||
if (haveAccessToProject) {
|
||||
return true;
|
||||
|
||||
114
packages/server/src/setup/server-audit.ts
Normal file
114
packages/server/src/setup/server-audit.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Client } from "ssh2";
|
||||
import { findServerById } from "../services/server";
|
||||
|
||||
// Thanks for the idea to https://github.com/healthyhost/audit-vps-script/tree/main
|
||||
const validateUfw = () => `
|
||||
if command -v ufw >/dev/null 2>&1; then
|
||||
isInstalled=true
|
||||
isActive=$(sudo ufw status | grep -q "Status: active" && echo true || echo false)
|
||||
defaultIncoming=$(sudo ufw status verbose | grep "Default:" | grep "incoming" | awk '{print $2}')
|
||||
echo "{\\"installed\\": $isInstalled, \\"active\\": $isActive, \\"defaultIncoming\\": \\"$defaultIncoming\\"}"
|
||||
else
|
||||
echo "{\\"installed\\": false, \\"active\\": false, \\"defaultIncoming\\": \\"unknown\\"}"
|
||||
fi
|
||||
`;
|
||||
|
||||
const validateSsh = () => `
|
||||
if systemctl is-active --quiet sshd; then
|
||||
isEnabled=true
|
||||
hasKeyAuth=$(find "$HOME/.ssh" -type f -name "authorized_keys" 2>/dev/null | grep -q . && echo true || echo false)
|
||||
permitRootLogin=$(sudo sshd -T | grep -i "^PermitRootLogin" | awk '{print $2}')
|
||||
passwordAuth=$(sudo sshd -T | grep -i "^PasswordAuthentication" | awk '{print $2}')
|
||||
usePam=$(sudo sshd -T | grep -i "^UsePAM" | awk '{print $2}')
|
||||
echo "{\\"enabled\\": $isEnabled, \\"keyAuth\\": $hasKeyAuth, \\"permitRootLogin\\": \\"$permitRootLogin\\", \\"passwordAuth\\": \\"$passwordAuth\\", \\"usePam\\": \\"$usePam\\"}"
|
||||
else
|
||||
echo "{\\"enabled\\": false, \\"keyAuth\\": false, \\"permitRootLogin\\": \\"unknown\\", \\"passwordAuth\\": \\"unknown\\", \\"usePam\\": \\"unknown\\"}"
|
||||
fi
|
||||
`;
|
||||
|
||||
const validateFail2ban = () => `
|
||||
if dpkg -l | grep -q "fail2ban"; then
|
||||
isInstalled=true
|
||||
isEnabled=$(systemctl is-enabled --quiet fail2ban.service && echo true || echo false)
|
||||
isActive=$(systemctl is-active --quiet fail2ban.service && echo true || echo false)
|
||||
|
||||
if [ -f "/etc/fail2ban/jail.local" ]; then
|
||||
sshEnabled=$(grep -A10 "^\\[sshd\\]" /etc/fail2ban/jail.local | grep "enabled" | awk '{print $NF}' | tr -d '[:space:]')
|
||||
sshMode=$(grep -A10 "^\\[sshd\\]" /etc/fail2ban/jail.local | grep "^mode[[:space:]]*=[[:space:]]*aggressive" >/dev/null && echo "aggressive" || echo "normal")
|
||||
echo "{\\"installed\\": $isInstalled, \\"enabled\\": $isEnabled, \\"active\\": $isActive, \\"sshEnabled\\": \\"$sshEnabled\\", \\"sshMode\\": \\"$sshMode\\"}"
|
||||
else
|
||||
echo "{\\"installed\\": $isInstalled, \\"enabled\\": $isEnabled, \\"active\\": $isActive, \\"sshEnabled\\": \\"false\\", \\"sshMode\\": \\"normal\\"}"
|
||||
fi
|
||||
else
|
||||
echo "{\\"installed\\": false, \\"enabled\\": false, \\"active\\": false, \\"sshEnabled\\": \\"false\\", \\"sshMode\\": \\"normal\\"}"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const serverAudit = async (serverId: string) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) {
|
||||
throw new Error("No SSH Key found");
|
||||
}
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const bashCommand = `
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
ufwStatus=$(${validateUfw()})
|
||||
sshStatus=$(${validateSsh()})
|
||||
fail2banStatus=$(${validateFail2ban()})
|
||||
|
||||
echo "{\\"ufw\\": $ufwStatus, \\"ssh\\": $sshStatus, \\"fail2ban\\": $fail2banStatus}"
|
||||
`;
|
||||
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let output = "";
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
try {
|
||||
const result = JSON.parse(output.trim());
|
||||
resolve(result);
|
||||
} catch (parseError) {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
output += data;
|
||||
})
|
||||
.stderr.on("data", (data) => {});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import {
|
||||
@@ -7,6 +6,9 @@ import {
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
TRAEFIK_PORT,
|
||||
TRAEFIK_SSL_PORT,
|
||||
TRAEFIK_VERSION,
|
||||
getDefaultMiddlewares,
|
||||
getDefaultServerTraefikConfig,
|
||||
} from "@dokploy/server/setup/traefik-setup";
|
||||
@@ -29,7 +31,10 @@ export const slugify = (text: string | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const serverSetup = async (serverId: string) => {
|
||||
export const serverSetup = async (
|
||||
serverId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const server = await findServerById(serverId);
|
||||
const { LOGS_PATH } = paths();
|
||||
|
||||
@@ -45,136 +50,149 @@ export const serverSetup = async (serverId: string) => {
|
||||
description: "Setup Server",
|
||||
});
|
||||
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
try {
|
||||
writeStream.write("\nInstalling Server Dependencies: ✅\n");
|
||||
await installRequirements(serverId, deployment.logPath);
|
||||
writeStream.close();
|
||||
onData?.("\nInstalling Server Dependencies: ✅\n");
|
||||
await installRequirements(serverId, onData);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
|
||||
onData?.("\nSetup Server: ✅\n");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
writeStream.write(err);
|
||||
writeStream.close();
|
||||
onData?.(`${err} ❌\n`);
|
||||
}
|
||||
};
|
||||
|
||||
const installRequirements = async (serverId: string, logPath: string) => {
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
export const defaultCommand = () => {
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
DOCKER_VERSION=27.0.3
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
SYS_ARCH=$(uname -m)
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
dnf install -y findutils >/dev/null
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| CPU Architecture | $SYS_ARCH"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
${installUtilities()}
|
||||
|
||||
echo -e "2. Validating ports. "
|
||||
${validatePorts()}
|
||||
|
||||
|
||||
|
||||
echo -e "3. Installing RClone. "
|
||||
${installRClone()}
|
||||
|
||||
echo -e "4. Installing Docker. "
|
||||
${installDocker()}
|
||||
|
||||
echo -e "5. Setting up Docker Swarm"
|
||||
${setupSwarm()}
|
||||
|
||||
echo -e "6. Setting up Network"
|
||||
${setupNetwork()}
|
||||
|
||||
echo -e "7. Setting up Directories"
|
||||
${setupMainDirectory()}
|
||||
${setupDirectories()}
|
||||
|
||||
echo -e "8. Setting up Traefik"
|
||||
${createTraefikConfig()}
|
||||
|
||||
echo -e "9. Setting up Middlewares"
|
||||
${createDefaultMiddlewares()}
|
||||
|
||||
echo -e "10. Setting up Traefik Instance"
|
||||
${createTraefikInstance()}
|
||||
|
||||
echo -e "11. Installing Nixpacks"
|
||||
${installNixpacks()}
|
||||
|
||||
echo -e "12. Installing Buildpacks"
|
||||
${installBuildpacks()}
|
||||
`;
|
||||
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
const installRequirements = async (
|
||||
serverId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) {
|
||||
writeStream.write("❌ No SSH Key found");
|
||||
writeStream.close();
|
||||
onData?.("❌ No SSH Key found, please assign one to this server");
|
||||
throw new Error("No SSH Key found");
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const bashCommand = `
|
||||
set -e;
|
||||
# Thanks to coolify <3
|
||||
|
||||
DOCKER_VERSION=27.0.3
|
||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
CURRENT_USER=$USER
|
||||
|
||||
echo "Installing requirements for: OS: $OS_TYPE"
|
||||
if [ $EUID != 0 ]; then
|
||||
echo "Please run this script as root or with sudo ❌"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Check if the OS is manjaro, if so, change it to arch
|
||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||
OS_TYPE="arch"
|
||||
fi
|
||||
|
||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||
OS_TYPE="fedora"
|
||||
fi
|
||||
|
||||
# Check if the OS is popOS, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "pop" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
#Check if the OS is zorin, if so, change it to ubuntu
|
||||
if [ "$OS_TYPE" = "zorin" ]; then
|
||||
OS_TYPE="ubuntu"
|
||||
fi
|
||||
|
||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||
OS_VERSION="rolling"
|
||||
else
|
||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "---------------------------------------------"
|
||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||
echo "| Docker | $DOCKER_VERSION"
|
||||
echo -e "---------------------------------------------\n"
|
||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||
|
||||
${installUtilities()}
|
||||
|
||||
echo -e "2. Validating ports. "
|
||||
${validatePorts()}
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
echo -e "3. Installing RClone. "
|
||||
${installRClone()}
|
||||
|
||||
echo -e "4. Installing Docker. "
|
||||
${installDocker()}
|
||||
|
||||
echo -e "5. Setting up Docker Swarm"
|
||||
${setupSwarm()}
|
||||
|
||||
echo -e "6. Setting up Network"
|
||||
${setupNetwork()}
|
||||
|
||||
echo -e "7. Setting up Directories"
|
||||
${setupMainDirectory()}
|
||||
${setupDirectories()}
|
||||
|
||||
echo -e "8. Setting up Traefik"
|
||||
${createTraefikConfig()}
|
||||
|
||||
echo -e "9. Setting up Middlewares"
|
||||
${createDefaultMiddlewares()}
|
||||
|
||||
echo -e "10. Setting up Traefik Instance"
|
||||
${createTraefikInstance()}
|
||||
|
||||
echo -e "11. Installing Nixpacks"
|
||||
${installNixpacks()}
|
||||
|
||||
echo -e "12. Installing Buildpacks"
|
||||
${installBuildpacks()}
|
||||
`;
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
const command = server.command || defaultCommand();
|
||||
client.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
writeStream.write(err);
|
||||
onData?.(err.message);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
@@ -184,17 +202,17 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
resolve();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
writeStream.write(data.toString());
|
||||
onData?.(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
writeStream.write(data.toString());
|
||||
onData?.(data.toString());
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
writeStream.write(
|
||||
onData?.(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
reject(
|
||||
@@ -203,9 +221,7 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
writeStream.write(
|
||||
`SSH connection error: ${err.message} ${err.level}`,
|
||||
);
|
||||
onData?.(`SSH connection error: ${err.message} ${err.level}`);
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
}
|
||||
})
|
||||
@@ -214,7 +230,6 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -256,20 +271,49 @@ export const setupSwarm = () => `
|
||||
else
|
||||
# Get IP address
|
||||
get_ip() {
|
||||
# Try to get IPv4
|
||||
local ipv4=\$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||
local ip=""
|
||||
|
||||
# Try IPv4 with multiple services
|
||||
# First attempt: ifconfig.io
|
||||
ip=\$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# Second attempt: icanhazip.com
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Third attempt: ipecho.net
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -n "\$ipv4" ]; then
|
||||
echo "\$ipv4"
|
||||
else
|
||||
# Try to get IPv6
|
||||
local ipv6=\$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||
if [ -n "\$ipv6" ]; then
|
||||
echo "\$ipv6"
|
||||
# If no IPv4, try IPv6 with multiple services
|
||||
if [ -z "\$ip" ]; then
|
||||
# Try IPv6 with ifconfig.io
|
||||
ip=\$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null)
|
||||
|
||||
# Try IPv6 with icanhazip.com
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null)
|
||||
fi
|
||||
|
||||
# Try IPv6 with ipecho.net
|
||||
if [ -z "\$ip" ]; then
|
||||
ip=\$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "\$ip" ]; then
|
||||
echo "Error: Could not determine server IP address automatically (neither IPv4 nor IPv6)." >&2
|
||||
echo "Please set the ADVERTISE_ADDR environment variable manually." >&2
|
||||
echo "Example: export ADVERTISE_ADDR=<your-server-ip>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "\$ip"
|
||||
}
|
||||
advertise_addr=\$(get_ip)
|
||||
echo "Advertise address: \$advertise_addr"
|
||||
|
||||
# Initialize Docker Swarm
|
||||
docker swarm init --advertise-addr \$advertise_addr
|
||||
@@ -316,8 +360,8 @@ const installUtilities = () => `
|
||||
apk add curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
ubuntu | debian | raspbian)
|
||||
apt-get update -y >/dev/null
|
||||
apt-get install -y curl wget git jq openssl >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
@@ -487,7 +531,8 @@ export const installRClone = () => `
|
||||
echo "RClone already installed ✅"
|
||||
else
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
echo "RClone installed successfully ✅"
|
||||
RCLONE_VERSION=$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//')
|
||||
echo "RClone version $RCLONE_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -498,18 +543,20 @@ export const createTraefikInstance = () => {
|
||||
echo "Traefik already exists ✅"
|
||||
else
|
||||
# Create the dokploy-traefik service
|
||||
docker service create \
|
||||
--name dokploy-traefik \
|
||||
--replicas 1 \
|
||||
--constraint 'node.role==manager' \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \
|
||||
--mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \
|
||||
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
--label traefik.enable=true \
|
||||
--publish mode=host,target=443,published=443 \
|
||||
--publish mode=host,target=80,published=80 \
|
||||
traefik:v3.1.2
|
||||
TRAEFIK_VERSION=${TRAEFIK_VERSION}
|
||||
docker service create \
|
||||
--name dokploy-traefik \
|
||||
--replicas 1 \
|
||||
--constraint 'node.role==manager' \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \
|
||||
--mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \
|
||||
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
--label traefik.enable=true \
|
||||
--publish mode=host,target=${TRAEFIK_SSL_PORT},published=${TRAEFIK_SSL_PORT} \
|
||||
--publish mode=host,target=${TRAEFIK_PORT},published=${TRAEFIK_PORT} \
|
||||
traefik:v$TRAEFIK_VERSION
|
||||
echo "Traefik version $TRAEFIK_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
@@ -520,16 +567,22 @@ const installNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
echo "Nixpacks already installed ✅"
|
||||
else
|
||||
VERSION=1.28.1 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version 1.28.1 installed ✅"
|
||||
export NIXPACKS_VERSION=1.29.1
|
||||
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
|
||||
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
const installBuildpacks = () => `
|
||||
SUFFIX=""
|
||||
if [ "$SYS_ARCH" = "aarch64" ] || [ "$SYS_ARCH" = "arm64" ]; then
|
||||
SUFFIX="-arm64"
|
||||
fi
|
||||
if command_exists pack; then
|
||||
echo "Buildpacks already installed ✅"
|
||||
else
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version 0.35.0 installed ✅"
|
||||
BUILDPACKS_VERSION=0.35.0
|
||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
|
||||
fi
|
||||
`;
|
||||
|
||||
153
packages/server/src/setup/server-validate.ts
Normal file
153
packages/server/src/setup/server-validate.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { Client } from "ssh2";
|
||||
import { findServerById } from "../services/server";
|
||||
|
||||
export const validateDocker = () => `
|
||||
if command_exists docker; then
|
||||
echo "$(docker --version | awk '{print $3}' | sed 's/,//') true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateRClone = () => `
|
||||
if command_exists rclone; then
|
||||
echo "$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//') true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateSwarm = () => `
|
||||
if docker info --format '{{.Swarm.LocalNodeState}}' | grep -q 'active'; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateNixpacks = () => `
|
||||
if command_exists nixpacks; then
|
||||
version=$(nixpacks --version | awk '{print $2}')
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateBuildpacks = () => `
|
||||
if command_exists pack; then
|
||||
version=$(pack --version | awk '{print $1}')
|
||||
if [ -n "$version" ]; then
|
||||
echo "$version true"
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
else
|
||||
echo "0.0.0 false"
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateMainDirectory = () => `
|
||||
if [ -d "/etc/dokploy" ]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
`;
|
||||
|
||||
export const validateDokployNetwork = () => `
|
||||
if docker network ls | grep -q 'dokploy-network'; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
`;
|
||||
|
||||
export const serverValidate = async (serverId: string) => {
|
||||
const client = new Client();
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) {
|
||||
throw new Error("No SSH Key found");
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const bashCommand = `
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
dockerVersionEnabled=$(${validateDocker()})
|
||||
rcloneVersionEnabled=$(${validateRClone()})
|
||||
nixpacksVersionEnabled=$(${validateNixpacks()})
|
||||
buildpacksVersionEnabled=$(${validateBuildpacks()})
|
||||
|
||||
dockerVersion=$(echo $dockerVersionEnabled | awk '{print $1}')
|
||||
dockerEnabled=$(echo $dockerVersionEnabled | awk '{print $2}')
|
||||
|
||||
rcloneVersion=$(echo $rcloneVersionEnabled | awk '{print $1}')
|
||||
rcloneEnabled=$(echo $rcloneVersionEnabled | awk '{print $2}')
|
||||
|
||||
nixpacksVersion=$(echo $nixpacksVersionEnabled | awk '{print $1}')
|
||||
nixpacksEnabled=$(echo $nixpacksVersionEnabled | awk '{print $2}')
|
||||
|
||||
buildpacksVersion=$(echo $buildpacksVersionEnabled | awk '{print $1}')
|
||||
buildpacksEnabled=$(echo $buildpacksVersionEnabled | awk '{print $2}')
|
||||
|
||||
isDokployNetworkInstalled=$(${validateDokployNetwork()})
|
||||
isSwarmInstalled=$(${validateSwarm()})
|
||||
isMainDirectoryInstalled=$(${validateMainDirectory()})
|
||||
|
||||
echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}"
|
||||
`;
|
||||
client.exec(bashCommand, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
let output = "";
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
try {
|
||||
const result = JSON.parse(output.trim());
|
||||
resolve(result);
|
||||
} catch (parseError) {
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
output += data;
|
||||
})
|
||||
.stderr.on("data", (data) => {});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -8,23 +8,31 @@ import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import type { FileConfig } from "../utils/traefik/file-types";
|
||||
import type { MainTraefikConfig } from "../utils/traefik/types";
|
||||
|
||||
const TRAEFIK_SSL_PORT =
|
||||
export const TRAEFIK_SSL_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
|
||||
const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
export const TRAEFIK_PORT =
|
||||
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
|
||||
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
|
||||
|
||||
interface TraefikOptions {
|
||||
enableDashboard?: boolean;
|
||||
env?: string[];
|
||||
serverId?: string;
|
||||
additionalPorts?: {
|
||||
targetPort: number;
|
||||
publishedPort: number;
|
||||
publishMode?: "ingress" | "host";
|
||||
}[];
|
||||
}
|
||||
|
||||
export const initializeTraefik = async ({
|
||||
enableDashboard = false,
|
||||
env,
|
||||
serverId,
|
||||
additionalPorts = [],
|
||||
}: TraefikOptions = {}) => {
|
||||
const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId);
|
||||
const imageName = "traefik:v3.1.2";
|
||||
const imageName = `traefik:v${TRAEFIK_VERSION}`;
|
||||
const containerName = "dokploy-traefik";
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: containerName,
|
||||
@@ -84,6 +92,11 @@ export const initializeTraefik = async ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...additionalPorts.map((port) => ({
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
PublishMode: port.publishMode || ("host" as const),
|
||||
})),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -176,10 +189,12 @@ export const getDefaultTraefikConfig = () => {
|
||||
: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
watch: true,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
watch: true,
|
||||
network: "dokploy-network",
|
||||
},
|
||||
}),
|
||||
file: {
|
||||
@@ -230,10 +245,12 @@ export const getDefaultServerTraefikConfig = () => {
|
||||
providers: {
|
||||
swarm: {
|
||||
exposedByDefault: false,
|
||||
watch: false,
|
||||
watch: true,
|
||||
},
|
||||
docker: {
|
||||
exposedByDefault: false,
|
||||
watch: true,
|
||||
network: "dokploy-network",
|
||||
},
|
||||
file: {
|
||||
directory: "/etc/dokploy/traefik/dynamic",
|
||||
|
||||
@@ -111,7 +111,7 @@ class LogRotationManager {
|
||||
);
|
||||
console.log("USR1 Signal send to Traefik");
|
||||
} catch (error) {
|
||||
console.error("Error to send USR1 Signal to Traefik:", error);
|
||||
console.error("Error sending USR1 Signal to Traefik:", error);
|
||||
}
|
||||
}
|
||||
public async getStatus(): Promise<boolean> {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
} from "../docker/utils";
|
||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
@@ -25,21 +26,26 @@ export const initCronJobs = async () => {
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
await sendDockerCleanupNotifications(admin.adminId);
|
||||
});
|
||||
}
|
||||
|
||||
const servers = await getAllServers();
|
||||
|
||||
for (const server of servers) {
|
||||
const { appName, serverId } = server;
|
||||
if (serverId) {
|
||||
const { serverId, enableDockerCleanup, name } = server;
|
||||
if (enableDockerCleanup) {
|
||||
scheduleJob(serverId, "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`,
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.adminId,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,8 +65,11 @@ export const initCronJobs = async () => {
|
||||
});
|
||||
for (const pg of pgs) {
|
||||
for (const backup of pg.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
const { schedule, backupId, enabled, database } = backup;
|
||||
if (enabled) {
|
||||
console.log(
|
||||
`[Backup] Postgres DB ${pg.name} for ${database} Activated`,
|
||||
);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
@@ -87,8 +96,11 @@ export const initCronJobs = async () => {
|
||||
|
||||
for (const maria of mariadbs) {
|
||||
for (const backup of maria.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
const { schedule, backupId, enabled, database } = backup;
|
||||
if (enabled) {
|
||||
console.log(
|
||||
`[Backup] MariaDB DB ${maria.name} for ${database} Activated`,
|
||||
);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
@@ -117,6 +129,7 @@ export const initCronJobs = async () => {
|
||||
for (const backup of mongo.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
console.log(`[Backup] MongoDB DB ${mongo.name} Activated`);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
@@ -145,6 +158,7 @@ export const initCronJobs = async () => {
|
||||
for (const backup of mysql.backups) {
|
||||
const { schedule, backupId, enabled } = backup;
|
||||
if (enabled) {
|
||||
console.log(`[Backup] MySQL DB ${mysql.name} Activated`);
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
console.log(
|
||||
`MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`,
|
||||
|
||||
@@ -49,6 +49,7 @@ Compose Type: ${composeType} ✅`;
|
||||
writeStream.write(`\n${logBox}\n`);
|
||||
|
||||
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[...command.split(" ")],
|
||||
@@ -68,7 +69,7 @@ Compose Type: ${composeType} ✅`;
|
||||
|
||||
writeStream.write("Docker Compose Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write("Error ❌");
|
||||
writeStream.write(`Error ❌ ${(error as Error).message}`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -148,6 +149,10 @@ const sanitizeCommand = (command: string) => {
|
||||
export const createCommand = (compose: ComposeNested) => {
|
||||
const { composeType, appName, sourceType } = compose;
|
||||
|
||||
if (compose.command) {
|
||||
return `${sanitizeCommand(compose.command)}`;
|
||||
}
|
||||
|
||||
const path =
|
||||
sourceType === "raw"
|
||||
? composeType === "stack"
|
||||
@@ -161,7 +166,6 @@ export const createCommand = (compose: ComposeNested) => {
|
||||
composeType === "docker-compose"
|
||||
? `compose -p ${appName} -f ${path} up -d --build --remove-orphans`
|
||||
: `stack deploy -c ${path} ${appName} --prune`;
|
||||
|
||||
const customCommand = sanitizeCommand(compose.command);
|
||||
return customCommand ? `${baseCommand} ${customCommand}` : baseCommand;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,9 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const zip = new AdmZip(buffer);
|
||||
const zipEntries = zip.getEntries();
|
||||
const zipEntries = zip
|
||||
.getEntries()
|
||||
.filter((entry) => !entry.entryName.startsWith("__MACOSX"));
|
||||
|
||||
const rootEntries = zipEntries.filter(
|
||||
(entry) =>
|
||||
@@ -59,14 +61,22 @@ export const unzipDrop = async (zipFile: File, application: Application) => {
|
||||
|
||||
if (!filePath) continue;
|
||||
|
||||
const fullPath = path.join(outputPath, filePath);
|
||||
const fullPath = path.join(outputPath, filePath).replace(/\\/g, "/");
|
||||
|
||||
if (application.serverId) {
|
||||
if (entry.isDirectory) {
|
||||
await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`);
|
||||
} else {
|
||||
if (!entry.isDirectory) {
|
||||
if (sftp === null) throw new Error("No SFTP connection available");
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
try {
|
||||
const dirPath = path.dirname(fullPath);
|
||||
await execAsyncRemote(
|
||||
application.serverId,
|
||||
`mkdir -p "${dirPath}"`,
|
||||
);
|
||||
await uploadFileToServer(sftp, entry.getData(), fullPath);
|
||||
} catch (err) {
|
||||
console.error(`Error uploading file ${fullPath}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.isDirectory) {
|
||||
@@ -103,7 +113,6 @@ const getSFTPConnection = async (serverId: string): Promise<SFTPWrapper> => {
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -115,7 +124,10 @@ const uploadFileToServer = (
|
||||
): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
sftp.writeFile(remotePath, data, (err) => {
|
||||
if (err) return reject(err);
|
||||
if (err) {
|
||||
console.error(`SFTP write error for ${remotePath}:`, err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import { buildHeroku, getHerokuCommand } from "./heroku";
|
||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||
import { buildStatic, getStaticCommand } from "./static";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@@ -211,21 +210,21 @@ const getImageName = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
if (registry) {
|
||||
return join(registry.imagePrefix || "", appName);
|
||||
return join(registry.registryUrl, registry.imagePrefix || "", appName);
|
||||
}
|
||||
|
||||
return `${appName}:latest`;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType } = application;
|
||||
const { registry, username, password, sourceType, registryUrl } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
if (username && password) {
|
||||
return {
|
||||
password,
|
||||
username,
|
||||
serveraddress: "https://index.docker.io/v1/",
|
||||
serveraddress: registryUrl || "",
|
||||
};
|
||||
}
|
||||
} else if (registry) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import path, { join } from "node:path";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
@@ -13,27 +13,32 @@ export const uploadImage = async (
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix, registryType } = registry;
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
|
||||
try {
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`,
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`,
|
||||
);
|
||||
await spawnAsync(
|
||||
const loginCommand = spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "-p", registry.password],
|
||||
["login", finalURL, "-u", registry.username, "--password-stdin"],
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
loginCommand.child?.stdin?.write(registry.password);
|
||||
loginCommand.child?.stdin?.end();
|
||||
await loginCommand;
|
||||
|
||||
await spawnAsync("docker", ["tag", imageName, registryTag], (data) => {
|
||||
if (writeStream.writable) {
|
||||
@@ -68,22 +73,23 @@ export const uploadImageRemoteCommand = (
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
|
||||
try {
|
||||
const command = `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath};
|
||||
docker login ${finalURL} -u ${registry.username} -p ${registry.password} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ DockerHub Failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ DockerHub Login Success" >> ${logPath};
|
||||
echo "✅ Registry Login Success" >> ${logPath};
|
||||
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Error tagging image" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
docker push ${registryTag} 2>> ${logPath} || {
|
||||
echo "❌ Error pushing image" >> ${logPath};
|
||||
exit 1;
|
||||
@@ -92,7 +98,6 @@ export const uploadImageRemoteCommand = (
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,17 +28,66 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
databasePassword,
|
||||
command,
|
||||
mounts,
|
||||
replicaSets,
|
||||
} = mongo;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${
|
||||
const startupScript = `
|
||||
#!/bin/bash
|
||||
${
|
||||
replicaSets
|
||||
? `
|
||||
mongod --port 27017 --replSet rs0 --bind_ip_all &
|
||||
MONGOD_PID=$!
|
||||
|
||||
# Wait for MongoDB to be ready
|
||||
while ! mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Check if replica set is already initialized
|
||||
REPLICA_STATUS=$(mongosh --quiet --eval "rs.status().ok || 0")
|
||||
|
||||
if [ "$REPLICA_STATUS" != "1" ]; then
|
||||
echo "Initializing replica set..."
|
||||
mongosh --eval '
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "localhost:27017", priority: 1 }]
|
||||
});
|
||||
|
||||
// Wait for the replica set to initialize
|
||||
while (!rs.isMaster().ismaster) {
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
// Create root user after replica set is initialized and we are primary
|
||||
db.getSiblingDB("admin").createUser({
|
||||
user: "${databaseUser}",
|
||||
pwd: "${databasePassword}",
|
||||
roles: ["root"]
|
||||
});
|
||||
'
|
||||
|
||||
else
|
||||
echo "Replica set already initialized."
|
||||
fi
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${command ?? "wait $MONGOD_PID"}`;
|
||||
|
||||
const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${replicaSets ? "\nMONGO_INITDB_DATABASE=admin" : ""}${
|
||||
env ? `\n${env}` : ""
|
||||
}`;
|
||||
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMongoEnv,
|
||||
mongo.project.env,
|
||||
@@ -56,12 +105,17 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
Image: dockerImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(command
|
||||
...(replicaSets
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", command],
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", startupScript],
|
||||
}
|
||||
: {}),
|
||||
: {
|
||||
...(command && {
|
||||
Command: ["/bin/bash"],
|
||||
Args: ["-c", command],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
Networks: [{ Target: "dokploy-network" }],
|
||||
Resources: {
|
||||
@@ -90,6 +144,7 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
: [],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
@@ -144,10 +144,11 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
|
||||
};
|
||||
export const cleanUpUnusedImages = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker image prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker image prune --all --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker image prune --all --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -157,10 +158,11 @@ export const cleanUpUnusedImages = async (serverId?: string) => {
|
||||
|
||||
export const cleanStoppedContainers = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker container prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker container prune --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker container prune --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -170,10 +172,11 @@ export const cleanStoppedContainers = async (serverId?: string) => {
|
||||
|
||||
export const cleanUpUnusedVolumes = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker volume prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker volume prune --all --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker volume prune --all --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -199,21 +202,20 @@ export const cleanUpInactiveContainers = async () => {
|
||||
};
|
||||
|
||||
export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||
const command = "docker builder prune --all --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, "docker builder prune --all --force");
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker builder prune --all --force");
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||
const command = "docker system prune --all --force --volumes";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
"docker system prune --all --force --volumes",
|
||||
);
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync("docker system prune --all --force --volumes");
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -238,9 +240,11 @@ export const startServiceRemote = async (serverId: string, appName: string) => {
|
||||
export const removeService = async (
|
||||
appName: string,
|
||||
serverId?: string | null,
|
||||
deleteVolumes = false,
|
||||
) => {
|
||||
try {
|
||||
const command = `docker service rm ${appName}`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
@@ -304,10 +308,10 @@ export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => {
|
||||
};
|
||||
|
||||
type Resources = {
|
||||
memoryLimit: number | null;
|
||||
memoryReservation: number | null;
|
||||
cpuLimit: number | null;
|
||||
cpuReservation: number | null;
|
||||
memoryLimit: string | null;
|
||||
memoryReservation: string | null;
|
||||
cpuLimit: string | null;
|
||||
cpuReservation: string | null;
|
||||
};
|
||||
export const calculateResources = ({
|
||||
memoryLimit,
|
||||
@@ -317,16 +321,14 @@ export const calculateResources = ({
|
||||
}: Resources): ResourceRequirements => {
|
||||
return {
|
||||
Limits: {
|
||||
MemoryBytes: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
|
||||
NanoCPUs: memoryLimit ? (cpuLimit || 1) * 1000 * 1000 * 1000 : undefined,
|
||||
MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined,
|
||||
NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined,
|
||||
},
|
||||
Reservations: {
|
||||
MemoryBytes: memoryLimit
|
||||
? (memoryReservation || 1) * 1024 * 1024
|
||||
: undefined,
|
||||
NanoCPUs: memoryLimit
|
||||
? (cpuReservation || 1) * 1000 * 1000 * 1000
|
||||
MemoryBytes: memoryReservation
|
||||
? Number.parseInt(memoryReservation)
|
||||
: undefined,
|
||||
NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const removeFileOrDirectory = async (path: string) => {
|
||||
try {
|
||||
await execAsync(`rm -rf ${path}`);
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${path}: ${error}`);
|
||||
console.error(`Error removing ${path}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -58,7 +58,7 @@ export const removeDirectoryCode = async (
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
console.error(`Error removing ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -77,7 +77,7 @@ export const removeComposeDirectory = async (
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
console.error(`Error removing ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -96,7 +96,7 @@ export const removeMonitoringDirectory = async (
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error to remove ${directoryPath}: ${error}`);
|
||||
console.error(`Error removing ${directoryPath}: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,7 +35,6 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||
...cudaInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in checkGPUStatus:", error);
|
||||
return {
|
||||
driverInstalled: false,
|
||||
driverVersion: undefined,
|
||||
@@ -303,7 +302,7 @@ const setupLocalServer = async (daemonConfig: any) => {
|
||||
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||
|
||||
const setupCommands = [
|
||||
`pkexec sh -c '
|
||||
`sudo sh -c '
|
||||
cp ${configFile} /etc/docker/daemon.json &&
|
||||
mkdir -p /etc/nvidia-container-runtime &&
|
||||
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
||||
@@ -314,7 +313,13 @@ const setupLocalServer = async (daemonConfig: any) => {
|
||||
`rm ${configFile}`,
|
||||
].join(" && ");
|
||||
|
||||
await execAsync(setupCommands);
|
||||
try {
|
||||
await execAsync(setupCommands);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
||||
@@ -337,11 +342,10 @@ const verifySetup = async (nodeId: string, serverId?: string) => {
|
||||
"cat /etc/nvidia-container-runtime/config.toml",
|
||||
].join(" && ");
|
||||
|
||||
const { stdout: diagnostics } = serverId
|
||||
? await execAsyncRemote(serverId, diagnosticCommands)
|
||||
: await execAsync(diagnosticCommands);
|
||||
await (serverId
|
||||
? execAsyncRemote(serverId, diagnosticCommands)
|
||||
: execAsync(diagnosticCommands));
|
||||
|
||||
console.error("Diagnostic Information:", diagnostics);
|
||||
throw new Error("GPU support not detected in swarm after setup");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -28,6 +30,7 @@ export const sendBuildErrorNotifications = async ({
|
||||
adminId,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.appBuildError, true),
|
||||
@@ -38,11 +41,12 @@ export const sendBuildErrorNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack } = notification;
|
||||
const { email, discord, telegram, slack, gotify } = notification;
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
BuildFailedEmail({
|
||||
@@ -58,46 +62,49 @@ export const sendBuildErrorNotifications = async ({
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `⚠️` - Build Failed",
|
||||
title: decorate(">", "`⚠️` Build Failed"),
|
||||
color: 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "`🛠️`・Project",
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚙️`・Application",
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❔`・Type",
|
||||
name: decorate("`❔`", "Type"),
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Failed",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚠️`・Error Message",
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
@@ -108,22 +115,35 @@ export const sendBuildErrorNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("⚠️", "Build Failed"),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("❔", `Type: ${applicationType}`)}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${decorate("⚠️", `Error:\n${errorMessage}`)}` +
|
||||
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const inlineButton = [
|
||||
[
|
||||
{
|
||||
text: "Deployment Logs",
|
||||
url: buildLink,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>⚠️ Build Failed</b>
|
||||
|
||||
<b>Project:</b> ${projectName}
|
||||
<b>Application:</b> ${applicationName}
|
||||
<b>Type:</b> ${applicationType}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
|
||||
<b>Error:</b>
|
||||
<pre>${errorMessage}</pre>
|
||||
|
||||
<b>Build Details:</b> ${buildLink}
|
||||
`,
|
||||
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
|
||||
inlineButton,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -16,6 +19,7 @@ interface Props {
|
||||
applicationType: string;
|
||||
buildLink: string;
|
||||
adminId: string;
|
||||
domains: Domain[];
|
||||
}
|
||||
|
||||
export const sendBuildSuccessNotifications = async ({
|
||||
@@ -24,8 +28,10 @@ export const sendBuildSuccessNotifications = async ({
|
||||
applicationType,
|
||||
buildLink,
|
||||
adminId,
|
||||
domains,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.appDeploy, true),
|
||||
@@ -36,11 +42,12 @@ export const sendBuildSuccessNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack } = notification;
|
||||
const { email, discord, telegram, slack, gotify } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -56,42 +63,45 @@ export const sendBuildSuccessNotifications = async ({
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `✅` - Build Success",
|
||||
title: decorate(">", "`✅` Build Success"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "`🛠️`・Project",
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚙️`・Application",
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❔`・Application Type",
|
||||
name: decorate("`❔`", "Type"),
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
name: decorate("`🧷`", "Build Link"),
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
@@ -102,19 +112,45 @@ export const sendBuildSuccessNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Build Success"),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("❔", `Type: ${applicationType}`)}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
|
||||
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
|
||||
array.slice(i * chunkSize, i * chunkSize + chunkSize),
|
||||
);
|
||||
|
||||
const inlineButton = [
|
||||
[
|
||||
{
|
||||
text: "Deployment Logs",
|
||||
url: buildLink,
|
||||
},
|
||||
],
|
||||
...chunkArray(domains, 2).map((chunk) =>
|
||||
chunk.map((data) => ({
|
||||
text: data.host,
|
||||
url: `${data.https ? "https" : "http"}://${data.host}`,
|
||||
})),
|
||||
),
|
||||
];
|
||||
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>✅ Build Success</b>
|
||||
|
||||
<b>Project:</b> ${projectName}
|
||||
<b>Application:</b> ${applicationName}
|
||||
<b>Type:</b> ${applicationType}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
|
||||
<b>Build Details:</b> ${buildLink}
|
||||
`,
|
||||
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
inlineButton,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { error } from "node:console";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -26,6 +29,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
errorMessage?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.databaseBackup, true),
|
||||
@@ -36,11 +40,12 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack } = notification;
|
||||
const { email, discord, telegram, slack, gotify } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -61,40 +66,43 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? "> `✅` - Database Backup Successful"
|
||||
: "> `❌` - Database Backup Failed",
|
||||
? decorate(">", "`✅` Database Backup Successful")
|
||||
: decorate(">", "`❌` Database Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "`🛠️`・Project",
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚙️`・Application",
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❔`・Database",
|
||||
name: decorate("`❔`", "Database"),
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
@@ -103,7 +111,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: "`⚠️`・Error Message",
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
@@ -116,19 +124,35 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("❔", `Type: ${databaseType}`)}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const messageText = `
|
||||
<b>${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"}</b>
|
||||
|
||||
<b>Project:</b> ${projectName}
|
||||
<b>Application:</b> ${applicationName}
|
||||
<b>Type:</b> ${databaseType}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
|
||||
<b>Status:</b> ${type === "success" ? "Successful" : "Failed"}
|
||||
${type === "error" && errorMessage ? `<b>Error:</b> ${errorMessage}` : ""}
|
||||
`;
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
@@ -15,6 +17,7 @@ export const sendDockerCleanupNotifications = async (
|
||||
message = "Docker cleanup for dokploy",
|
||||
) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.dockerCleanup, true),
|
||||
@@ -25,11 +28,12 @@ export const sendDockerCleanupNotifications = async (
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack } = notification;
|
||||
const { email, discord, telegram, slack, gotify } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -44,27 +48,30 @@ export const sendDockerCleanupNotifications = async (
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `✅` - Docker Cleanup",
|
||||
title: decorate(">", "`✅` Docker Cleanup"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📜`・Message",
|
||||
name: decorate("`📜`", "Message"),
|
||||
value: `\`\`\`${message}\`\`\``,
|
||||
},
|
||||
],
|
||||
@@ -75,14 +82,21 @@ export const sendDockerCleanupNotifications = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Docker Cleanup"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${decorate("📜", `Message:\n${message}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>✅ Docker Cleanup</b>
|
||||
<b>Message:</b> ${message}
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`,
|
||||
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,19 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
export const sendDokployRestartNotifications = async () => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.dokployRestart, true),
|
||||
with: {
|
||||
@@ -19,11 +22,12 @@ export const sendDokployRestartNotifications = async () => {
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
gotify: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack } = notification;
|
||||
const { email, discord, telegram, slack, gotify } = notification;
|
||||
|
||||
if (email) {
|
||||
const template = await renderAsync(
|
||||
@@ -33,22 +37,25 @@ export const sendDokployRestartNotifications = async () => {
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "> `✅` - Dokploy Server Restarted",
|
||||
title: decorate(">", "`✅` Dokploy Server Restarted"),
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
@@ -60,13 +67,20 @@ export const sendDokployRestartNotifications = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate("✅", "Dokploy Server Restarted"),
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
await sendTelegramNotification(
|
||||
telegram,
|
||||
`
|
||||
<b>✅ Dokploy Serverd Restarted</b>
|
||||
<b>Time:</b> ${date.toLocaleString()}
|
||||
`,
|
||||
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
discord,
|
||||
email,
|
||||
gotify,
|
||||
slack,
|
||||
telegram,
|
||||
} from "@dokploy/server/db/schema";
|
||||
@@ -41,20 +42,24 @@ export const sendDiscordNotification = async (
|
||||
connection: typeof discord.$inferInsert,
|
||||
embed: any,
|
||||
) => {
|
||||
try {
|
||||
await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ embeds: [embed] }),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
// try {
|
||||
await fetch(connection.webhookUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ embeds: [embed] }),
|
||||
});
|
||||
// } catch (err) {
|
||||
// console.log(err);
|
||||
// }
|
||||
};
|
||||
|
||||
export const sendTelegramNotification = async (
|
||||
connection: typeof telegram.$inferInsert,
|
||||
messageText: string,
|
||||
inlineButton?: {
|
||||
text: string;
|
||||
url: string;
|
||||
}[][],
|
||||
) => {
|
||||
try {
|
||||
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;
|
||||
@@ -66,6 +71,9 @@ export const sendTelegramNotification = async (
|
||||
text: messageText,
|
||||
parse_mode: "HTML",
|
||||
disable_web_page_preview: true,
|
||||
reply_markup: {
|
||||
inline_keyboard: inlineButton,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -87,3 +95,33 @@ export const sendSlackNotification = async (
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendGotifyNotification = async (
|
||||
connection: typeof gotify.$inferInsert,
|
||||
title: string,
|
||||
message: string,
|
||||
) => {
|
||||
const response = await fetch(`${connection.serverUrl}/message`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Gotify-Key": connection.appToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: title,
|
||||
message: message,
|
||||
priority: connection.priority,
|
||||
extras: {
|
||||
"client::display": {
|
||||
contentType: "text/plain",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to send Gotify notification: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ export const execAsync = util.promisify(exec);
|
||||
export const execAsyncRemote = async (
|
||||
serverId: string | null,
|
||||
command: string,
|
||||
onData?: (data: string) => void,
|
||||
): Promise<{ stdout: string; stderr: string }> => {
|
||||
if (!serverId) return { stdout: "", stderr: "" };
|
||||
const server = await findServerById(serverId);
|
||||
@@ -21,7 +22,10 @@ export const execAsyncRemote = async (
|
||||
conn
|
||||
.once("ready", () => {
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
onData?.(err.message);
|
||||
throw err;
|
||||
}
|
||||
stream
|
||||
.on("close", (code: number, signal: string) => {
|
||||
conn.end();
|
||||
@@ -37,21 +41,27 @@ export const execAsyncRemote = async (
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
stdout += data.toString();
|
||||
onData?.(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
onData?.(data.toString());
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
conn.end();
|
||||
if (err.level === "client-authentication") {
|
||||
onData?.(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
onData?.(`SSH connection error: ${err.message}`);
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
}
|
||||
})
|
||||
|
||||
@@ -53,7 +53,7 @@ export const buildRemoteDocker = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { sourceType, dockerImage, username, password } = application;
|
||||
const { registryUrl, dockerImage, username, password } = application;
|
||||
|
||||
try {
|
||||
if (!dockerImage) {
|
||||
@@ -65,7 +65,7 @@ echo "Pulling ${dockerImage}" >> ${logPath};
|
||||
|
||||
if (username && password) {
|
||||
command += `
|
||||
if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then
|
||||
if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then
|
||||
echo "❌ Login failed" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
@@ -69,6 +69,7 @@ export const cloneGitRepository = async (
|
||||
});
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
@@ -91,7 +92,7 @@ export const cloneGitRepository = async (
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -168,7 +169,8 @@ export const getCustomGitCloneCommand = async (
|
||||
);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
@@ -304,6 +306,7 @@ export const cloneGitRawRepository = async (entity: {
|
||||
});
|
||||
}
|
||||
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
await spawnAsync(
|
||||
"git",
|
||||
[
|
||||
@@ -322,7 +325,7 @@ export const cloneGitRawRepository = async (entity: {
|
||||
env: {
|
||||
...process.env,
|
||||
...(customGitSSHKeyId && {
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -381,7 +384,8 @@ export const cloneRawGitRepositoryRemote = async (compose: Compose) => {
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
if (customGitSSHKeyId) {
|
||||
const sshKey = await findSSHKeyById(customGitSSHKeyId);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
const { port } = sanitizeRepoPathSSH(customGitUrl);
|
||||
const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`;
|
||||
command.push(
|
||||
`
|
||||
echo "${sshKey.privateKey}" > /tmp/id_rsa
|
||||
|
||||
@@ -26,7 +26,7 @@ export const refreshGitlabToken = async (gitlabProviderId: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("https://gitlab.com/oauth/token", {
|
||||
const response = await fetch(`${gitlabProvider.gitlabUrl}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
@@ -122,7 +122,7 @@ export const cloneGitlabRepository = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const repoclone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
@@ -218,7 +218,7 @@ export const getGitlabCloneCommand = async (
|
||||
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const repoclone = `${gitlab?.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
@@ -244,7 +244,7 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
@@ -304,7 +304,7 @@ export const getGitlabBranches = async (input: {
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
|
||||
const branchesResponse = await fetch(
|
||||
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches`,
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
@@ -350,7 +350,9 @@ export const cloneRawGitlabRepository = async (entity: Compose) => {
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const gitlabUrl = gitlabProvider.gitlabUrl;
|
||||
// What happen with oauth in self hosted instances?
|
||||
const repoclone = `${gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
@@ -390,7 +392,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
await refreshGitlabToken(gitlabId);
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `gitlab.com/${gitlabPathNamespace}.git`;
|
||||
const repoclone = `${gitlabProvider.gitlabUrl.replace(/^https?:\/\//, "")}/${gitlabPathNamespace}.git`;
|
||||
const cloneUrl = `https://oauth2:${gitlabProvider?.accessToken}@${repoclone}`;
|
||||
try {
|
||||
const command = `
|
||||
@@ -417,7 +419,7 @@ export const testGitlabConnection = async (
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`https://gitlab.com/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import type http from "node:http";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { validateWebSocketRequest } from "../auth/auth";
|
||||
import { getShell } from "./utils";
|
||||
|
||||
export const setupDockerContainerLogsWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
const wssTerm = new WebSocketServer({
|
||||
noServer: true,
|
||||
path: "/docker-container-logs",
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
|
||||
if (pathname === "/_next/webpack-hmr") {
|
||||
return;
|
||||
}
|
||||
if (pathname === "/docker-container-logs") {
|
||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||
wssTerm.emit("connection", ws, req);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
const containerId = url.searchParams.get("containerId");
|
||||
const tail = url.searchParams.get("tail");
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
|
||||
if (!containerId) {
|
||||
ws.close(4000, "containerId no provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (!server.sshKeyId) return;
|
||||
const client = new Client();
|
||||
new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => {
|
||||
const command = `
|
||||
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
||||
`;
|
||||
client.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("Execution error:", err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
resolve();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
ws.send(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
});
|
||||
});
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const shell = getShell();
|
||||
const ptyProcess = spawn(
|
||||
shell,
|
||||
[
|
||||
"-c",
|
||||
`docker container logs --tail ${tail} --follow ${containerId}`,
|
||||
],
|
||||
{
|
||||
name: "xterm-256color",
|
||||
cwd: process.env.HOME,
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
},
|
||||
);
|
||||
|
||||
ptyProcess.onData((data) => {
|
||||
ws.send(data);
|
||||
});
|
||||
ws.on("close", () => {
|
||||
ptyProcess.kill();
|
||||
});
|
||||
ws.on("message", (message) => {
|
||||
try {
|
||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||
if (Buffer.isBuffer(message)) {
|
||||
command = message.toString("utf8");
|
||||
} else {
|
||||
command = message;
|
||||
}
|
||||
ptyProcess.write(command.toString());
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
|
||||
ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
import type http from "node:http";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { validateWebSocketRequest } from "../auth/auth";
|
||||
import { getShell } from "./utils";
|
||||
|
||||
export const setupDockerContainerTerminalWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
const wssTerm = new WebSocketServer({
|
||||
noServer: true,
|
||||
path: "/docker-container-terminal",
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
|
||||
if (pathname === "/_next/webpack-hmr") {
|
||||
return;
|
||||
}
|
||||
if (pathname === "/docker-container-terminal") {
|
||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||
wssTerm.emit("connection", ws, req);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
const containerId = url.searchParams.get("containerId");
|
||||
const activeWay = url.searchParams.get("activeWay");
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
|
||||
if (!containerId) {
|
||||
ws.close(4000, "containerId no provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId)
|
||||
throw new Error("No SSH key available for this server");
|
||||
|
||||
const conn = new Client();
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
conn
|
||||
.once("ready", () => {
|
||||
conn.exec(
|
||||
`docker exec -it ${containerId} ${activeWay}`,
|
||||
{ pty: true },
|
||||
(err, stream) => {
|
||||
if (err) throw err;
|
||||
|
||||
stream
|
||||
.on("close", (code: number, signal: string) => {
|
||||
ws.send(`\nContainer closed with code: ${code}\n`);
|
||||
conn.end();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
stdout += data.toString();
|
||||
ws.send(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
ws.send(data.toString());
|
||||
console.error("Error: ", data.toString());
|
||||
});
|
||||
|
||||
ws.on("message", (message) => {
|
||||
try {
|
||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||
if (Buffer.isBuffer(message)) {
|
||||
command = message.toString("utf8");
|
||||
} else {
|
||||
command = message;
|
||||
}
|
||||
stream.write(command.toString());
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
stream.end();
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
} else {
|
||||
const shell = getShell();
|
||||
const ptyProcess = spawn(
|
||||
shell,
|
||||
["-c", `docker exec -it ${containerId} ${activeWay}`],
|
||||
{
|
||||
name: "xterm-256color",
|
||||
cwd: process.env.HOME,
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
},
|
||||
);
|
||||
|
||||
ptyProcess.onData((data) => {
|
||||
ws.send(data);
|
||||
});
|
||||
ws.on("close", () => {
|
||||
ptyProcess.kill();
|
||||
});
|
||||
ws.on("message", (message) => {
|
||||
try {
|
||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||
if (Buffer.isBuffer(message)) {
|
||||
command = message.toString("utf8");
|
||||
} else {
|
||||
command = message;
|
||||
}
|
||||
ptyProcess.write(command.toString());
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
|
||||
ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
import type http from "node:http";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { validateWebSocketRequest } from "../auth/auth";
|
||||
import { docker } from "../constants";
|
||||
import {
|
||||
getLastAdvancedStatsFile,
|
||||
recordAdvancedStats,
|
||||
} from "../monitoring/utilts";
|
||||
|
||||
export const setupDockerStatsMonitoringSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
const wssTerm = new WebSocketServer({
|
||||
noServer: true,
|
||||
path: "/listen-docker-stats-monitoring",
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
|
||||
if (pathname === "/_next/webpack-hmr") {
|
||||
return;
|
||||
}
|
||||
if (pathname === "/listen-docker-stats-monitoring") {
|
||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||
wssTerm.emit("connection", ws, req);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
const appName = url.searchParams.get("appName");
|
||||
const appType = (url.searchParams.get("appType") || "application") as
|
||||
| "application"
|
||||
| "stack"
|
||||
| "docker-compose";
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
|
||||
if (!appName) {
|
||||
ws.close(4000, "appName no provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const intervalId = setInterval(async () => {
|
||||
try {
|
||||
const filter = {
|
||||
status: ["running"],
|
||||
...(appType === "application" && {
|
||||
label: [`com.docker.swarm.service.name=${appName}`],
|
||||
}),
|
||||
...(appType === "stack" && {
|
||||
label: [`com.docker.swarm.task.name=${appName}`],
|
||||
}),
|
||||
...(appType === "docker-compose" && {
|
||||
name: [appName],
|
||||
}),
|
||||
};
|
||||
|
||||
const containers = await docker.listContainers({
|
||||
filters: JSON.stringify(filter),
|
||||
});
|
||||
|
||||
const container = containers[0];
|
||||
if (!container || container?.State !== "running") {
|
||||
ws.close(4000, "Container not running");
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await docker.getContainer(container.Id).stats({
|
||||
stream: false,
|
||||
});
|
||||
|
||||
await recordAdvancedStats(stats, appName);
|
||||
const data = await getLastAdvancedStatsFile(appName);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
data,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
ws.close(4000, `Error: ${error.message}`);
|
||||
}
|
||||
}, 1300);
|
||||
|
||||
ws.on("close", () => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import type http from "node:http";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { validateWebSocketRequest } from "../auth/auth";
|
||||
|
||||
export const setupDeploymentLogsWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
const wssTerm = new WebSocketServer({
|
||||
noServer: true,
|
||||
path: "/listen-deployment",
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
|
||||
if (pathname === "/_next/webpack-hmr") {
|
||||
return;
|
||||
}
|
||||
if (pathname === "/listen-deployment") {
|
||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||
wssTerm.emit("connection", ws, req);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
const logPath = url.searchParams.get("logPath");
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
|
||||
if (!logPath) {
|
||||
ws.close(4000, "logPath no provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (!server.sshKeyId) return;
|
||||
const client = new Client();
|
||||
new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.on("ready", () => {
|
||||
const command = `
|
||||
tail -n +1 -f ${logPath};
|
||||
`;
|
||||
client.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("Execution error:", err);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
resolve();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
ws.send(data.toString());
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
});
|
||||
});
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||
|
||||
tail.stdout.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
});
|
||||
|
||||
tail.stderr.on("data", (data) => {
|
||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
// const errorMessage = error?.message as unknown as string;
|
||||
ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
import type http from "node:http";
|
||||
import path from "node:path";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { validateWebSocketRequest } from "../auth/auth";
|
||||
import { paths } from "../constants";
|
||||
|
||||
export const getPublicIpWithFallback = async () => {
|
||||
// @ts-ignore
|
||||
let ip = null;
|
||||
try {
|
||||
ip = await publicIpv4();
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Error to obtain public IPv4 address, falling back to IPv6",
|
||||
// @ts-ignore
|
||||
error.message,
|
||||
);
|
||||
try {
|
||||
ip = await publicIpv6();
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
console.error("Error to obtain public IPv6 address", error.message);
|
||||
ip = null;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
};
|
||||
|
||||
export const setupTerminalWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
const wssTerm = new WebSocketServer({
|
||||
noServer: true,
|
||||
path: "/terminal",
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
if (pathname === "/_next/webpack-hmr") {
|
||||
return;
|
||||
}
|
||||
if (pathname === "/terminal") {
|
||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
||||
wssTerm.emit("connection", ws, req);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const { user, session } = await validateWebSocketRequest(req);
|
||||
if (!user || !session || !serverId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (!server) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const { SSH_PATH } = paths();
|
||||
const privateKey = path.join(SSH_PATH, `${server.sshKeyId}_rsa`);
|
||||
const sshCommand = [
|
||||
"ssh",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-i",
|
||||
privateKey,
|
||||
`${server.username}@${server.ipAddress}`,
|
||||
];
|
||||
const ptyProcess = spawn("ssh", sshCommand.slice(1), {
|
||||
name: "xterm-256color",
|
||||
cwd: process.env.HOME,
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
});
|
||||
|
||||
ptyProcess.onData((data) => {
|
||||
ws.send(data);
|
||||
});
|
||||
ws.on("message", (message) => {
|
||||
try {
|
||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||
if (Buffer.isBuffer(message)) {
|
||||
command = message.toString("utf8");
|
||||
} else {
|
||||
command = message;
|
||||
}
|
||||
ptyProcess.write(command.toString());
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
ptyProcess.kill();
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import os from "node:os";
|
||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||
|
||||
export const getShell = () => {
|
||||
switch (os.platform()) {
|
||||
@@ -10,3 +11,25 @@ export const getShell = () => {
|
||||
return "bash";
|
||||
}
|
||||
};
|
||||
|
||||
export const getPublicIpWithFallback = async () => {
|
||||
// @ts-ignore
|
||||
let ip = null;
|
||||
try {
|
||||
ip = await publicIpv4();
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Error obtaining public IPv4 address, falling back to IPv6",
|
||||
// @ts-ignore
|
||||
error.message,
|
||||
);
|
||||
try {
|
||||
ip = await publicIpv6();
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
console.error("Error obtaining public IPv6 address", error.message);
|
||||
ip = null;
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user