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

This commit is contained in:
Mauricio Siu
2025-08-02 19:37:24 -06:00
71 changed files with 1033 additions and 644 deletions

View File

@@ -323,6 +323,7 @@ export const apiUpdateWebServerMonitoring = z.object({
export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(),
currentPassword: z.string().optional(),
name: z.string().optional(),
metricsConfig: z
.object({
server: z.object({

View File

@@ -237,6 +237,7 @@ export const deployApplication = async ({
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
applicationName: application.name,
@@ -370,8 +371,9 @@ export const deployRemoteApplication = async ({
domains: application.domains,
});
} catch (error) {
// @ts-ignore
const encodedContent = encodeBase64(error?.message);
const errorMessage = error instanceof Error ? error.message : String(error);
const encodedContent = encodeBase64(errorMessage);
await execAsyncRemote(
application.serverId,
@@ -383,12 +385,12 @@ export const deployRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({
projectName: application.project.name,
applicationName: application.name,
applicationType: "application",
// @ts-ignore
errorMessage: error?.message || "Error building",
errorMessage: `Please check the logs for details: ${errorMessage}`,
buildLink,
organizationId: application.project.organizationId,
});

View File

@@ -1,8 +1,12 @@
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import {
type apiCreateCompose,
buildAppName,
cleanAppName,
compose,
} from "@dokploy/server/db/schema";
import {
buildCompose,
getBuildComposeCommand,
@@ -516,19 +520,20 @@ export const startCompose = async (composeId: string) => {
const compose = await findComposeById(composeId);
try {
const { COMPOSE_PATH } = paths(!!compose.serverId);
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const path =
compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath;
const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`;
if (compose.composeType === "docker-compose") {
if (compose.serverId) {
await execAsyncRemote(
compose.serverId,
`cd ${join(
COMPOSE_PATH,
compose.appName,
"code",
)} && docker compose -p ${compose.appName} up -d`,
`cd ${projectPath} && ${baseCommand}`,
);
} else {
await execAsync(`docker compose -p ${compose.appName} up -d`, {
cwd: join(COMPOSE_PATH, compose.appName, "code"),
await execAsync(baseCommand, {
cwd: projectPath,
});
}
}

View File

@@ -441,13 +441,13 @@ export const getNodeApplications = async (serverId?: string) => {
};
export const getApplicationInfo = async (
appName: string,
appNames: string[],
serverId?: string,
) => {
try {
let stdout = "";
let stderr = "";
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`;
const command = `docker service ps ${appNames.join(" ")} --format '{{json .}}' --no-trunc`;
if (serverId) {
const result = await execAsyncRemote(serverId, command);

View File

@@ -2,18 +2,22 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db";
import {
type ServiceType,
type apiCreateMount,
mounts,
type ServiceType,
} from "@dokploy/server/db/schema";
import {
createFile,
encodeBase64,
getCreateFileCommand,
} from "@dokploy/server/utils/docker/utils";
import { removeFileOrDirectory } from "@dokploy/server/utils/filesystem/directory";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { type SQL, eq, sql } from "drizzle-orm";
import { eq, type SQL, sql } from "drizzle-orm";
export type Mount = typeof mounts.$inferSelect;
@@ -123,7 +127,7 @@ export const updateMount = async (
mountId: string,
mountData: Partial<Mount>,
) => {
return await db.transaction(async (tx) => {
const mount = await db.transaction(async (tx) => {
const mount = await tx
.update(mounts)
.set({
@@ -140,13 +144,13 @@ export const updateMount = async (
});
}
if (mount.type === "file") {
await deleteFileMount(mountId);
await createFileMount(mountId);
}
return await findMountById(mountId);
});
if (mount.type === "file") {
await updateFileMount(mountId);
}
return mount;
};
export const findMountsByApplicationId = async (
@@ -198,6 +202,26 @@ export const deleteMount = async (mountId: string) => {
return deletedMount[0];
};
export const updateFileMount = async (mountId: string) => {
const mount = await findMountById(mountId);
if (!mount || !mount.filePath) return;
const basePath = await getBaseFilesPath(mountId);
const fullPath = path.join(basePath, mount.filePath);
try {
const serverId = await getServerId(mount);
const encodedContent = encodeBase64(mount.content || "");
const command = `echo "${encodedContent}" | base64 -d > ${fullPath}`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} catch {
console.log("Error updating file mount");
}
};
export const deleteFileMount = async (mountId: string) => {
const mount = await findMountById(mountId);
if (!mount.filePath) return;

View File

@@ -6,18 +6,17 @@ import {
} from "@dokploy/server/services/deployment";
import { findServerById } from "@dokploy/server/services/server";
import {
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
TRAEFIK_HTTP3_PORT,
TRAEFIK_PORT,
TRAEFIK_SSL_PORT,
TRAEFIK_VERSION,
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
} from "@dokploy/server/setup/traefik-setup";
import slug from "slugify";
import { Client } from "ssh2";
import { recreateDirectory } from "../utils/filesystem/directory";
import slug from "slugify";
export const slugify = (text: string | undefined) => {
if (!text) {
return "";
@@ -609,7 +608,7 @@ const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=0.0.64
export RAILPACK_VERSION=0.2.2
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi

View File

@@ -17,7 +17,7 @@ export const runComposeBackup = async (
const project = await findProjectById(projectId);
const { prefix, databaseType } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -1,7 +1,11 @@
import path from "node:path";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server";
import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
import { startLogCleanup } from "../access-log/handler";
import {
cleanUpDockerBuilder,
cleanUpSystemPrune,
@@ -11,11 +15,6 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");

View File

@@ -14,7 +14,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const project = await findProjectById(projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.dump.gz`;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -1,7 +1,6 @@
import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
@@ -9,6 +8,7 @@ import {
import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256");
@@ -75,7 +75,7 @@ export const buildRailpack = async (
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.64",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.2.2",
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",

View File

@@ -313,40 +313,46 @@ export const createDomainLabels = (
`traefik.http.routers.${routerName}.service=${routerName}`,
];
// Validate stripPath - it should only be used when path is defined and not "/"
if (stripPath) {
if (!path || path === "/") {
console.warn(
`stripPath is enabled but path is not defined or is "/" for domain ${host}`,
);
} else {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Collect middlewares for this router
const middlewares: string[] = [];
// Add HTTPS redirect for web entrypoint (must be first)
if (entrypoint === "web" && https) {
middlewares.push("redirect-to-https@file");
}
// Add stripPath middleware if needed
if (stripPath && path && path !== "/") {
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
// Only define middleware once (on web entrypoint)
if (entrypoint === "web") {
labels.push(
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
);
}
middlewares.push(middlewareName);
}
// Validate internalPath - ensure it's a valid path format
if (internalPath && internalPath !== "/") {
if (!internalPath.startsWith("/")) {
console.warn(
`internalPath "${internalPath}" should start with "/" and not be empty for domain ${host}`,
);
} else {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
// Add internalPath middleware if needed
if (internalPath && internalPath !== "/" && internalPath.startsWith("/")) {
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
// Only define middleware once (on web entrypoint)
if (entrypoint === "web") {
labels.push(
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
);
}
middlewares.push(middlewareName);
}
if (entrypoint === "web" && https) {
// Apply middlewares to router if any exist
if (middlewares.length > 0) {
labels.push(
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
`traefik.http.routers.${routerName}.middlewares=${middlewares.join(",")}`,
);
}
// Add TLS configuration for websecure
if (entrypoint === "websecure") {
if (certificateType === "letsencrypt") {
labels.push(

View File

@@ -65,6 +65,8 @@ export const sendBuildErrorNotifications = async ({
const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245,
@@ -101,7 +103,7 @@ export const sendBuildErrorNotifications = async ({
},
{
name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``,
value: `\`\`\`${truncatedErrorMessage}\`\`\``,
},
{
name: decorate("`🧷`", "Build Link"),

View File

@@ -4,8 +4,8 @@ import { paths } from "@dokploy/server/constants";
import type { apiGitlabTestConnection } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import {
type Gitlab,
findGitlabById,
type Gitlab,
updateGitlab,
} from "@dokploy/server/services/gitlab";
import type { InferResultType } from "@dokploy/server/types/with";
@@ -310,22 +310,43 @@ export const getGitlabBranches = async (input: {
const gitlabProvider = await findGitlabById(input.gitlabId);
const branchesResponse = await fetch(
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
const allBranches = [];
let page = 1;
const perPage = 100; // GitLab's max per page is 100
if (!branchesResponse.ok) {
throw new Error(`Failed to fetch branches: ${branchesResponse.statusText}`);
while (true) {
const branchesResponse = await fetch(
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,
},
},
);
if (!branchesResponse.ok) {
throw new Error(
`Failed to fetch branches: ${branchesResponse.statusText}`,
);
}
const branches = await branchesResponse.json();
if (branches.length === 0) {
break;
}
allBranches.push(...branches);
page++;
// Check if we've reached the total using headers (optional optimization)
const total = branchesResponse.headers.get("x-total");
if (total && allBranches.length >= Number.parseInt(total)) {
break;
}
}
const branches = await branchesResponse.json();
return branches as {
return allBranches as {
id: string;
name: string;
commit: {

View File

@@ -81,7 +81,7 @@ const getMongoSpecificCommand = (
backupFile: string,
): string => {
const tempDir = "/tmp/dokploy-restore";
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
const fileName = backupFile.split("/").pop() || "backup.sql.gz";
const decompressedName = fileName.replace(".gz", "");
return `
rm -rf ${tempDir} && \

View File

@@ -2,8 +2,7 @@ import path from "node:path";
import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils";
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
@@ -37,6 +36,9 @@ export const backupVolume = async (
echo "Starting upload to S3..."
${rcloneCommand}
echo "Upload to S3 done ✅"
echo "Cleaning up local backup file..."
rm "${volumeBackupPath}/${backupFileName}"
echo "Local backup file cleaned up ✅"
`;
if (!turnOff) {