Merge branch 'canary' into bitbucket-api-token

This commit is contained in:
Mauricio Siu
2025-09-21 03:10:37 -06:00
98 changed files with 8239 additions and 471 deletions

View File

@@ -328,6 +328,26 @@ export const apiFindOneApplication = createSchema
})
.required();
export const apiDeployApplication = createSchema
.pick({
applicationId: true,
})
.extend({
applicationId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiRedeployApplication = createSchema
.pick({
applicationId: true,
})
.extend({
applicationId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiReloadApplication = createSchema
.pick({
appName: true,

View File

@@ -181,6 +181,18 @@ export const apiFindCompose = z.object({
composeId: z.string().min(1),
});
export const apiDeployCompose = z.object({
composeId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiRedeployCompose = z.object({
composeId: z.string().min(1),
title: z.string().optional(),
description: z.string().optional(),
});
export const apiDeleteCompose = z.object({
composeId: z.string().min(1),
deleteVolumes: z.boolean(),

View File

@@ -58,4 +58,5 @@ export const apiUpdateGithub = createSchema.extend({
githubId: z.string().min(1),
name: z.string().min(1),
gitProviderId: z.string().min(1),
githubAppName: z.string().min(1),
});

View File

@@ -11,6 +11,7 @@ export const notificationType = pgEnum("notificationType", [
"discord",
"email",
"gotify",
"ntfy",
]);
export const notifications = pgTable("notification", {
@@ -44,6 +45,9 @@ export const notifications = pgTable("notification", {
gotifyId: text("gotifyId").references(() => gotify.gotifyId, {
onDelete: "cascade",
}),
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
onDelete: "cascade",
}),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -101,6 +105,17 @@ export const gotify = pgTable("gotify", {
decoration: boolean("decoration"),
});
export const ntfy = pgTable("ntfy", {
ntfyId: text("ntfyId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
serverUrl: text("serverUrl").notNull(),
topic: text("topic").notNull(),
accessToken: text("accessToken").notNull(),
priority: integer("priority").notNull().default(3),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, {
fields: [notifications.slackId],
@@ -122,6 +137,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.gotifyId],
references: [gotify.gotifyId],
}),
ntfy: one(ntfy, {
fields: [notifications.ntfyId],
references: [ntfy.ntfyId],
}),
organization: one(organization, {
fields: [notifications.organizationId],
references: [organization.id],
@@ -284,6 +303,36 @@ export const apiTestGotifyConnection = apiCreateGotify
decoration: z.boolean().optional(),
});
export const apiCreateNtfy = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
})
.extend({
serverUrl: z.string().min(1),
topic: z.string().min(1),
accessToken: z.string().min(1),
priority: z.number().min(1),
})
.required();
export const apiUpdateNtfy = apiCreateNtfy.partial().extend({
notificationId: z.string().min(1),
ntfyId: z.string().min(1),
organizationId: z.string().optional(),
});
export const apiTestNtfyConnection = apiCreateNtfy.pick({
serverUrl: true,
topic: true,
accessToken: true,
priority: true,
});
export const apiFindOneNotification = notificationsSchema
.pick({
notificationId: true,
@@ -303,7 +352,9 @@ export const apiSendTest = notificationsSchema
password: z.string(),
toAddresses: z.array(z.string()),
serverUrl: z.string(),
topic: z.string(),
appToken: z.string(),
accessToken: z.string(),
priority: z.number(),
})
.partial();

View File

@@ -322,6 +322,11 @@ export const apiUpdateWebServerMonitoring = z.object({
});
export const apiUpdateUser = createSchema.partial().extend({
email: z
.string()
.email("Please enter a valid email address")
.min(1, "Email is required")
.optional(),
password: z.string().optional(),
currentPassword: z.string().optional(),
name: z.string().optional(),

View File

@@ -92,31 +92,48 @@ export const suggestVariants = async ({
const { object } = await generateObject({
model,
output: "array",
output: "object",
schema: z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
suggestions: z.array(
z.object({
id: z.string(),
name: z.string(),
shortDescription: z.string(),
description: z.string(),
}),
),
}),
prompt: `
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion
should include id, name, shortDescription, and description. Use slug of title for id.
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
Return your response as a JSON object with the following structure:
{
"suggestions": [
{
"id": "project-slug",
"name": "Project Name",
"shortDescription": "Brief one-line description",
"description": "Detailed description"
}
]
}
Important rules for the response:
1. The description field should ONLY contain a plain text description of the project, its features, and use cases
2. Do NOT include any code snippets, configuration examples, or installation instructions in the description
3. The shortDescription should be a single-line summary focusing on the main technologies
1. Use slug format for the id field (lowercase, hyphenated)
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
4. The shortDescription should be a single-line summary focusing on the main technologies
5. All projects should be installable in docker and have docker compose support
User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it:
User wants to create a new project with the following details:
${input}
`,
});
if (object?.length) {
if (object?.suggestions?.length) {
const result = [];
for (const suggestion of object) {
for (const suggestion of object.suggestions) {
try {
const { object: docker } = await generateObject({
model,
@@ -136,16 +153,29 @@ export const suggestVariants = async ({
serviceName: z.string(),
}),
),
configFiles: z.array(
z.object({
content: z.string(),
filePath: z.string(),
}),
),
configFiles: z
.array(
z.object({
content: z.string(),
filePath: z.string(),
}),
)
.optional(),
}),
prompt: `
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
Return the docker compose as a YAML string and environment variables configuration. Follow these rules:
Return your response as a JSON object with this structure:
{
"dockerCompose": "yaml string here",
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
}
Note: configFiles is optional - only include it if configuration files are absolutely required.
Follow these rules:
Docker Compose Rules:
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
@@ -198,6 +228,7 @@ export const suggestVariants = async ({
console.error("Error in docker compose generation:", error);
}
}
return result;
}

View File

@@ -9,7 +9,7 @@ import {
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { stringify } from "yaml";
import type { z } from "zod";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsyncRemote } from "../utils/process/execAsync";
@@ -101,7 +101,7 @@ const createCertificateFiles = async (certificate: Certificate) => {
],
},
};
const yamlConfig = dump(traefikConfig);
const yamlConfig = stringify(traefikConfig);
const configFile = path.join(certDir, "certificate.yml");
if (certificate.serverId) {

View File

@@ -3,17 +3,20 @@ import {
type apiCreateDiscord,
type apiCreateEmail,
type apiCreateGotify,
type apiCreateNtfy,
type apiCreateSlack,
type apiCreateTelegram,
type apiUpdateDiscord,
type apiUpdateEmail,
type apiUpdateGotify,
type apiUpdateNtfy,
type apiUpdateSlack,
type apiUpdateTelegram,
discord,
email,
gotify,
notifications,
ntfy,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -482,6 +485,96 @@ export const updateGotifyNotification = async (
});
};
export const createNtfyNotification = async (
input: typeof apiCreateNtfy._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newNtfy = await tx
.insert(ntfy)
.values({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
priority: input.priority,
})
.returning()
.then((value) => value[0]);
if (!newNtfy) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting ntfy",
});
}
const newDestination = await tx
.insert(notifications)
.values({
ntfyId: newNtfy.ntfyId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
notificationType: "ntfy",
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updateNtfyNotification = async (
input: typeof apiUpdateNtfy._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,
})
.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(ntfy)
.set({
serverUrl: input.serverUrl,
topic: input.topic,
accessToken: input.accessToken,
priority: input.priority,
})
.where(eq(ntfy.ntfyId, input.ntfyId));
return newDestination;
});
};
export const findNotificationById = async (notificationId: string) => {
const notification = await db.query.notifications.findFirst({
where: eq(notifications.notificationId, notificationId),
@@ -491,6 +584,7 @@ export const findNotificationById = async (notificationId: string) => {
discord: true,
email: true,
gotify: true,
ntfy: true,
},
});
if (!notification) {

View File

@@ -10,6 +10,22 @@ import { IS_CLOUD } from "../constants";
export type Registry = typeof registry.$inferSelect;
function shEscape(s: string | undefined): string {
if (!s) return "''";
return `'${s.replace(/'/g, `'\\''`)}'`;
}
function safeDockerLoginCommand(
registry: string | undefined,
user: string | undefined,
pass: string | undefined,
) {
const escapedRegistry = shEscape(registry);
const escapedUser = shEscape(user);
const escapedPassword = shEscape(pass);
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
}
export const createRegistry = async (
input: typeof apiCreateRegistry._type,
organizationId: string,
@@ -37,7 +53,11 @@ export const createRegistry = async (
message: "Select a server to add the registry",
});
}
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
const loginCommand = safeDockerLoginCommand(
input.registryUrl,
input.username,
input.password,
);
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else if (newRegistry.registryType === "cloud") {
@@ -91,7 +111,11 @@ export const updateRegistry = async (
.returning()
.then((res) => res[0]);
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`;
const loginCommand = safeDockerLoginCommand(
response?.registryUrl,
response?.username,
response?.password,
);
if (
IS_CLOUD &&

View File

@@ -342,6 +342,8 @@ export const readPorts = async (
command = `docker service inspect ${resourceName} --format '{{json .Spec.EndpointSpec.Ports}}'`;
} else if (resourceType === "standalone") {
command = `docker container inspect ${resourceName} --format '{{json .NetworkSettings.Ports}}'`;
} else {
throw new Error("Resource type not found");
}
let result = "";
if (serverId) {
@@ -397,17 +399,20 @@ export const writeTraefikSetup = async (input: TraefikOptions) => {
"dokploy-traefik",
input.serverId,
);
if (resourceType === "service") {
await initializeTraefikService({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: input.serverId,
});
} else {
} else if (resourceType === "standalone") {
await initializeStandaloneTraefik({
env: input.env,
additionalPorts: input.additionalPorts,
serverId: input.serverId,
});
} else {
throw new Error("Traefik resource type not found");
}
};

View File

@@ -296,6 +296,19 @@ export const findMemberById = async (
};
export const updateUser = async (userId: string, userData: Partial<User>) => {
// Validate email if it's being updated
if (userData.email !== undefined) {
if (!userData.email || userData.email.trim() === "") {
throw new Error("Email is required and cannot be empty");
}
// Basic email format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new Error("Please enter a valid email address");
}
}
const user = await db
.update(users_temp)
.set({

View File

@@ -1,7 +1,14 @@
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode";
import { dump } from "js-yaml";
import { stringify } from "yaml";
import { paths } from "../constants";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import type { FileConfig } from "../utils/traefik/file-types";
@@ -87,16 +94,27 @@ export const initializeStandaloneTraefik = async ({
};
const docker = await getRemoteDocker(serverId);
try {
await docker.pull(imageName);
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Traefik Image Pulled ✅");
} catch (error) {
console.log("Traefik Image Not Found: Pulling ", error);
}
try {
const container = docker.getContainer(containerName);
await container.remove({ force: true });
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch {}
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik Started ✅");
try {
await docker.createContainer(settings);
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik Started ✅");
} catch (error) {
console.log("Traefik Not Found: Starting ", error);
}
};
export const initializeTraefikService = async ({
@@ -223,7 +241,7 @@ export const createDefaultServerTraefikConfig = () => {
},
};
const yamlStr = dump(config);
const yamlStr = stringify(config);
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
writeFileSync(
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
@@ -297,7 +315,7 @@ export const getDefaultTraefikConfig = () => {
}),
};
const yamlStr = dump(configObject);
const yamlStr = stringify(configObject);
return yamlStr;
};
@@ -351,7 +369,7 @@ export const getDefaultServerTraefikConfig = () => {
},
};
const yamlStr = dump(configObject);
const yamlStr = stringify(configObject);
return yamlStr;
};
@@ -364,13 +382,26 @@ export const createDefaultTraefikConfig = () => {
if (existsSync(acmeJsonPath)) {
chmodSync(acmeJsonPath, "600");
}
if (existsSync(mainConfig)) {
console.log("Main config already exists");
return;
}
const yamlStr = getDefaultTraefikConfig();
// Create the traefik directory first
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
// Check if traefik.yml exists and handle the case where it might be a directory
if (existsSync(mainConfig)) {
const stats = statSync(mainConfig);
if (stats.isDirectory()) {
// If traefik.yml is a directory, remove it
console.log("Found traefik.yml as directory, removing it...");
rmSync(mainConfig, { recursive: true, force: true });
} else if (stats.isFile()) {
console.log("Main config already exists");
return;
}
}
const yamlStr = getDefaultTraefikConfig();
writeFileSync(mainConfig, yamlStr, "utf8");
console.log("Traefik config created successfully");
};
export const getDefaultMiddlewares = () => {
@@ -386,7 +417,7 @@ export const getDefaultMiddlewares = () => {
},
},
};
const yamlStr = dump(defaultMiddlewares);
const yamlStr = stringify(defaultMiddlewares);
return yamlStr;
};
export const createDefaultMiddlewares = () => {

View File

@@ -89,7 +89,7 @@ export const getMariadbBackupCommand = (
databaseUser: string,
databasePassword: string,
) => {
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --single-transaction --quick --databases ${database} | gzip"`;
};
export const getMysqlBackupCommand = (

View File

@@ -220,8 +220,8 @@ const getImageName = (application: ApplicationNested) => {
if (registry) {
const { registryUrl, imagePrefix, username } = registry;
const registryTag = imagePrefix
? `${registryUrl}/${imagePrefix}/${imageName}`
: `${registryUrl}/${username}/${imageName}`;
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
return registryTag;
}

View File

@@ -22,8 +22,8 @@ export const uploadImage = async (
// For ghcr.io: ghcr.io/username/image:tag
// For docker.io: docker.io/username/image:tag
const registryTag = imagePrefix
? `${registryUrl}/${imagePrefix}/${imageName}`
: `${registryUrl}/${username}/${imageName}`;
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
try {
writeStream.write(

View File

@@ -1,5 +1,5 @@
import { findComposeById } from "@dokploy/server/services/compose";
import { dump } from "js-yaml";
import { stringify } from "yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume";
@@ -59,7 +59,7 @@ export const randomizeIsolatedDeploymentComposeFile = async (
)
: composeData;
return dump(newComposeFile);
return stringify(newComposeFile);
};
export const randomizeDeployableSpecificationFile = (

View File

@@ -1,6 +1,6 @@
import crypto from "node:crypto";
import { findComposeById } from "@dokploy/server/services/compose";
import { dump, load } from "js-yaml";
import { parse, stringify } from "yaml";
import { addSuffixToAllConfigs } from "./compose/configs";
import { addSuffixToAllNetworks } from "./compose/network";
import { addSuffixToAllSecrets } from "./compose/secrets";
@@ -18,13 +18,13 @@ export const randomizeComposeFile = async (
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification;
const composeData = parse(composeFile) as ComposeSpecification;
const randomSuffix = suffix || generateRandomHash();
const newComposeFile = addSuffixToAllProperties(composeData, randomSuffix);
return dump(newComposeFile);
return stringify(newComposeFile);
};
export const randomizeSpecificationFile = (

View File

@@ -4,7 +4,7 @@ import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Compose } from "@dokploy/server/services/compose";
import type { Domain } from "@dokploy/server/services/domain";
import { dump, load } from "js-yaml";
import { parse, stringify } from "yaml";
import { execAsyncRemote } from "../process/execAsync";
import {
cloneRawBitbucketRepository,
@@ -92,7 +92,7 @@ export const loadDockerCompose = async (
if (existsSync(path)) {
const yamlStr = readFileSync(path, "utf8");
const parsedConfig = load(yamlStr) as ComposeSpecification;
const parsedConfig = parse(yamlStr) as ComposeSpecification;
return parsedConfig;
}
return null;
@@ -115,7 +115,7 @@ export const loadDockerComposeRemote = async (
return null;
}
if (!stdout) return null;
const parsedConfig = load(stdout) as ComposeSpecification;
const parsedConfig = parse(stdout) as ComposeSpecification;
return parsedConfig;
} catch {
return null;
@@ -141,7 +141,7 @@ export const writeDomainsToCompose = async (
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
const composeString = dump(composeConverted, { lineWidth: 1000 });
const composeString = stringify(composeConverted, { lineWidth: 1000 });
try {
await writeFile(path, composeString, "utf8");
} catch (error) {
@@ -169,7 +169,7 @@ exit 1;
`;
}
if (compose.serverId) {
const composeString = dump(composeConverted, { lineWidth: 1000 });
const composeString = stringify(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
}
@@ -251,11 +251,15 @@ export const addDomainToCompose = async (
}
labels.unshift(...httpLabels);
if (!compose.isolatedDeployment) {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
labels.unshift("traefik.swarm.network=dokploy-network");
if (compose.composeType === "docker-compose") {
if (!labels.includes("traefik.docker.network=dokploy-network")) {
labels.unshift("traefik.docker.network=dokploy-network");
}
} else {
// Stack Case
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
labels.unshift("traefik.swarm.network=dokploy-network");
}
}
}
}
@@ -283,7 +287,7 @@ export const writeComposeFile = async (
const path = getComposePath(compose);
try {
const composeFile = dump(composeSpec, {
const composeFile = stringify(composeSpec, {
lineWidth: 1000,
});
fs.writeFileSync(path, composeFile, "utf8");

View File

@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -42,11 +43,12 @@ export const sendBuildErrorNotifications = async ({
telegram: true,
slack: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify } = notification;
const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
BuildFailedEmail({
@@ -132,6 +134,20 @@ export const sendBuildErrorNotifications = async ({
);
}
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 = [
[

View File

@@ -9,6 +9,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -43,11 +44,12 @@ export const sendBuildSuccessNotifications = async ({
telegram: true,
slack: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify } = notification;
const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -126,6 +128,19 @@ export const sendBuildSuccessNotifications = async ({
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Build Success",
"white_check_mark",
`view, Build details, ${buildLink}, clear=true;`,
`🛠Project: ${projectName}\n` +
`Application: ${applicationName}\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) =>

View File

@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -42,11 +43,12 @@ export const sendDatabaseBackupNotifications = async ({
telegram: true,
slack: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify } = notification;
const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -149,6 +151,21 @@ export const sendDatabaseBackupNotifications = async ({
);
}
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;

View File

@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -29,11 +30,12 @@ export const sendDockerCleanupNotifications = async (
telegram: true,
slack: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify } = notification;
const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -93,6 +95,16 @@ export const sendDockerCleanupNotifications = async (
);
}
if (ntfy) {
await sendNtfyNotification(
ntfy,
"Docker Cleanup",
"white_check_mark",
"",
`🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`,
);
}
if (telegram) {
await sendTelegramNotification(
telegram,

View File

@@ -8,6 +8,7 @@ import {
sendDiscordNotification,
sendEmailNotification,
sendGotifyNotification,
sendNtfyNotification,
sendSlackNotification,
sendTelegramNotification,
} from "./utils";
@@ -23,11 +24,12 @@ export const sendDokployRestartNotifications = async () => {
telegram: true,
slack: true,
gotify: true,
ntfy: true,
},
});
for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify } = notification;
const { email, discord, telegram, slack, gotify, ntfy } = notification;
if (email) {
const template = await renderAsync(
@@ -85,6 +87,20 @@ export const sendDokployRestartNotifications = async () => {
}
}
if (ntfy) {
try {
await sendNtfyNotification(
ntfy,
"Dokploy Server Restarted",
"white_check_mark",
"",
`🕒Date: ${date.toLocaleString()}`,
);
} catch (error) {
console.log(error);
}
}
if (telegram) {
try {
await sendTelegramNotification(

View File

@@ -2,6 +2,7 @@ import type {
discord,
email,
gotify,
ntfy,
slack,
telegram,
} from "@dokploy/server/db/schema";
@@ -126,3 +127,27 @@ export const sendGotifyNotification = async (
);
}
};
export const sendNtfyNotification = async (
connection: typeof ntfy.$inferInsert,
title: string,
tags: string,
actions: string,
message: string,
) => {
const response = await fetch(`${connection.serverUrl}/${connection.topic}`, {
method: "POST",
headers: {
Authorization: `Bearer ${connection.accessToken}`,
"X-Priority": connection.priority?.toString() || "3",
"X-Title": title,
"X-Tags": tags,
"X-Actions": actions,
},
body: message,
});
if (!response.ok) {
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
}
};

View File

@@ -171,7 +171,7 @@ export const cloneGithubRepository = async ({
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
"clone",
"--branch",

View File

@@ -401,7 +401,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
const {
appName,
gitlabPathNamespace,
branch,
gitlabBranch,
gitlabId,
serverId,
enableSubmodules,
@@ -429,7 +429,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
try {
const command = `
rm -rf ${outputPath};
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
`;
await execAsyncRemote(serverId, command);
} catch (error) {

View File

@@ -3,7 +3,7 @@ import path from "node:path";
import { createInterface } from "node:readline";
import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain";
import { dump, load } from "js-yaml";
import { parse, stringify } from "yaml";
import { encodeBase64 } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
@@ -40,7 +40,7 @@ export const createTraefikConfig = (appName: string) => {
},
},
};
const yamlStr = dump(config);
const yamlStr = stringify(config);
const { DYNAMIC_TRAEFIK_PATH } = paths();
fs.mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
writeFileSync(
@@ -87,7 +87,7 @@ export const loadOrCreateConfig = (appName: string): FileConfig => {
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
if (fs.existsSync(configPath)) {
const yamlStr = fs.readFileSync(configPath, "utf8");
const parsedConfig = (load(yamlStr) as FileConfig) || {
const parsedConfig = (parse(yamlStr) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
@@ -107,7 +107,7 @@ export const loadOrCreateConfigRemote = async (
if (!stdout) return fileConfig;
const parsedConfig = (load(stdout) as FileConfig) || {
const parsedConfig = (parse(stdout) as FileConfig) || {
http: { routers: {}, services: {} },
};
return parsedConfig;
@@ -248,7 +248,7 @@ export const writeTraefikConfig = (
try {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = dump(traefikConfig);
const yamlStr = stringify(traefikConfig);
fs.writeFileSync(configPath, yamlStr, "utf8");
} catch (e) {
console.error("Error saving the YAML config file:", e);
@@ -263,7 +263,7 @@ export const writeTraefikConfigRemote = async (
try {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = dump(traefikConfig);
const yamlStr = stringify(traefikConfig);
await execAsyncRemote(serverId, `echo '${yamlStr}' > ${configPath}`);
} catch (e) {
console.error("Error saving the YAML config file:", e);

View File

@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain";
import { dump, load } from "js-yaml";
import { parse, stringify } from "yaml";
import type { ApplicationNested } from "../builders";
import { execAsyncRemote } from "../process/execAsync";
import { writeTraefikConfigRemote } from "./application";
@@ -76,7 +76,7 @@ export const loadMiddlewares = <T>() => {
throw new Error(`File not found: ${configPath}`);
}
const yamlStr = readFileSync(configPath, "utf8");
const config = load(yamlStr) as T;
const config = parse(yamlStr) as T;
return config;
};
@@ -94,7 +94,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => {
console.error(`Error: ${stderr}`);
throw new Error(`File not found: ${configPath}`);
}
const config = load(stdout) as FileConfig;
const config = parse(stdout) as FileConfig;
return config;
} catch (_) {
throw new Error(`File not found: ${configPath}`);
@@ -103,7 +103,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => {
export const writeMiddleware = <T>(config: T) => {
const { DYNAMIC_TRAEFIK_PATH } = paths();
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
const newYamlContent = dump(config);
const newYamlContent = stringify(config);
writeFileSync(configPath, newYamlContent, "utf8");
};

View File

@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user";
import { dump, load } from "js-yaml";
import { parse, stringify } from "yaml";
import {
loadOrCreateConfig,
removeTraefikConfig,
@@ -79,13 +79,13 @@ export const updateLetsEncryptEmail = (newEmail: string | null) => {
const { MAIN_TRAEFIK_PATH } = paths();
const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml");
const configContent = readFileSync(configPath, "utf8");
const config = load(configContent) as MainTraefikConfig;
const config = parse(configContent) as MainTraefikConfig;
if (config?.certificatesResolvers?.letsencrypt?.acme) {
config.certificatesResolvers.letsencrypt.acme.email = newEmail;
} else {
throw new Error("Invalid Let's Encrypt configuration structure.");
}
const newYamlContent = dump(config);
const newYamlContent = stringify(config);
writeFileSync(configPath, newYamlContent, "utf8");
} catch (error) {
throw error;

View File

@@ -1,15 +1,17 @@
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduledJobs, scheduleJob } from "node-schedule";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import {
createDeploymentVolumeBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { backupVolume } from "./backup";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
import { backupVolume } from "./backup";
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
@@ -76,7 +78,20 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
await updateDeploymentStatus(deployment.deploymentId, "done");
} catch (error) {
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
const volumeBackupPath = path.join(
VOLUME_BACKUPS_PATH,
volumeBackup.appName,
);
// delete all the .tar files
const command = `rm -rf ${volumeBackupPath}/*.tar`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
console.error(error);
}
};