mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-20 14:45:42 +02:00
Merge branch 'canary' into feature/stop-grace-period-2227
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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....");
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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} && \
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user