Merge branch 'canary' into pass-dokploy-preview-url-at-build-time

This commit is contained in:
Mauricio Siu
2025-10-25 13:15:25 -06:00
98 changed files with 36135 additions and 470 deletions
+6
View File
@@ -108,6 +108,12 @@ export const member = pgTable("member", {
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull()
.default(false),
canDeleteEnvironments: boolean("canDeleteEnvironments")
.notNull()
.default(false),
canCreateEnvironments: boolean("canCreateEnvironments")
.notNull()
.default(false),
accessedProjects: text("accesedProjects")
.array()
.notNull()
+8 -1
View File
@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm";
import {
bigint,
boolean,
integer,
json,
@@ -20,7 +21,6 @@ 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";
import { security } from "./security";
@@ -80,6 +80,7 @@ export const applications = pgTable("application", {
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
previewBuildSecrets: text("previewBuildSecrets"),
previewLabels: text("previewLabels").array(),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
@@ -99,6 +100,7 @@ export const applications = pgTable("application", {
).default(true),
rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"),
buildSecrets: text("buildSecrets"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
@@ -164,6 +166,7 @@ export const applications = pgTable("application", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
//
replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus")
@@ -252,6 +255,7 @@ const createSchema = createInsertSchema(applications, {
autoDeploy: z.boolean(),
env: z.string().optional(),
buildArgs: z.string().optional(),
buildSecrets: z.string().optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.string().optional(),
@@ -303,6 +307,7 @@ const createSchema = createInsertSchema(applications, {
previewPort: z.number().optional(),
previewEnv: z.string().optional(),
previewBuildArgs: z.string().optional(),
previewBuildSecrets: z.string().optional(),
previewWildcard: z.string().optional(),
previewLimit: z.number().optional(),
previewHttps: z.boolean().optional(),
@@ -312,6 +317,7 @@ const createSchema = createInsertSchema(applications, {
watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateApplication = createSchema.pick({
@@ -456,6 +462,7 @@ export const apiSaveEnvironmentVariables = createSchema
applicationId: true,
env: true,
buildArgs: true,
buildSecrets: true,
})
.required();
+3 -1
View File
@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -62,6 +62,7 @@ export const mariadb = pgTable("mariadb", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -128,6 +129,7 @@ const createSchema = createInsertSchema(mariadb, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateMariaDB = createSchema
+10 -1
View File
@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm";
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -58,6 +65,7 @@ export const mongo = pgTable("mongo", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -118,6 +126,7 @@ const createSchema = createInsertSchema(mongo, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateMongo = createSchema
+3 -1
View File
@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -60,6 +60,7 @@ export const mysql = pgTable("mysql", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -125,6 +126,7 @@ const createSchema = createInsertSchema(mysql, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateMySql = createSchema
@@ -12,6 +12,7 @@ export const notificationType = pgEnum("notificationType", [
"email",
"gotify",
"ntfy",
"lark",
]);
export const notifications = pgTable("notification", {
@@ -48,6 +49,9 @@ export const notifications = pgTable("notification", {
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
larkId: text("larkId").references(() => lark.larkId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -116,6 +120,14 @@ export const ntfy = pgTable("ntfy", {
priority: integer("priority").notNull().default(3),
});
export const lark = pgTable("lark", {
larkId: text("larkId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
webhookUrl: text("webhookUrl").notNull(),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -141,6 +153,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
lark: one(lark, {
fields: [notifications.larkId],
references: [lark.larkId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
@@ -339,6 +355,31 @@ export const apiFindOneNotification = notificationsSchema
})
.required();
export const apiCreateLark = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
webhookUrl: z.string().min(1),
})
.required();
export const apiUpdateLark = apiCreateLark.partial().extend({
notificationId: z.string().min(1),
larkId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestLarkConnection = apiCreateLark.pick({
webhookUrl: true,
});
export const apiSendTest = notificationsSchema
.extend({
botToken: z.string(),
+3 -1
View File
@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -60,6 +60,7 @@ export const postgres = pgTable("postgres", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
@@ -118,6 +119,7 @@ const createSchema = createInsertSchema(postgres, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreatePostgres = createSchema
+3 -1
View File
@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -60,6 +60,7 @@ export const redis = pgTable("redis", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(),
environmentId: text("environmentId")
@@ -108,6 +109,7 @@ const createSchema = createInsertSchema(redis, {
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
});
export const apiCreateRedis = createSchema
+2
View File
@@ -186,6 +186,8 @@ export const apiAssignPermissions = createSchema
canAccessToAPI: z.boolean().optional(),
canAccessToSSHKeys: z.boolean().optional(),
canAccessToGitProviders: z.boolean().optional(),
canDeleteEnvironments: z.boolean().optional(),
canCreateEnvironments: z.boolean().optional(),
})
.required();
+10 -8
View File
@@ -473,6 +473,7 @@ export const deployPreviewApplication = async ({
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
if (application.sourceType === "github") {
await cloneGithubRepository({
@@ -490,7 +491,7 @@ export const deployPreviewApplication = async ({
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
body: "### Dokploy Preview Deployment\n\n$successComment",
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
@@ -500,7 +501,7 @@ export const deployPreviewApplication = async ({
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
body: "### Dokploy Preview Deployment\n\n$comment",
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
@@ -575,11 +576,12 @@ export const deployRemotePreviewApplication = async ({
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
body: "### Dokploy Preview Deployment\n\n$buildingComment",
});
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
if (application.serverId) {
let command = "set -e;";
@@ -605,7 +607,7 @@ export const deployRemotePreviewApplication = async ({
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
body: "### Dokploy Preview Deployment\n\n$successComment",
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
@@ -615,7 +617,7 @@ export const deployRemotePreviewApplication = async ({
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
body: "### Dokploy Preview Deployment\n\n$comment",
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
@@ -662,8 +664,8 @@ export const rebuildRemoteApplication = async ({
await execAsyncRemote(
application.serverId,
`
echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath};
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
echo "\n\n===================================EXTRA LOGS============================================" >> $deployment.logPath;
echo "Error occurred ❌, check the logs for details." >> $deployment.logPath;
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
);
@@ -678,7 +680,7 @@ export const rebuildRemoteApplication = async ({
export const getApplicationStats = async (appName: string) => {
const filter = {
status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`],
label: ["com.docker.swarm.service.name=$appName"],
};
const containers = await docker.listContainers({
@@ -2,18 +2,21 @@ import { db } from "@dokploy/server/db";
import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateLark,
type apiCreateGotify,
type apiCreateNtfy,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateLark,
type apiUpdateGotify,
type apiUpdateNtfy,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
lark,
gotify,
notifications,
ntfy,
@@ -585,6 +588,7 @@ export const findNotificationById = async (notificationId: string) => {
email: true,
gotify: true,
ntfy: true,
lark: true,
},
});
if (!notification) {
@@ -605,6 +609,94 @@ export const removeNotificationById = async (notificationId: string) => {
return result[0];
};
export const createLarkNotification = async (
input: typeof apiCreateLark._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newLark = await tx
.insert(lark)
.values({
webhookUrl: input.webhookUrl,
})
.returning()
.then((value) => value[0]);
if (!newLark) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting lark",
});
}
const newDestination = await tx
.insert(notifications)
.values({
larkId: newLark.larkId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "lark",
organizationId: organizationId,
serverThreshold: input.serverThreshold,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateLarkNotification = async (
input: typeof apiUpdateLark._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,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.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(lark)
.set({
webhookUrl: input.webhookUrl,
})
.where(eq(lark.larkId, input.larkId))
.returning()
.then((value) => value[0]);
return newDestination;
});
};
export const updateNotificationById = async (
notificationId: string,
notificationData: Partial<Notification>,
+94
View File
@@ -163,6 +163,24 @@ export const canPerformAccessEnvironment = async (
return false;
};
export const canPerformDeleteEnvironment = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const { accessedProjects, canDeleteEnvironments } = await findMemberById(
userId,
organizationId,
);
const haveAccessToProject = accessedProjects.includes(projectId);
if (canDeleteEnvironments && haveAccessToProject) {
return true;
}
return false;
};
export const canAccessToTraefikFiles = async (
userId: string,
organizationId: string,
@@ -240,6 +258,42 @@ export const checkEnvironmentAccess = async (
}
};
export const checkEnvironmentDeletionPermission = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const member = await findMemberById(userId, organizationId);
if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}
if (member.role === "owner" || member.role === "admin") {
return true;
}
if (!member.canDeleteEnvironments) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have permission to delete environments",
});
}
const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
return true;
};
export const checkProjectAccess = async (
authId: string,
action: "create" | "delete" | "access",
@@ -272,6 +326,46 @@ export const checkProjectAccess = async (
}
};
export const checkEnvironmentCreationPermission = async (
userId: string,
projectId: string,
organizationId: string,
) => {
// Get user's member record
const member = await findMemberById(userId, organizationId);
if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}
// Owners and admins can always create environments
if (member.role === "owner" || member.role === "admin") {
return true;
}
// Check if user has canCreateEnvironments permission
if (!member.canCreateEnvironments) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have permission to create environments",
});
}
// Check if user has access to the project
const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
return true;
};
export const findMemberById = async (
userId: string,
organizationId: string,
+2 -2
View File
@@ -141,8 +141,8 @@ export function processValue(
}
if (
typeof payload === "string" &&
payload.startsWith("{") &&
payload.endsWith("}")
payload.trimStart().startsWith("{") &&
payload.trimEnd().endsWith("}")
) {
try {
payload = JSON.parse(payload);
@@ -16,6 +16,7 @@ export function getProviderName(apiUrl: string) {
if (apiUrl.includes("api.mistral.ai")) return "mistral";
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
if (apiUrl.includes("generativelanguage.googleapis.com")) return "gemini";
return "custom";
}
@@ -66,6 +67,13 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
baseURL: config.apiUrl,
apiKey: config.apiKey,
});
case "gemini":
return createOpenAICompatible({
name: "gemini",
baseURL: config.apiUrl,
queryParams: { key: config.apiKey },
headers: {},
});
case "custom":
return createOpenAICompatible({
name: "custom",
@@ -1,5 +1,8 @@
import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
import {
getEnviromentVariablesObject,
prepareEnvironmentVariables,
} from "@dokploy/server/utils/docker/utils";
import {
getBuildAppDirectory,
getDockerContextPath,
@@ -17,6 +20,7 @@ export const buildCustomDocker = async (
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
cleanCache,
} = application;
@@ -26,11 +30,6 @@ export const buildCustomDocker = async (
const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath = getDockerContextPath(application);
@@ -44,9 +43,29 @@ export const buildCustomDocker = async (
commandArgs.push("--target", dockerBuildStage);
}
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to type=file.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -70,6 +89,10 @@ export const buildCustomDocker = async (
},
{
cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...secrets,
},
},
);
} catch (error) {
@@ -86,6 +109,7 @@ export const getDockerCommand = (
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
cleanCache,
} = application;
@@ -96,11 +120,6 @@ export const getDockerCommand = (
const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath =
getDockerContextPath(application) || defaultContextPath;
@@ -115,10 +134,33 @@ export const getDockerCommand = (
commandArgs.push("--no-cache");
}
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) {
commandArgs.push("--build-arg", `'${arg}'`);
}
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
const joinedSecrets = Object.entries(secrets)
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
.join(" ");
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to `type=file`.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -140,7 +182,7 @@ cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
exit 1;
}
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
echo "❌ Docker build failed" >> ${logPath};
exit 1;
}
@@ -142,6 +142,7 @@ export const mechanizeDockerContainer = async (
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
} = generateConfigContainer(application);
const bindsMount = generateBindMounts(mounts);
@@ -191,6 +192,8 @@ export const mechanizeDockerContainer = async (
})),
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
@@ -45,6 +45,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
} = generateConfigContainer(mariadb);
const resources = calculateResources({
memoryLimit,
@@ -102,6 +103,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
const service = docker.getService(appName);
@@ -91,6 +91,7 @@ ${command ?? "wait $MONGOD_PID"}`;
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
} = generateConfigContainer(mongo);
const resources = calculateResources({
@@ -155,6 +156,8 @@ ${command ?? "wait $MONGOD_PID"}`;
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
@@ -51,6 +51,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
} = generateConfigContainer(mysql);
const resources = calculateResources({
memoryLimit,
@@ -108,6 +109,8 @@ export const buildMysql = async (mysql: MysqlNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
const service = docker.getService(appName);
@@ -44,6 +44,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
} = generateConfigContainer(postgres);
const resources = calculateResources({
memoryLimit,
@@ -101,6 +102,8 @@ export const buildPostgres = async (postgres: PostgresNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
const service = docker.getService(appName);
@@ -42,6 +42,7 @@ export const buildRedis = async (redis: RedisNested) => {
RollbackConfig,
UpdateConfig,
Networks,
StopGracePeriod,
} = generateConfigContainer(redis);
const resources = calculateResources({
memoryLimit,
@@ -98,6 +99,8 @@ export const buildRedis = async (redis: RedisNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
+10
View File
@@ -394,8 +394,14 @@ export const generateConfigContainer = (
replicas,
mounts,
networkSwarm,
stopGracePeriodSwarm,
} = application;
const sanitizedStopGracePeriodSwarm =
typeof stopGracePeriodSwarm === "bigint"
? Number(stopGracePeriodSwarm)
: stopGracePeriodSwarm;
const haveMounts = mounts && mounts.length > 0;
return {
@@ -444,6 +450,10 @@ export const generateConfigContainer = (
Order: "start-first",
},
}),
...(sanitizedStopGracePeriodSwarm !== null &&
sanitizedStopGracePeriodSwarm !== undefined && {
StopGracePeriod: sanitizedStopGracePeriodSwarm,
}),
...(networkSwarm
? {
Networks: networkSwarm,
@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -44,11 +45,13 @@ export const sendBuildErrorNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
@@ -211,5 +214,117 @@ export const sendBuildErrorNotifications = async ({
],
});
}
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "⚠️ Build Failed",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
type: "danger",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
});
}
}
};
@@ -8,6 +8,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -45,11 +46,13 @@ export const sendBuildSuccessNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -210,5 +213,109 @@ export const sendBuildSuccessNotifications = async ({
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Build Success",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Type:**\n${applicationType}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
type: "primary",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
});
}
}
};
@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -44,11 +45,13 @@ export const sendDatabaseBackupNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -238,5 +241,120 @@ export const sendDatabaseBackupNotifications = async ({
],
});
}
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage =
errorMessage && errorMessage.length > limitCharacter
? errorMessage.substring(0, limitCharacter)
: errorMessage;
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content:
type === "success"
? "✅ Database Backup Successful"
: "❌ Database Backup Failed",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: type === "success" ? "green" : "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Project:**\n${projectName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Type:**\n${databaseType}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Application:**\n${applicationName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Database Name:**\n${databaseName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
...(type === "error" && truncatedErrorMessage
? [
{
tag: "markdown",
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
text_align: "left",
text_size: "normal_v2",
},
]
: []),
],
},
},
});
}
}
};
@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -31,11 +32,13 @@ export const sendDockerCleanupNotifications = async (
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -135,5 +138,83 @@ export const sendDockerCleanupNotifications = async (
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Docker Cleanup",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Cleanup Details:**\n${message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Date:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
}
}
};
@@ -7,6 +7,7 @@ import { eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
@@ -25,11 +26,13 @@ export const sendDokployRestartNotifications = async () => {
slack: true,
gotify: true,
ntfy: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
const { email, discord, telegram, slack, gotify, ntfy, lark } =
notification;
if (email) {
const template = await renderAsync(
@@ -135,5 +138,81 @@ export const sendDokployRestartNotifications = async () => {
console.log(error);
}
}
if (lark) {
try {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: "✅ Dokploy Server Restarted",
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "green",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Restart Time:**\n${format(date, "PP pp")}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
} catch (error) {
console.log(error);
}
}
}
};
@@ -3,6 +3,7 @@ import { db } from "../../db";
import { notifications } from "../../db/schema";
import {
sendDiscordNotification,
sendLarkNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -34,6 +35,7 @@ export const sendServerThresholdNotifications = async (
discord: true,
telegram: true,
slack: true,
lark: true,
},
});
@@ -41,7 +43,7 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) {
const { discord, telegram, slack } = notification;
const { discord, telegram, slack, lark } = notification;
if (discord) {
const decorate = (decoration: string, text: string) =>
@@ -151,5 +153,101 @@ export const sendServerThresholdNotifications = async (
],
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
schema: "2.0",
config: {
update_multi: true,
style: {
text_size: {
normal_v2: {
default: "normal",
pc: "normal",
mobile: "heading",
},
},
},
},
header: {
title: {
tag: "plain_text",
content: `⚠️ Server ${payload.Type} Alert`,
},
subtitle: {
tag: "plain_text",
content: "",
},
template: "red",
padding: "12px 12px 12px 12px",
},
body: {
direction: "vertical",
padding: "12px 12px 12px 12px",
elements: [
{
tag: "column_set",
columns: [
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Server Name:**\n${payload.ServerName}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Message:**\n${payload.Message}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
{
tag: "column",
width: "weighted",
elements: [
{
tag: "markdown",
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
text_align: "left",
text_size: "normal_v2",
},
{
tag: "markdown",
content: `**Alert Time:**\n${date.toLocaleString()}`,
text_align: "left",
text_size: "normal_v2",
},
],
vertical_align: "top",
weight: 1,
},
],
},
],
},
},
});
}
}
};
@@ -1,6 +1,7 @@
import type {
discord,
email,
lark,
gotify,
ntfy,
slack,
@@ -33,6 +34,7 @@ export const sendEmailNotification = async (
to: toAddresses.join(", "),
subject,
html: htmlContent,
textEncoding: "base64",
});
} catch (err) {
console.log(err);
@@ -151,3 +153,18 @@ export const sendNtfyNotification = async (
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
}
};
export const sendLarkNotification = async (
connection: typeof lark.$inferInsert,
message: any,
) => {
try {
await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
} catch (err) {
console.log(err);
}
};
+51 -13
View File
@@ -46,8 +46,14 @@ export const deleteMiddleware = (
};
export const deleteAllMiddlewares = async (application: ApplicationNested) => {
const config = loadMiddlewares<FileConfig>();
const { security, appName, redirects } = application;
const { security, appName, redirects, serverId } = application;
let config: FileConfig;
if (serverId) {
config = await loadRemoteMiddlewares(serverId);
} else {
config = loadMiddlewares<FileConfig>();
}
if (config.http?.middlewares) {
if (security.length > 0) {
@@ -62,8 +68,8 @@ export const deleteAllMiddlewares = async (application: ApplicationNested) => {
}
}
if (application.serverId) {
await writeTraefikConfigRemote(config, "middlewares", application.serverId);
if (serverId) {
await writeTraefikConfigRemote(config, "middlewares", serverId);
} else {
writeMiddleware(config);
}
@@ -100,7 +106,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => {
throw new Error(`File not found: ${configPath}`);
}
};
export const writeMiddleware = <T>(config: T) => {
export const writeMiddleware = (config: FileConfig) => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
const newYamlContent = stringify(config);
@@ -111,6 +117,18 @@ export const createPathMiddlewares = async (
app: ApplicationNested,
domain: Domain,
) => {
const { appName } = app;
const { uniqueConfigKey, internalPath, stripPath, path } = domain;
// Early return if there's no path middleware to create
const needsInternalPathMiddleware =
internalPath && internalPath !== "/" && internalPath !== path;
const needsStripPathMiddleware = stripPath && path && path !== "/";
if (!needsInternalPathMiddleware && !needsStripPathMiddleware) {
return;
}
let config: FileConfig;
if (app.serverId) {
@@ -127,20 +145,19 @@ export const createPathMiddlewares = async (
}
}
const { appName } = app;
const { uniqueConfigKey, internalPath, stripPath, path } = domain;
if (!config.http) {
if (!config) {
config = { http: { middlewares: {} } };
} else if (!config.http) {
config.http = { middlewares: {} };
}
if (!config.http.middlewares) {
config.http.middlewares = {};
if (!config.http?.middlewares) {
config.http!.middlewares = {};
}
// Add internal path prefix middleware
if (internalPath && internalPath !== "/" && internalPath !== path) {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
config.http.middlewares[middlewareName] = {
config.http!.middlewares[middlewareName] = {
addPrefix: {
prefix: internalPath,
},
@@ -150,7 +167,7 @@ export const createPathMiddlewares = async (
// Strip external path middleware if needed
if (stripPath && path && path !== "/") {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
config.http.middlewares[middlewareName] = {
config.http!.middlewares[middlewareName] = {
stripPrefix: {
prefixes: [path],
},
@@ -184,6 +201,10 @@ export const removePathMiddlewares = async (
}
}
if (!config) {
return;
}
const { appName } = app;
if (config.http?.middlewares) {
@@ -194,6 +215,23 @@ export const removePathMiddlewares = async (
delete config.http.middlewares[stripPrefixMiddleware];
}
if (
config?.http?.middlewares &&
Object.keys(config.http.middlewares).length === 0
) {
// if there aren't any middlewares, remove the whole section
delete config.http.middlewares;
}
// // If http section is empty, remove it completely
if (config?.http && Object.keys(config?.http).length === 0) {
delete config.http;
}
if (config && Object.keys(config || {}).length === 0) {
config = {};
}
if (app.serverId) {
await writeTraefikConfigRemote(config, "middlewares", app.serverId);
} else {