Merge branch 'canary' into feature/custom-entrypoint

This commit is contained in:
Mauricio Siu
2025-12-07 20:46:32 -06:00
160 changed files with 84257 additions and 2353 deletions

View File

@@ -1,5 +1,5 @@
import { paths } from "@dokploy/server/constants";
import { findAdmin } from "@dokploy/server/services/admin";
import { findOwner } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync";
@@ -29,9 +29,9 @@ export const startLogCleanup = async (
}
});
const admin = await findAdmin();
if (admin) {
await updateUser(admin.user.id, {
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
logCleanupCron: cronExpression,
});
}
@@ -51,9 +51,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
}
// Update database
const admin = await findAdmin();
if (admin) {
await updateUser(admin.user.id, {
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
logCleanupCron: null,
});
}
@@ -69,8 +69,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean;
cronExpression: string | null;
}> => {
const admin = await findAdmin();
const cronExpression = admin?.user.logCleanupCron ?? null;
const owner = await findOwner();
const cronExpression = owner?.user.logCleanupCron ?? null;
return {
enabled: cronExpression !== null,
cronExpression,

View File

@@ -6,11 +6,7 @@ import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
import { startLogCleanup } from "../access-log/handler";
import {
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
} from "../docker/utils";
import { cleanupAll } from "../docker/utils";
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils";
@@ -34,9 +30,9 @@ export const initCronJobs = async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await cleanupAll();
await sendDockerCleanupNotifications(admin.user.id);
});
}
@@ -50,9 +46,9 @@ export const initCronJobs = async () => {
console.log(
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
);
await cleanUpUnusedImages(serverId);
await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId);
await cleanupAll(serverId);
await sendDockerCleanupNotifications(
admin.user.id,
`Docker cleanup for Server ${name} (${serverId})`,

View File

@@ -3,8 +3,8 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";

View File

@@ -3,8 +3,8 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";

View File

@@ -3,8 +3,8 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";

View File

@@ -3,8 +3,8 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";

View File

@@ -62,16 +62,16 @@ export const getS3Credentials = (destination: Destination) => {
const { accessKey, secretAccessKey, region, endpoint, provider } =
destination;
const rcloneFlags = [
`--s3-access-key-id=${accessKey}`,
`--s3-secret-access-key=${secretAccessKey}`,
`--s3-region=${region}`,
`--s3-endpoint=${endpoint}`,
`--s3-access-key-id="${accessKey}"`,
`--s3-secret-access-key="${secretAccessKey}"`,
`--s3-region="${region}"`,
`--s3-endpoint="${endpoint}"`,
"--s3-no-check-bucket",
"--s3-force-path-style",
];
if (provider) {
rcloneFlags.unshift(`--s3-provider=${provider}`);
rcloneFlags.unshift(`--s3-provider="${provider}"`);
}
return rcloneFlags;

View File

@@ -8,7 +8,6 @@ import {
getDockerContextPath,
} from "../filesystem/directory";
import type { ApplicationNested } from ".";
import { createEnvFileCommand } from "./utils";
export const getDockerCommand = (application: ApplicationNested) => {
const {
@@ -68,21 +67,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
commandArgs.push("--secret", `type=env,id=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
*/
let command = "";
if (!publishDirectory) {
command += createEnvFileCommand(
dockerFilePath,
env,
application.environment.project.env,
application.environment.env,
);
}
command += `
const command = `
echo "Building ${appName}" ;
cd ${dockerContextPath} || {
echo "❌ The path ${dockerContextPath} does not exist" ;

View File

@@ -1,6 +1,6 @@
import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions } from "dockerode";
import { uploadImageRemoteCommand } from "../cluster/upload";
import { getRegistryTag, uploadImageRemoteCommand } from "../cluster/upload";
import {
calculateResources,
generateBindMounts,
@@ -29,39 +29,46 @@ export type ApplicationNested = InferResultType<
redirects: true;
ports: true;
registry: true;
buildRegistry: true;
rollbackRegistry: true;
deployments: true;
environment: { with: { project: true } };
}
>;
export const getBuildCommand = (application: ApplicationNested) => {
export const getBuildCommand = async (application: ApplicationNested) => {
let command = "";
const { buildType, registry } = application;
if (application.sourceType === "docker") {
return "";
if (application.sourceType !== "docker") {
const { buildType } = application;
switch (buildType) {
case "nixpacks":
command = getNixpacksCommand(application);
break;
case "heroku_buildpacks":
command = getHerokuCommand(application);
break;
case "paketo_buildpacks":
command = getPaketoCommand(application);
break;
case "static":
command = getStaticCommand(application);
break;
case "dockerfile":
command = getDockerCommand(application);
break;
case "railpack":
command = getRailpackCommand(application);
break;
}
}
switch (buildType) {
case "nixpacks":
command = getNixpacksCommand(application);
break;
case "heroku_buildpacks":
command = getHerokuCommand(application);
break;
case "paketo_buildpacks":
command = getPaketoCommand(application);
break;
case "static":
command = getStaticCommand(application);
break;
case "dockerfile":
command = getDockerCommand(application);
break;
case "railpack":
command = getRailpackCommand(application);
break;
}
if (registry) {
command += uploadImageRemoteCommand(application);
if (
application.registry ||
application.buildRegistry ||
application.rollbackRegistry
) {
command += await uploadImageRemoteCommand(application);
}
return command;
@@ -79,6 +86,7 @@ export const mechanizeDockerContainer = async (
memoryReservation,
cpuReservation,
command,
args,
ports,
} = application;
@@ -125,12 +133,16 @@ export const mechanizeDockerContainer = async (
Image: image,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,
@@ -153,8 +165,6 @@ export const mechanizeDockerContainer = async (
})),
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
@@ -169,23 +179,26 @@ export const mechanizeDockerContainer = async (
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
} catch (error) {
console.log(error);
await docker.createService(settings);
}
};
const getImageName = (application: ApplicationNested) => {
const { appName, sourceType, dockerImage, registry } = application;
const { appName, sourceType, dockerImage, registry, buildRegistry } =
application;
const imageName = `${appName}:latest`;
if (sourceType === "docker") {
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
}
if (registry) {
const { registryUrl, imagePrefix, username } = registry;
const registryTag = imagePrefix
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
const registryTag = getRegistryTag(registry, imageName);
return registryTag;
}
if (buildRegistry) {
const registryTag = getRegistryTag(buildRegistry, imageName);
return registryTag;
}
@@ -193,7 +206,14 @@ const getImageName = (application: ApplicationNested) => {
};
export const getAuthConfig = (application: ApplicationNested) => {
const { registry, username, password, sourceType, registryUrl } = application;
const {
registry,
buildRegistry,
username,
password,
sourceType,
registryUrl,
} = application;
if (sourceType === "docker") {
if (username && password) {
@@ -209,6 +229,12 @@ export const getAuthConfig = (application: ApplicationNested) => {
username: registry.username,
serveraddress: registry.registryUrl,
};
} else if (buildRegistry) {
return {
password: buildRegistry.password,
username: buildRegistry.username,
serveraddress: buildRegistry.registryUrl,
};
}
return undefined;

View File

@@ -85,6 +85,9 @@ export const getRailpackCommand = (application: ApplicationNested) => {
const bashCommand = `
# Ensure we have a builder with containerd
export RAILPACK_VERSION=${application.railpackVersion}
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
docker buildx create --use --name builder-containerd --driver docker-container || true
docker buildx use builder-containerd

View File

@@ -1,44 +1,107 @@
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
import type { Registry } from "@dokploy/server/services/registry";
import { createRollback } from "@dokploy/server/services/rollbacks";
import type { ApplicationNested } from "../builders";
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
export const uploadImageRemoteCommand = async (
application: ApplicationNested,
) => {
const registry = application.registry;
const buildRegistry = application.buildRegistry;
const rollbackRegistry = application.rollbackRegistry;
if (!registry) {
throw new Error("Registry not found");
if (!registry && !buildRegistry && !rollbackRegistry) {
throw new Error("No registry found");
}
const { registryUrl, imagePrefix, username } = registry;
const { appName } = application;
const imageName = `${appName}:latest`;
const imageName =
application.sourceType === "docker"
? application.dockerImage || ""
: `${appName}:latest`;
const finalURL = registryUrl;
const commands: string[] = [];
if (registry) {
const registryTag = getRegistryTag(registry, imageName);
if (registryTag) {
commands.push(`echo "📦 [Enabled Registry Swarm]"`);
commands.push(getRegistryCommands(registry, imageName, registryTag));
}
}
if (buildRegistry) {
const buildRegistryTag = getRegistryTag(buildRegistry, imageName);
if (buildRegistryTag) {
commands.push(`echo "🔑 [Enabled Build Registry]"`);
commands.push(
getRegistryCommands(buildRegistry, imageName, buildRegistryTag),
);
commands.push(
`echo "⚠️ INFO: After the build is finished, you need to wait a few seconds for the server to download the image and run the container."`,
);
commands.push(
`echo "📊 Check the Logs tab to see when the container starts running."`,
);
}
}
// Build registry tag in correct format: registry.com/owner/image:tag
const registryTag = imagePrefix
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
if (rollbackRegistry && application.rollbackActive) {
const deployment = await findAllDeploymentsByApplicationId(
application.applicationId,
);
if (!deployment || !deployment[0]) {
throw new Error("Deployment not found");
}
const deploymentId = deployment[0].deploymentId;
const rollback = await createRollback({
appName: appName,
deploymentId: deploymentId,
});
const rollbackRegistryTag = getRegistryTag(
rollbackRegistry,
rollback?.image || "",
);
if (rollbackRegistryTag) {
commands.push(`echo "🔄 [Enabled Rollback Registry]"`);
commands.push(
getRegistryCommands(rollbackRegistry, imageName, rollbackRegistryTag),
);
}
}
try {
const command = `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || {
echo "❌ DockerHub Failed" ;
exit 1;
}
echo "✅ Registry Login Success" ;
docker tag ${imageName} ${registryTag} || {
echo "❌ Error tagging image" ;
exit 1;
}
echo "✅ Image Tagged" ;
docker push ${registryTag} || {
echo "❌ Error pushing image" ;
exit 1;
}
echo "✅ Image Pushed" ;
`;
return command;
return commands.join("\n");
} catch (error) {
throw error;
}
};
export const getRegistryTag = (registry: Registry, imageName: string) => {
const { registryUrl, imagePrefix, username } = registry;
return imagePrefix
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
};
const getRegistryCommands = (
registry: Registry,
imageName: string,
registryTag: string,
): string => {
return `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
echo "❌ DockerHub Failed" ;
exit 1;
}
echo "✅ Registry Login Success" ;
docker tag ${imageName} ${registryTag} || {
echo "❌ Error tagging image" ;
exit 1;
}
echo "✅ Image Tagged" ;
docker push ${registryTag} || {
echo "❌ Error pushing image" ;
exit 1;
}
echo "✅ Image Pushed" ;
`;
};

View File

@@ -29,6 +29,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
cpuLimit,
cpuReservation,
command,
args,
mounts,
} = mariadb;
@@ -73,12 +74,16 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,
@@ -106,8 +111,6 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
const service = docker.getService(appName);

View File

@@ -28,6 +28,7 @@ export const buildMongo = async (mongo: MongoNested) => {
databaseUser,
databasePassword,
command,
args,
mounts,
replicaSets,
} = mongo;
@@ -121,17 +122,24 @@ ${command ?? "wait $MONGOD_PID"}`;
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(replicaSets
? {
Command: ["/bin/bash"],
Args: ["-c", startupScript],
}
: {
...(command && {
Command: ["/bin/bash"],
Args: ["-c", command],
}),
}),
: {}),
...(command &&
!replicaSets && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 &&
!replicaSets && {
Args: args,
}),
Labels,
},
Networks,
@@ -159,8 +167,6 @@ ${command ?? "wait $MONGOD_PID"}`;
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {

View File

@@ -30,6 +30,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
cpuLimit,
cpuReservation,
command,
args,
mounts,
} = mysql;
@@ -79,12 +80,16 @@ export const buildMysql = async (mysql: MysqlNested) => {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,
@@ -112,8 +117,6 @@ export const buildMysql = async (mysql: MysqlNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
const service = docker.getService(appName);

View File

@@ -28,6 +28,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
databaseUser,
databasePassword,
command,
args,
mounts,
} = postgres;
@@ -72,12 +73,16 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(command
? {
Command: ["/bin/sh"],
Args: ["-c", command],
}
: {}),
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
Labels,
},
Networks,
@@ -105,8 +110,6 @@ export const buildPostgres = async (postgres: PostgresNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {
const service = docker.getService(appName);

View File

@@ -26,6 +26,7 @@ export const buildRedis = async (redis: RedisNested) => {
cpuLimit,
cpuReservation,
command,
args,
mounts,
} = redis;
@@ -70,11 +71,22 @@ export const buildRedis = async (redis: RedisNested) => {
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
Command: ["/bin/sh"],
Args: [
"-c",
command ? command : `redis-server --requirepass ${databasePassword}`,
],
...(StopGracePeriod !== null &&
StopGracePeriod !== undefined && { StopGracePeriod }),
...(command || args
? {
...(command && {
Command: command.split(" "),
}),
...(args &&
args.length > 0 && {
Args: args,
}),
}
: {
Command: ["/bin/sh"],
Args: ["-c", `redis-server --requirepass ${databasePassword}`],
}),
Labels,
},
Networks,
@@ -102,8 +114,6 @@ export const buildRedis = async (redis: RedisNested) => {
: [],
},
UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
};
try {

View File

@@ -144,81 +144,116 @@ 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, command);
} else {
await execAsync(command);
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanStoppedContainers = async (serverId?: string) => {
/**
* Docker commands passed through this method are held during Docker's build or pull process.
*
* https://github.com/Dokploy/dokploy/pull/3064
* https://github.com/fir4tozden
*/
export const dockerSafeExec = (exec: string) => `CHECK_INTERVAL=10
echo "Preparing for execution..."
while true; do
PROCESSES=$(ps aux | grep -E "docker build|docker pull" | grep -v grep)
if [ -z "$PROCESSES" ]; then
echo "Docker is idle. Starting execution..."
break
else
echo "Docker is busy. Will check again in $CHECK_INTERVAL seconds..."
sleep $CHECK_INTERVAL
fi
done
${exec}
echo "Execution completed."`;
export const cleanupContainers = async (serverId?: string) => {
try {
const command = "docker container prune --force";
if (serverId) {
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(command);
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanUpUnusedVolumes = async (serverId?: string) => {
export const cleanupImages = async (serverId?: string) => {
try {
const command = "docker volume prune --force";
const command = "docker image prune --all --force";
if (serverId) {
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, dockerSafeExec(command));
} else await execAsync(dockerSafeExec(command));
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanupVolumes = async (serverId?: string) => {
try {
const command = "docker volume prune --all --force";
if (serverId) {
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(command);
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanUpInactiveContainers = async () => {
export const cleanupBuilders = async (serverId?: string) => {
try {
const containers = await docker.listContainers({ all: true });
const inactiveContainers = containers.filter(
(container) => container.State !== "running",
);
const command = "docker builder prune --all --force";
for (const container of inactiveContainers) {
await docker.getContainer(container.Id).remove({ force: true });
console.log(`Cleaning up inactive container: ${container.Id}`);
if (serverId) {
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error("Error cleaning up inactive containers:", error);
console.error(error);
throw error;
}
};
export const cleanUpDockerBuilder = async (serverId?: string) => {
const command = "docker builder prune --all --force";
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
export const cleanupSystem = async (serverId?: string) => {
try {
const command = "docker system prune --all --force";
if (serverId) {
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(dockerSafeExec(command));
}
} catch (error) {
console.error(error);
throw error;
}
};
export const cleanUpSystemPrune = async (serverId?: string) => {
const command = "docker system prune --force --volumes";
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
export const cleanupAll = async (serverId?: string) => {
await cleanupContainers(serverId);
await cleanupImages(serverId);
await cleanupBuilders(serverId);
await cleanupSystem(serverId);
};
export const startService = async (appName: string) => {

View File

@@ -5,10 +5,11 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -45,286 +46,311 @@ export const sendBuildErrorNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
custom: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
try {
if (email) {
const template = await renderAsync(
BuildFailedEmail({
projectName,
applicationName,
applicationType,
errorMessage: errorMessage,
buildLink,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Build failed for dokploy",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Failed",
inline: true,
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
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 (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Failed",
"warning",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` +
`Error:\n${errorMessage}`,
);
}
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
await sendTelegramNotification(
telegram,
`<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,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":warning: *Build Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Error",
value: `\`\`\`${errorMessage}\`\`\``,
short: false,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Error",
message: "Build failed with errors",
projectName,
applicationName,
applicationType,
errorMessage: errorMessage,
errorMessage,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(email, "Build failed for dokploy", template);
}
status: "error",
type: "build",
});
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Failed",
inline: true,
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
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 (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Failed",
"warning",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}\n` +
`Error:\n${errorMessage}`,
);
}
if (telegram) {
const inlineButton = [
[
{
text: "Deployment Logs",
url: buildLink,
},
],
];
await sendTelegramNotification(
telegram,
`<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,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#FF0000",
pretext: ":warning: *Build Failed*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Error",
value: `\`\`\`${errorMessage}\`\`\``,
short: false,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
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",
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,
},
],
header: {
title: {
tag: "plain_text",
content: "⚠️ Build Failed",
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
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,
},
],
},
type: "danger",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
],
margin: "0px 0px 0px 0px",
},
],
type: "danger",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -6,10 +6,11 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -48,295 +49,322 @@ export const sendBuildSuccessNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
custom: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
const template = await renderAsync(
BuildSuccessEmail({
projectName,
applicationName,
applicationType,
buildLink,
date: date.toLocaleString(),
environmentName,
}),
).catch();
await sendEmailNotification(
email,
"Build success for dokploy",
template,
);
}
if (email) {
const template = await renderAsync(
BuildSuccessEmail({
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Successes"),
color: 0x57f287,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`🌍`", "Environment"),
value: environmentName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
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("🌍", `Environment: ${environmentName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`🌍Environment: ${environmentName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}`,
);
}
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>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(
date,
"PP",
)}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Environment",
value: environmentName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
if (custom) {
await sendCustomNotification(custom, {
title: "Build Success",
message: "Build completed successfully",
projectName,
applicationName,
applicationType,
buildLink,
timestamp: date.toISOString(),
date: date.toLocaleString(),
environmentName,
}),
).catch();
await sendEmailNotification(email, "Build success for dokploy", template);
}
domains: domains.map((domain) => domain.host).join(", "),
status: "success",
type: "build",
});
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Build Successes"),
color: 0x57f287,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`🌍`", "Environment"),
value: environmentName,
inline: true,
},
{
name: decorate("`❔`", "Type"),
value: applicationType,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`🧷`", "Build Link"),
value: `[Click here to access build link](${buildLink})`,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Build Notification",
},
});
}
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("🌍", `Environment: ${environmentName}`)}` +
`${decorate("❔", `Type: ${applicationType}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${decorate("🔗", `Build details:\n${buildLink}`)}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`🌍Environment: ${environmentName}\n` +
`❔Type: ${applicationType}\n` +
`🕒Date: ${date.toLocaleString()}`,
);
}
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>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Environment:</b> ${environmentName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Build Success*",
fields: [
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Environment",
value: environmentName,
short: true,
},
{
title: "Type",
value: applicationType,
short: true,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
actions: [
{
type: "button",
text: "View Build Details",
url: buildLink,
},
],
},
],
});
}
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",
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: `**Environment:**\n${environmentName}`,
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,
},
],
header: {
title: {
tag: "plain_text",
content: "✅ Build Success",
},
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
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: `**Environment:**\n${environmentName}`,
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,
},
],
},
type: "primary",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
{
tag: "button",
text: {
tag: "plain_text",
content: "View Build Details",
},
],
margin: "0px 0px 0px 0px",
},
],
type: "primary",
width: "default",
size: "medium",
behaviors: [
{
type: "open_url",
default_url: buildLink,
pc_url: "",
ios_url: "",
android_url: "",
},
],
margin: "0px 0px 0px 0px",
},
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -5,10 +5,11 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -45,316 +46,339 @@ export const sendDatabaseBackupNotifications = async ({
slack: true,
gotify: true,
ntfy: true,
custom: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
const template = await renderAsync(
DatabaseBackupEmail({
projectName,
applicationName,
databaseType,
type,
errorMessage,
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Database backup for dokploy",
template,
);
}
if (email) {
const template = await renderAsync(
DatabaseBackupEmail({
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Database Backup Successful")
: decorate(">", "`❌` Database Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Database"),
value: databaseType,
inline: true,
},
{
name: decorate("`📂`", "Database Name"),
value: databaseName,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && errorMessage
? [
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
},
]
: []),
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Database Backup Notification",
},
});
}
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("📂", `Database Name: ${databaseName}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${databaseType}\n` +
`📂Database Name: ${databaseName}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
);
}
if (telegram) {
const isError = type === "error" && errorMessage;
const statusEmoji = type === "success" ? "✅" : "❌";
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>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
await sendTelegramNotification(telegram, messageText);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: type === "success" ? "#00FF00" : "#FF0000",
pretext:
type === "success"
? ":white_check_mark: *Database Backup Successful*"
: ":x: *Database Backup Failed*",
fields: [
...(type === "error" && errorMessage
? [
{
title: "Error Message",
value: errorMessage,
short: false,
},
]
: []),
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: databaseType,
short: true,
},
{
title: "Database Name",
value: databaseName,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
},
],
},
],
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Database Backup ${type === "success" ? "Successful" : "Failed"}`,
message:
type === "success"
? "Database backup completed successfully"
: "Database backup failed",
projectName,
applicationName,
databaseType,
databaseName,
type,
errorMessage,
errorMessage: errorMessage || "",
timestamp: date.toISOString(),
date: date.toLocaleString(),
}),
).catch();
await sendEmailNotification(
email,
"Database backup for dokploy",
template,
);
}
status: type,
});
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (lark) {
const limitCharacter = 800;
const truncatedErrorMessage =
errorMessage && errorMessage.length > limitCharacter
? errorMessage.substring(0, limitCharacter)
: errorMessage;
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Database Backup Successful")
: decorate(">", "`❌` Database Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`❔`", "Database"),
value: databaseType,
inline: true,
},
{
name: decorate("`📂`", "Database Name"),
value: databaseName,
inline: true,
},
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && 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: [
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
},
]
: []),
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Database Backup Notification",
},
});
}
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("📂", `Database Name: ${databaseName}`)}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`❔Type: ${databaseType}\n` +
`📂Database Name: ${databaseName}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
);
}
if (telegram) {
const isError = type === "error" && errorMessage;
const statusEmoji = type === "success" ? "✅" : "❌";
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>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
await sendTelegramNotification(telegram, messageText);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: type === "success" ? "#00FF00" : "#FF0000",
pretext:
type === "success"
? ":white_check_mark: *Database Backup Successful*"
: ":x: *Database Backup Failed*",
fields: [
...(type === "error" && errorMessage
? [
tag: "column_set",
columns: [
{
title: "Error Message",
value: errorMessage,
short: false,
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,
},
]
: []),
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Type",
value: databaseType,
short: true,
},
{
title: "Database Name",
value: databaseName,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
},
],
},
],
});
}
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",
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",
},
]
: []),
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -5,10 +5,11 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -32,189 +33,205 @@ export const sendDockerCleanupNotifications = async (
slack: true,
gotify: true,
ntfy: true,
custom: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
try {
if (email) {
const template = await renderAsync(
DockerCleanupEmail({ message, date: date.toLocaleString() }),
).catch();
if (email) {
const template = await renderAsync(
DockerCleanupEmail({ message, date: date.toLocaleString() }),
).catch();
await sendEmailNotification(
email,
"Docker cleanup for dokploy",
template,
);
}
await sendEmailNotification(
email,
"Docker cleanup for dokploy",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Docker Cleanup"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Docker Cleanup"),
color: 0x57f287,
fields: [
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`📜`", "Message"),
value: `\`\`\`${message}\`\`\``,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Docker Cleanup Notification",
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: "Successful",
inline: true,
},
{
name: decorate("`📜`", "Message"),
value: `\`\`\`${message}\`\`\``,
},
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Docker Cleanup Notification",
},
});
}
});
}
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 (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 (ntfy) {
await sendNtfyNotification(
ntfy,
"Docker Cleanup",
"white_check_mark",
"",
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Docker Cleanup",
"white_check_mark",
"",
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Docker Cleanup*",
fields: [
{
title: "Message",
value: message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: "#00FF00",
pretext: ":white_check_mark: *Docker Cleanup*",
fields: [
{
title: "Message",
value: message,
},
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
],
},
],
});
}
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",
if (custom) {
await sendCustomNotification(custom, {
title: "Docker Cleanup",
message: "Docker cleanup completed successfully",
cleanupMessage: message,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "success",
type: "docker-cleanup",
});
}
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,
},
],
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,
},
],
},
],
},
},
},
});
});
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -5,10 +5,11 @@ import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { eq } from "drizzle-orm";
import {
sendCustomNotification,
sendDiscordNotification,
sendEmailNotification,
sendLarkNotification,
sendGotifyNotification,
sendLarkNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
@@ -26,26 +27,32 @@ export const sendDokployRestartNotifications = async () => {
slack: true,
gotify: true,
ntfy: true,
custom: true,
lark: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, lark } =
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } =
notification;
if (email) {
const template = await renderAsync(
DokployRestartEmail({ date: date.toLocaleString() }),
).catch();
await sendEmailNotification(email, "Dokploy Server Restarted", template);
}
try {
if (email) {
const template = await renderAsync(
DokployRestartEmail({ date: date.toLocaleString() }),
).catch();
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendEmailNotification(
email,
"Dokploy Server Restarted",
template,
);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
try {
await sendDiscordNotification(discord, {
title: decorate(">", "`✅` Dokploy Server Restarted"),
color: 0x57f287,
@@ -71,27 +78,19 @@ export const sendDokployRestartNotifications = async () => {
text: "Dokploy Restart Notification",
},
});
} catch (error) {
console.log(error);
}
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
try {
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate("✅", "Dokploy Server Restarted"),
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}`,
);
} catch (error) {
console.log(error);
}
}
if (ntfy) {
try {
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Dokploy Server Restarted",
@@ -99,25 +98,20 @@ export const sendDokployRestartNotifications = async () => {
"",
`🕒Date: ${date.toLocaleString()}`,
);
} catch (error) {
console.log(error);
}
}
if (telegram) {
try {
if (telegram) {
await sendTelegramNotification(
telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(
date,
"PP",
)}\n<b>Time:</b> ${format(date, "pp")}`,
);
} catch (error) {
console.log(error);
}
}
if (slack) {
const { channel } = slack;
try {
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
@@ -134,13 +128,24 @@ export const sendDokployRestartNotifications = async () => {
},
],
});
} catch (error) {
console.log(error);
}
}
if (lark) {
try {
if (custom) {
try {
await sendCustomNotification(custom, {
title: "Dokploy Server Restarted",
message: "Dokploy server has been restarted successfully",
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "success",
type: "dokploy-restart",
});
} catch (error) {
console.log(error);
}
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",
card: {
@@ -182,7 +187,7 @@ export const sendDokployRestartNotifications = async () => {
elements: [
{
tag: "markdown",
content: `**Status:**\nSuccessful`,
content: "**Status:**\nSuccessful",
text_align: "left",
text_size: "normal_v2",
},
@@ -196,7 +201,10 @@ export const sendDokployRestartNotifications = async () => {
elements: [
{
tag: "markdown",
content: `**Restart Time:**\n${format(date, "PP pp")}`,
content: `**Restart Time:**\n${format(
date,
"PP pp",
)}`,
text_align: "left",
text_size: "normal_v2",
},
@@ -210,9 +218,9 @@ export const sendDokployRestartNotifications = async () => {
},
},
});
} catch (error) {
console.log(error);
}
} catch (error) {
console.log(error);
}
}
};

View File

@@ -2,6 +2,7 @@ import { and, eq } from "drizzle-orm";
import { db } from "../../db";
import { notifications } from "../../db/schema";
import {
sendCustomNotification,
sendDiscordNotification,
sendLarkNotification,
sendSlackNotification,
@@ -35,6 +36,7 @@ export const sendServerThresholdNotifications = async (
discord: true,
telegram: true,
slack: true,
custom: true,
lark: true,
},
});
@@ -43,7 +45,7 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) {
const { discord, telegram, slack, lark } = notification;
const { discord, telegram, slack, custom, lark } = notification;
if (discord) {
const decorate = (decoration: string, text: string) =>
@@ -154,6 +156,21 @@ export const sendServerThresholdNotifications = async (
});
}
if (custom) {
await sendCustomNotification(custom, {
title: `Server ${payload.Type} Alert`,
message: payload.Message,
serverName: payload.ServerName,
type: payload.Type,
currentValue: payload.Value,
threshold: payload.Threshold,
timestamp: date.toISOString(),
date: date.toLocaleString(),
status: "alert",
alertType: "server-threshold",
});
}
if (lark) {
await sendLarkNotification(lark, {
msg_type: "interactive",

View File

@@ -1,8 +1,9 @@
import type {
custom,
discord,
email,
lark,
gotify,
lark,
ntfy,
slack,
telegram,
@@ -38,6 +39,9 @@ export const sendEmailNotification = async (
});
} catch (err) {
console.log(err);
throw new Error(
`Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
@@ -45,15 +49,23 @@ 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 {
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});
if (!response.ok) {
throw new Error(
`Failed to send discord notification ${response.statusText}`,
);
}
} catch (err) {
console.log("error", err);
throw new Error(
`Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
export const sendTelegramNotification = async (
@@ -90,13 +102,21 @@ export const sendSlackNotification = async (
message: any,
) => {
try {
await fetch(connection.webhookUrl, {
const response = await fetch(connection.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
});
if (!response.ok) {
throw new Error(
`Failed to send slack notification ${response.statusText}`,
);
}
} catch (err) {
console.log(err);
console.log("error", err);
throw new Error(
`Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`,
);
}
};
@@ -140,7 +160,9 @@ export const sendNtfyNotification = async (
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
method: "POST",
headers: {
Authorization: `Bearer ${connection.accessToken}`,
...(connection.accessToken && {
Authorization: `Bearer ${connection.accessToken}`,
}),
"X-Priority": connection.priority?.toString() || "3",
"X-Title": title,
"X-Tags": tags,
@@ -154,6 +176,39 @@ export const sendNtfyNotification = async (
}
};
export const sendCustomNotification = async (
connection: typeof custom.$inferInsert,
payload: Record<string, any>,
) => {
try {
// Merge default headers with custom headers (now already an object from jsonb)
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(connection.headers || {}),
};
// Default body with payload
const body = JSON.stringify(payload);
const response = await fetch(connection.endpoint, {
method: "POST",
headers,
body,
});
if (!response.ok) {
throw new Error(
`Failed to send custom notification: ${response.statusText}`,
);
}
return response;
} catch (error) {
console.error("Error sending custom notification:", error);
throw error;
}
};
export const sendLarkNotification = async (
connection: typeof lark.$inferInsert,
message: any,

View File

@@ -0,0 +1,274 @@
import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema";
import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup";
import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
export const sendVolumeBackupNotifications = async ({
projectName,
applicationName,
volumeName,
serviceType,
type,
errorMessage,
organizationId,
backupSize,
}: {
projectName: string;
applicationName: string;
volumeName: string;
serviceType:
| "application"
| "postgres"
| "mysql"
| "mongodb"
| "mariadb"
| "redis"
| "compose";
type: "error" | "success";
organizationId: string;
errorMessage?: string;
backupSize?: string;
}) => {
const date = new Date();
const unixDate = ~~(Number(date) / 1000);
const notificationList = await db.query.notifications.findMany({
where: and(
eq(notifications.volumeBackup, true),
eq(notifications.organizationId, organizationId),
),
with: {
email: true,
discord: true,
telegram: true,
slack: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
const htmlContent = await renderAsync(
VolumeBackupEmail({
projectName,
applicationName,
volumeName,
serviceType,
type,
errorMessage,
backupSize,
date: date.toISOString(),
}),
);
await sendEmailNotification(email, subject, htmlContent);
}
if (discord) {
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
await sendDiscordNotification(discord, {
title:
type === "success"
? decorate(">", "`✅` Volume Backup Successful")
: decorate(">", "`❌` Volume Backup Failed"),
color: type === "success" ? 0x57f287 : 0xed4245,
fields: [
{
name: decorate("`🛠️`", "Project"),
value: projectName,
inline: true,
},
{
name: decorate("`⚙️`", "Application"),
value: applicationName,
inline: true,
},
{
name: decorate("`💾`", "Volume Name"),
value: volumeName,
inline: true,
},
{
name: decorate("`🔧`", "Service Type"),
value: serviceType,
inline: true,
},
...(backupSize
? [
{
name: decorate("`📊`", "Backup Size"),
value: backupSize,
inline: true,
},
]
: []),
{
name: decorate("`📅`", "Date"),
value: `<t:${unixDate}:D>`,
inline: true,
},
{
name: decorate("`⌚`", "Time"),
value: `<t:${unixDate}:t>`,
inline: true,
},
{
name: decorate("`❓`", "Type"),
value: type
.replace("error", "Failed")
.replace("success", "Successful"),
inline: true,
},
...(type === "error" && errorMessage
? [
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
},
]
: []),
],
timestamp: date.toISOString(),
footer: {
text: "Dokploy Volume Backup Notification",
},
});
}
if (gotify) {
const decorate = (decoration: string, text: string) =>
`${gotify.decoration ? decoration : ""} ${text}\n`;
await sendGotifyNotification(
gotify,
decorate(
type === "success" ? "✅" : "❌",
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
),
`${decorate("🛠️", `Project: ${projectName}`)}` +
`${decorate("⚙️", `Application: ${applicationName}`)}` +
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`${type === "success" ? "white_check_mark" : "x"}`,
"",
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\n` +
`💾Volume Name: ${volumeName}\n` +
`🔧Service Type: ${serviceType}\n` +
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
`🕒Date: ${date.toLocaleString()}\n` +
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
);
}
if (telegram) {
const isError = type === "error" && errorMessage;
const statusEmoji = type === "success" ? "✅" : "❌";
const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg = isError
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
: "";
const sizeInfo = backupSize ? `\n<b>Backup Size:</b> ${backupSize}` : "";
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
await sendTelegramNotification(telegram, messageText);
}
if (slack) {
const { channel } = slack;
await sendSlackNotification(slack, {
channel: channel,
attachments: [
{
color: type === "success" ? "#00FF00" : "#FF0000",
pretext:
type === "success"
? ":white_check_mark: *Volume Backup Successful*"
: ":x: *Volume Backup Failed*",
fields: [
...(type === "error" && errorMessage
? [
{
title: "Error Message",
value: errorMessage,
short: false,
},
]
: []),
{
title: "Project",
value: projectName,
short: true,
},
{
title: "Application",
value: applicationName,
short: true,
},
{
title: "Volume Name",
value: volumeName,
short: true,
},
{
title: "Service Type",
value: serviceType,
short: true,
},
...(backupSize
? [
{
title: "Backup Size",
value: backupSize,
short: true,
},
]
: []),
{
title: "Time",
value: date.toLocaleString(),
short: true,
},
{
title: "Type",
value: type,
short: true,
},
{
title: "Status",
value: type === "success" ? "Successful" : "Failed",
short: true,
},
],
},
],
});
}
}
};

View File

@@ -0,0 +1,55 @@
export interface ExecErrorDetails {
command: string;
stdout?: string;
stderr?: string;
exitCode?: number;
originalError?: Error;
serverId?: string | null;
}
export class ExecError extends Error {
public readonly command: string;
public readonly stdout?: string;
public readonly stderr?: string;
public readonly exitCode?: number;
public readonly originalError?: Error;
public readonly serverId?: string | null;
constructor(message: string, details: ExecErrorDetails) {
super(message);
this.name = "ExecError";
this.command = details.command;
this.stdout = details.stdout;
this.stderr = details.stderr;
this.exitCode = details.exitCode;
this.originalError = details.originalError;
this.serverId = details.serverId;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ExecError);
}
}
/**
* Get a formatted error message with all details
*/
getDetailedMessage(): string {
const parts = [
`Command: ${this.command}`,
this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null,
this.serverId ? `Server ID: ${this.serverId}` : "Location: Local",
this.stderr ? `Stderr: ${this.stderr}` : null,
this.stdout ? `Stdout: ${this.stdout}` : null,
].filter(Boolean);
return `${this.message}\n${parts.join("\n")}`;
}
/**
* Check if this error is from a remote execution
*/
isRemote(): boolean {
return !!this.serverId;
}
}

View File

@@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process";
import util from "node:util";
import { findServerById } from "@dokploy/server/services/server";
import { Client } from "ssh2";
import { ExecError } from "./ExecError";
export const execAsync = util.promisify(exec);
// Re-export ExecError for easier imports
export { ExecError } from "./ExecError";
const execAsyncBase = util.promisify(exec);
export const execAsync = async (
command: string,
options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string },
): Promise<{ stdout: string; stderr: string }> => {
try {
const result = await execAsyncBase(command, options);
return {
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
};
} catch (error) {
if (error instanceof Error) {
// @ts-ignore - exec error has these properties
const exitCode = error.code;
// @ts-ignore
const stdout = error.stdout?.toString() || "";
// @ts-ignore
const stderr = error.stderr?.toString() || "";
throw new ExecError(`Command execution failed: ${error.message}`, {
command,
stdout,
stderr,
exitCode,
originalError: error,
});
}
throw error;
}
};
interface ExecOptions {
cwd?: string;
@@ -21,7 +56,16 @@ export const execAsyncStream = (
const childProcess = exec(command, options, (error) => {
if (error) {
reject(error);
reject(
new ExecError(`Command execution failed: ${error.message}`, {
command,
stdout: stdoutComplete,
stderr: stderrComplete,
// @ts-ignore
exitCode: error.code,
originalError: error,
}),
);
return;
}
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
@@ -45,7 +89,14 @@ export const execAsyncStream = (
childProcess.on("error", (error) => {
console.log(error);
reject(error);
reject(
new ExecError(`Command execution error: ${error.message}`, {
command,
stdout: stdoutComplete,
stderr: stderrComplete,
originalError: error,
}),
);
});
});
};
@@ -108,7 +159,14 @@ export const execAsyncRemote = async (
conn.exec(command, (err, stream) => {
if (err) {
onData?.(err.message);
throw err;
reject(
new ExecError(`Remote command execution failed: ${err.message}`, {
command,
serverId,
originalError: err,
}),
);
return;
}
stream
.on("close", (code: number, _signal: string) => {
@@ -116,7 +174,18 @@ export const execAsyncRemote = async (
if (code === 0) {
resolve({ stdout, stderr });
} else {
reject(new Error(`Error occurred ❌: ${stderr}`));
reject(
new ExecError(
`Remote command failed with exit code ${code}`,
{
command,
stdout,
stderr,
exitCode: code,
serverId,
},
),
);
}
})
.on("data", (data: string) => {
@@ -132,17 +201,25 @@ export const execAsyncRemote = async (
.on("error", (err) => {
conn.end();
if (err.level === "client-authentication") {
onData?.(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
);
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
onData?.(errorMsg);
reject(
new Error(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
),
new ExecError(errorMsg, {
command,
serverId,
originalError: err,
}),
);
} else {
onData?.(`SSH connection error: ${err.message}`);
reject(new Error(`SSH connection error: ${err.message}`));
const errorMsg = `SSH connection error: ${err.message}`;
onData?.(errorMsg);
reject(
new ExecError(errorMsg, {
command,
serverId,
originalError: err,
}),
);
}
})
.connect({

View File

@@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({
const octokit = authGithub(githubProvider);
const token = await getGithubToken(octokit);
const repoclone = `github.com/${owner}/${repository}.git`;
// await recreateDirectory(outputPath);
command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`;
const cloneUrl = `https://oauth2:${token}@${repoclone}`;

View File

@@ -290,7 +290,7 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
while (true) {
const response = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,

View File

@@ -23,11 +23,18 @@ export const updateServerTraefik = (
config.http.routers = config.http.routers || {};
config.http.services = config.http.services || {};
// Get or create router config, but always update the rule with newHost
const currentRouterConfig = config.http.routers[`${appName}-router-app`] || {
rule: `Host(\`${newHost}\`)`,
service: `${appName}-service-app`,
entryPoints: ["web"],
rule: `Host(\`${newHost}\`)`,
};
// Always update the rule with the new host
if (newHost) {
currentRouterConfig.rule = `Host(\`${newHost}\`)`;
}
config.http.routers[`${appName}-router-app`] = currentRouterConfig;
config.http.services = {

View File

@@ -11,8 +11,54 @@ import {
} from "@dokploy/server/utils/process/execAsync";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
import { backupVolume } from "./backup";
// Helper functions to extract project info from volume backup
const getProjectName = (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
): string => {
const services = [
volumeBackup.application,
volumeBackup.compose,
volumeBackup.postgres,
volumeBackup.mysql,
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
];
for (const service of services) {
if (service?.environment?.project?.name) {
return service.environment.project.name;
}
}
return "Unknown Project";
};
const getOrganizationId = (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
): string => {
const services = [
volumeBackup.application,
volumeBackup.compose,
volumeBackup.postgres,
volumeBackup.mysql,
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
];
for (const service of services) {
if (service?.environment?.project?.organizationId) {
return service.environment.project.organizationId;
}
}
return "";
};
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
@@ -61,7 +107,8 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
title: "Volume Backup",
description: "Volume Backup",
});
const projectName = getProjectName(volumeBackup);
const organizationId = getOrganizationId(volumeBackup);
try {
const command = await backupVolume(volumeBackup);
@@ -77,6 +124,21 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
}
await updateDeploymentStatus(deployment.deploymentId, "done");
// Map service type to match notification function expectations
const mappedServiceType =
volumeBackup.serviceType === "mongo"
? "mongodb"
: volumeBackup.serviceType;
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "success",
organizationId,
});
} catch (error) {
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
const volumeBackupPath = path.join(
@@ -92,6 +154,20 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
}
await updateDeploymentStatus(deployment.deploymentId, "error");
console.error(error);
// Send error notification
const mappedServiceType =
volumeBackup.serviceType === "mongo"
? "mongodb"
: volumeBackup.serviceType;
await sendVolumeBackupNotifications({
projectName,
applicationName: volumeBackup.name,
volumeName: volumeBackup.volumeName,
serviceType: mappedServiceType,
type: "error",
organizationId,
errorMessage: error instanceof Error ? error.message : String(error),
});
}
};