Merge branch 'canary' into feature/stop-grace-period-2227

This commit is contained in:
Lucas Manchine
2025-09-24 08:52:32 -03:00
111 changed files with 15260 additions and 648 deletions
@@ -331,6 +331,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,
+18 -1
View File
@@ -11,7 +11,9 @@ export const bitbucket = pgTable("bitbucket", {
.primaryKey()
.$defaultFn(() => nanoid()),
bitbucketUsername: text("bitbucketUsername"),
bitbucketEmail: text("bitbucketEmail"),
appPassword: text("appPassword"),
apiToken: text("apiToken"),
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
gitProviderId: text("gitProviderId")
.notNull()
@@ -29,7 +31,9 @@ const createSchema = createInsertSchema(bitbucket);
export const apiCreateBitbucket = createSchema.extend({
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
gitProviderId: z.string().optional(),
authId: z.string().min(1),
@@ -46,9 +50,19 @@ export const apiBitbucketTestConnection = createSchema
.extend({
bitbucketId: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
workspaceName: z.string().optional(),
apiToken: z.string().optional(),
appPassword: z.string().optional(),
})
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true });
.pick({
bitbucketId: true,
bitbucketUsername: true,
bitbucketEmail: true,
workspaceName: true,
apiToken: true,
appPassword: true,
});
export const apiFindBitbucketBranches = z.object({
owner: z.string(),
@@ -60,6 +74,9 @@ export const apiUpdateBitbucket = createSchema.extend({
bitbucketId: z.string().min(1),
name: z.string().min(1),
bitbucketUsername: z.string().optional(),
bitbucketEmail: z.string().email().optional(),
appPassword: z.string().optional(),
apiToken: z.string().optional(),
bitbucketWorkspaceName: z.string().optional(),
organizationId: z.string().optional(),
});
+12
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(),
+1
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),
});
@@ -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();
+5
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(),
+51 -20
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;
}
+18 -2
View File
@@ -68,10 +68,26 @@ export const updateBitbucket = async (
input: typeof apiUpdateBitbucket._type,
) => {
return await db.transaction(async (tx) => {
// First get the current bitbucket provider to get gitProviderId
const currentProvider = await tx.query.bitbucket.findFirst({
where: eq(bitbucket.bitbucketId, bitbucketId),
});
if (!currentProvider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Bitbucket provider not found",
});
}
const result = await tx
.update(bitbucket)
.set({
...input,
bitbucketUsername: input.bitbucketUsername,
bitbucketEmail: input.bitbucketEmail,
appPassword: input.appPassword,
apiToken: input.apiToken,
bitbucketWorkspaceName: input.bitbucketWorkspaceName,
})
.where(eq(bitbucket.bitbucketId, bitbucketId))
.returning();
@@ -83,7 +99,7 @@ export const updateBitbucket = async (
name: input.name,
organizationId: input.organizationId,
})
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
.where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId))
.returning();
}
+2 -2
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) {
@@ -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) {
+26 -2
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 &&
+6 -1
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");
}
};
+13
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({
+46 -15
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 = () => {
+1 -1
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 = (
+2 -2
View File
@@ -222,8 +222,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;
}
+2 -2
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(
@@ -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 = (
+3 -3
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 = (
+15 -11
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");
@@ -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 = [
[
@@ -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) =>
@@ -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;
@@ -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,
@@ -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(
@@ -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}`);
}
};
@@ -5,7 +5,10 @@ import type {
apiBitbucketTestConnection,
apiFindBitbucketBranches,
} from "@dokploy/server/db/schema";
import { findBitbucketById } from "@dokploy/server/services/bitbucket";
import {
type Bitbucket,
findBitbucketById,
} from "@dokploy/server/services/bitbucket";
import type { Compose } from "@dokploy/server/services/compose";
import type { InferResultType } from "@dokploy/server/types/with";
import { TRPCError } from "@trpc/server";
@@ -23,6 +26,39 @@ export type ComposeWithBitbucket = InferResultType<
{ bitbucket: true }
>;
export const getBitbucketCloneUrl = (
bitbucketProvider: {
apiToken?: string | null;
bitbucketUsername?: string | null;
appPassword?: string | null;
} | null,
repoClone: string,
) => {
if (!bitbucketProvider) {
throw new Error("Bitbucket provider is required");
}
return bitbucketProvider.apiToken
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}`
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
};
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
if (bitbucketProvider.apiToken) {
// For API tokens, use HTTP Basic auth with email and token
// According to Bitbucket docs: email:token for API calls
const email =
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
return {
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`,
};
}
// For app passwords, use HTTP Basic auth with username and app password
return {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
};
};
export const cloneBitbucketRepository = async (
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
logPath: string,
@@ -51,7 +87,7 @@ export const cloneBitbucketRepository = async (
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
try {
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
@@ -103,7 +139,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneArgs = [
@@ -153,7 +189,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code");
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
try {
const cloneCommand = `
@@ -206,7 +242,7 @@ export const getBitbucketCloneCommand = async (
const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath);
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
const cloneCommand = `
rm -rf ${outputPath};
@@ -241,9 +277,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
while (url) {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
},
headers: getBitbucketHeaders(bitbucketProvider),
});
if (!response.ok) {
@@ -279,35 +313,43 @@ export const getBitbucketBranches = async (
}
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
const { owner, repo } = input;
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100`;
let url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1`;
let allBranches: {
name: string;
commit: {
sha: string;
};
}[] = [];
try {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
},
});
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `HTTP error! status: ${response.status}`,
while (url) {
const response = await fetch(url, {
method: "GET",
headers: getBitbucketHeaders(bitbucketProvider),
});
if (!response.ok) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `HTTP error! status: ${response.status}`,
});
}
const data = await response.json();
const mappedData = data.values.map((branch: any) => {
return {
name: branch.name,
commit: {
sha: branch.target.hash,
},
};
});
allBranches = allBranches.concat(mappedData);
url = data.next || null;
}
const data = await response.json();
const mappedData = data.values.map((branch: any) => {
return {
name: branch.name,
commit: {
sha: branch.target.hash,
},
};
});
return mappedData as {
return allBranches as {
name: string;
commit: {
sha: string;
@@ -335,9 +377,7 @@ export const testBitbucketConnection = async (
try {
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
},
headers: getBitbucketHeaders(bitbucketProvider),
});
if (!response.ok) {
+84 -64
View File
@@ -362,17 +362,22 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
const limit = 30;
let allRepos = 0;
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
while (nextUrl) {
const response = await fetch(nextUrl, {
headers: {
Accept: "application/json",
Authorization: `token ${provider.accessToken}`,
// Use /user/repos to get authenticated user's repositories with pagination
let allRepos = 0;
let page = 1;
const limit = 50; // Max per page
while (true) {
const response = await fetch(
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
{
headers: {
Accept: "application/json",
Authorization: `token ${provider.accessToken}`,
},
},
});
);
if (!response.ok) {
throw new Error(
@@ -381,22 +386,18 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
}
const repos = await response.json();
allRepos += repos.data.length;
const linkHeader = response.headers.get("link");
nextUrl = "";
if (linkHeader) {
const nextLink = linkHeader
.split(",")
.find((link) => link.includes('rel="next"'));
if (nextLink) {
const matches = nextLink.match(/<([^>]+)>/);
if (matches?.[1]) {
nextUrl = matches[1];
}
}
if (!Array.isArray(repos) || repos.length === 0) {
break; // No more repositories
}
allRepos += repos.length;
// Check if there are more pages
if (repos.length < limit) {
break; // Last page (fewer results than limit)
}
page++;
}
await updateGitea(giteaId, {
@@ -418,17 +419,22 @@ export const getGiteaRepositories = async (giteaId?: string) => {
const giteaProvider = await findGiteaById(giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const limit = 30;
let allRepositories: any[] = [];
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
while (nextUrl) {
const response = await fetch(nextUrl, {
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
// Use /user/repos to get authenticated user's repositories with pagination
let allRepositories: any[] = [];
let page = 1;
const limit = 50; // Max per page
while (true) {
const response = await fetch(
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
{
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
},
});
);
if (!response.ok) {
throw new TRPCError({
@@ -437,23 +443,19 @@ export const getGiteaRepositories = async (giteaId?: string) => {
});
}
const result = await response.json();
allRepositories = [...allRepositories, ...result.data];
const linkHeader = response.headers.get("link");
nextUrl = "";
if (linkHeader) {
const nextLink = linkHeader
.split(",")
.find((link) => link.includes('rel="next"'));
if (nextLink) {
const matches = nextLink.match(/<([^>]+)>/);
if (matches?.[1]) {
nextUrl = matches[1];
}
}
const repos = await response.json();
if (!Array.isArray(repos) || repos.length === 0) {
break; // No more repositories
}
allRepositories = [...allRepositories, ...repos];
// Check if there are more pages
if (repos.length < limit) {
break; // Last page (fewer results than limit)
}
page++;
}
return (
@@ -482,25 +484,43 @@ export const getGiteaBranches = async (input: {
const giteaProvider = await findGiteaById(input.giteaId);
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
const response = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
});
// Handle pagination for branches
let allBranches: any[] = [];
let page = 1;
const limit = 50; // Max per page
if (!response.ok) {
throw new Error(`Failed to fetch branches: ${response.statusText}`);
while (true) {
const response = await fetch(
`${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches?page=${page}&limit=${limit}`,
{
headers: {
Accept: "application/json",
Authorization: `token ${giteaProvider.accessToken}`,
},
},
);
if (!response.ok) {
throw new Error(`Failed to fetch branches: ${response.statusText}`);
}
const branches = await response.json();
if (!Array.isArray(branches) || branches.length === 0) {
break; // No more branches
}
allBranches = [...allBranches, ...branches];
// Check if there are more pages
if (branches.length < limit) {
break; // Last page (fewer results than limit)
}
page++;
}
const branches = await response.json();
if (!branches) {
return [];
}
return branches?.map((branch: any) => ({
return allBranches?.map((branch: any) => ({
id: branch.name,
name: branch.name,
commit: {
@@ -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",
@@ -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) {
@@ -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);
@@ -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");
};
@@ -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;
@@ -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);
}
};