Merge branch 'canary' into feat/ntfy

This commit is contained in:
Mauricio Siu
2025-08-28 19:07:53 -06:00
32 changed files with 7246 additions and 313 deletions

View File

@@ -32,7 +32,7 @@ export const aiRelations = relations(ai, ({ one }) => ({
const createSchema = createInsertSchema(ai, {
name: z.string().min(1, { message: "Name is required" }),
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
apiKey: z.string().min(1, { message: "API Key is required" }),
apiKey: z.string(),
model: z.string().min(1, { message: "Model is required" }),
isEnabled: z.boolean().optional(),
});

View File

@@ -79,6 +79,7 @@ export const applications = pgTable("application", {
previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"),
previewLabels: text("previewLabels").array(),
previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000),
previewHttps: boolean("previewHttps").notNull().default(false),
@@ -308,6 +309,7 @@ const createSchema = createInsertSchema(applications, {
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
});

View File

@@ -70,7 +70,7 @@ export const findApplicationByPreview = async (applicationId: string) => {
if (!application) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Applicationnot found",
message: "Application not found",
});
}
return application;
@@ -78,35 +78,41 @@ export const findApplicationByPreview = async (applicationId: string) => {
export const removePreviewDeployment = async (previewDeploymentId: string) => {
try {
const application = await findApplicationByPreview(previewDeploymentId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const deployment = await db
.delete(previewDeployments)
.where(eq(previewDeployments.previewDeploymentId, previewDeploymentId))
.returning();
const application = await findApplicationById(
previewDeployment.applicationId,
);
application.appName = previewDeployment.appName;
const cleanupOperations = [
async () =>
await removeService(application?.appName, application?.serverId),
async () =>
await removeDeploymentsByPreviewDeploymentId(
previewDeployment,
application.serverId,
application?.serverId,
),
async () =>
await removeDirectoryCode(application.appName, application.serverId),
await removeDirectoryCode(application?.appName, application?.serverId),
async () =>
await removeTraefikConfig(application.appName, application.serverId),
await removeTraefikConfig(application?.appName, application?.serverId),
async () =>
await removeService(application?.appName, application.serverId),
await db
.delete(previewDeployments)
.where(
eq(previewDeployments.previewDeploymentId, previewDeploymentId),
)
.returning(),
];
for (const operation of cleanupOperations) {
try {
await operation();
} catch {}
} catch (error) {
console.error(error);
}
}
return deployment[0];
return previewDeployment;
} catch (error) {
const message =
error instanceof Error

View File

@@ -13,7 +13,7 @@ export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.1.2";
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0";
export interface TraefikOptions {
env?: string[];

View File

@@ -5,17 +5,16 @@ import { createDeepInfra } from "@ai-sdk/deepinfra";
import { createMistral } from "@ai-sdk/mistral";
import { createOpenAI } from "@ai-sdk/openai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { createOllama } from "ollama-ai-provider";
import { createOllama } from "ai-sdk-ollama";
function getProviderName(apiUrl: string) {
export function getProviderName(apiUrl: string) {
if (apiUrl.includes("api.openai.com")) return "openai";
if (apiUrl.includes("azure.com")) return "azure";
if (apiUrl.includes("api.anthropic.com")) return "anthropic";
if (apiUrl.includes("api.cohere.ai")) return "cohere";
if (apiUrl.includes("api.perplexity.ai")) return "perplexity";
if (apiUrl.includes("api.mistral.ai")) return "mistral";
if (apiUrl.includes("localhost:11434") || apiUrl.includes("ollama"))
return "ollama";
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
return "custom";
}

View File

@@ -254,6 +254,9 @@ export const addDomainToCompose = async (
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");
}
}
}

View File

@@ -273,6 +273,14 @@ export const prepareEnvironmentVariables = (
throw new Error(`Invalid project environment variable: project.${ref}`);
});
}
resolvedValue = resolvedValue.replace(/\$\{\{(.*?)\}\}/g, (_, ref) => {
if (serviceVars[ref] !== undefined) {
return serviceVars[ref];
}
throw new Error(`Invalid service environment variable: ${ref}`);
});
return `${key}=${resolvedValue}`;
});

View File

@@ -2,11 +2,14 @@ import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduledJobs, scheduleJob } from "node-schedule";
import {
createDeploymentVolumeBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import {
execAsync,
execAsyncRemote,
updateDeploymentStatus,
} from "../..";
} from "@dokploy/server/utils/process/execAsync";
import { backupVolume } from "./backup";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
@@ -20,6 +23,33 @@ export const removeVolumeBackupJob = async (volumeBackupId: string) => {
currentJob?.cancel();
};
const cleanupOldVolumeBackups = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
serverId?: string | null,
) => {
const { keepLatestCount, destination, prefix, volumeName } = volumeBackup;
if (!keepLatestCount) return;
try {
const rcloneFlags = getS3Credentials(destination);
const normalizedPrefix = normalizeS3Path(prefix);
const backupFilesPath = `:s3:${destination.bucket}/${normalizedPrefix}`;
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} --include \"${volumeName}-*.tar\" :s3:${destination.bucket}/${normalizedPrefix}`;
const sortAndPick = `sort -r | tail -n +$((${keepLatestCount}+1)) | xargs -I{}`;
const deleteCommand = `rclone delete ${rcloneFlags.join(" ")} ${backupFilesPath}{}`;
const fullCommand = `${listCommand} | ${sortAndPick} ${deleteCommand}`;
if (serverId) {
await execAsyncRemote(serverId, fullCommand);
} else {
await execAsync(fullCommand);
}
} catch (error) {
console.error("Volume backup retention error", error);
}
};
export const runVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
const serverId =
@@ -40,6 +70,10 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
await execAsync(commandWithLog);
}
if (volumeBackup.keepLatestCount && volumeBackup.keepLatestCount > 0) {
await cleanupOldVolumeBackups(volumeBackup, serverId);
}
await updateDeploymentStatus(deployment.deploymentId, "done");
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");