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

This commit is contained in:
Lucas Manchine
2025-09-05 12:34:17 -03:00
427 changed files with 35912 additions and 4680 deletions

View File

@@ -1,7 +1,7 @@
import { paths } from "@dokploy/server/constants";
import { findAdmin } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync";
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";

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

@@ -4,6 +4,7 @@ import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runComposeBackup = async (
compose: Compose,
backup: BackupSchedule,
) => {
const { projectId, name } = compose;
const project = await findProjectById(projectId);
const { environmentId, name } = compose;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix, databaseType } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,6 +4,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mariadb } from "@dokploy/server/services/mariadb";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runMariadbBackup = async (
mariadb: Mariadb,
backup: BackupSchedule,
) => {
const { projectId, name } = mariadb;
const project = await findProjectById(projectId);
const { environmentId, name } = mariadb;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,14 +4,16 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Mongo } from "@dokploy/server/services/mongo";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
const { projectId, name } = mongo;
const project = await findProjectById(projectId);
const { environmentId, name } = mongo;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,14 +4,16 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { MySql } from "@dokploy/server/services/mysql";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
const { projectId, name } = mysql;
const project = await findProjectById(projectId);
const { environmentId, name } = mysql;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;

View File

@@ -4,6 +4,7 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import type { Postgres } from "@dokploy/server/services/postgres";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
@@ -13,8 +14,9 @@ export const runPostgresBackup = async (
postgres: Postgres,
backup: BackupSchedule,
) => {
const { name, projectId } = postgres;
const project = await findProjectById(projectId);
const { name, environmentId } = postgres;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const deployment = await createDeploymentBackup({
backupId: backup.backupId,

View File

@@ -1,7 +1,7 @@
import { logger } from "@dokploy/server/lib/logger";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Destination } from "@dokploy/server/services/destination";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { keepLatestNBackups } from ".";
import { runComposeBackup } from "./compose";
import { runMariadbBackup } from "./mariadb";

View File

@@ -80,7 +80,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
writeStream.write("Zipped database and filesystem\n");
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
writeStream.write(`Running command: ${uploadCommand}\n`);
writeStream.write("Running command to upload backup to S3\n");
await execAsync(uploadCommand);
writeStream.write("Uploaded backup to S3 ✅\n");
writeStream.end();

View File

@@ -22,7 +22,7 @@ import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType<
"compose",
{ project: true; mounts: true; domains: true }
{ environment: { with: { project: true } }; mounts: true; domains: true }
>;
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
@@ -72,7 +72,10 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => {
NODE_ENV: process.env.NODE_ENV,
PATH: process.env.PATH,
...(composeType === "stack" && {
...getEnviromentVariablesObject(compose.env, compose.project.env),
...getEnviromentVariablesObject(
compose.env,
compose.environment.project.env,
),
}),
},
},
@@ -202,7 +205,8 @@ const createEnvFile = (compose: ComposeNested) => {
const envFileContent = prepareEnvironmentVariables(
envContent,
compose.project.env,
compose.environment.project.env,
compose.environment.env,
).join("\n");
if (!existsSync(dirname(envFilePath))) {
@@ -232,7 +236,8 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
const envFileContent = prepareEnvironmentVariables(
envContent,
compose.project.env,
compose.environment.project.env,
compose.environment.env,
).join("\n");
const encodedContent = encodeBase64(envFileContent);
@@ -247,7 +252,7 @@ const getExportEnvCommand = (compose: ComposeNested) => {
const envVars = getEnviromentVariablesObject(
compose.env,
compose.project.env,
compose.environment.project.env,
);
const exports = Object.entries(envVars)
.map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)

View File

@@ -1,11 +1,11 @@
import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
import type { ApplicationNested } from ".";
import {
getBuildAppDirectory,
getDockerContextPath,
} from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
import { createEnvFile, createEnvFileCommand } from "./utils";
export const buildCustomDocker = async (
@@ -28,7 +28,8 @@ export const buildCustomDocker = async (
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath = getDockerContextPath(application);
@@ -51,7 +52,12 @@ export const buildCustomDocker = async (
as it could be publicly exposed.
*/
if (!publishDirectory) {
createEnvFile(dockerFilePath, env, application.project.env);
createEnvFile(
dockerFilePath,
env,
application.environment.project.env,
application.environment.env,
);
}
await spawnAsync(
@@ -92,7 +98,8 @@ export const getDockerCommand = (
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath =
@@ -121,7 +128,8 @@ export const getDockerCommand = (
command += createEnvFileCommand(
dockerFilePath,
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
}

View File

@@ -1,8 +1,8 @@
import type { WriteStream } from "node:fs";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildHeroku = async (
@@ -13,7 +13,8 @@ export const buildHeroku = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
try {
const args = [
@@ -53,7 +54,8 @@ export const getHerokuCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const args = [

View File

@@ -30,7 +30,7 @@ export type ApplicationNested = InferResultType<
redirects: true;
ports: true;
registry: true;
project: true;
environment: { with: { project: true } };
}
>;
@@ -149,7 +149,8 @@ export const mechanizeDockerContainer = async (
const filesMount = generateFileMounts(appName, application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const image = getImageName(application);

View File

@@ -1,14 +1,14 @@
import { type WriteStream, existsSync, mkdirSync } from "node:fs";
import { existsSync, mkdirSync, type WriteStream } from "node:fs";
import path from "node:path";
import {
buildStatic,
getStaticCommand,
} from "@dokploy/server/utils/builders/static";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
export const buildNixpacks = async (
application: ApplicationNested,
@@ -20,7 +20,8 @@ export const buildNixpacks = async (
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const writeToStream = (data: string) => {
@@ -101,7 +102,8 @@ export const getNixpacksCommand = (
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const args = ["build", buildAppDirectory, "--name", appName];

View File

@@ -1,8 +1,8 @@
import type { WriteStream } from "node:fs";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { ApplicationNested } from ".";
export const buildPaketo = async (
application: ApplicationNested,
@@ -12,7 +12,8 @@ export const buildPaketo = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
try {
const args = [
@@ -52,7 +53,8 @@ export const getPaketoCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
const args = [

View File

@@ -26,7 +26,8 @@ export const buildRailpack = async (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
try {
@@ -123,7 +124,8 @@ export const getRailpackCommand = (
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables(
env,
application.project.env,
application.environment.project.env,
application.environment.env,
);
// Prepare command

View File

@@ -3,9 +3,9 @@ import {
buildCustomDocker,
getDockerCommand,
} from "@dokploy/server/utils/builders/docker-file";
import type { ApplicationNested } from ".";
import { createFile, getCreateFileCommand } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
const nginxSpaConfig = `
worker_processes 1;

View File

@@ -6,14 +6,17 @@ export const createEnvFile = (
directory: string,
env: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const envFilePath = join(dirname(directory), ".env");
if (!existsSync(dirname(envFilePath))) {
mkdirSync(dirname(envFilePath), { recursive: true });
}
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
"\n",
);
const envFileContent = prepareEnvironmentVariables(
env,
projectEnv,
environmentEnv,
).join("\n");
writeFileSync(envFilePath, envFileContent);
};
@@ -21,10 +24,13 @@ export const createEnvFileCommand = (
directory: string,
env: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
"\n",
);
const envFileContent = prepareEnvironmentVariables(
env,
projectEnv,
environmentEnv,
).join("\n");
const encodedContent = encodeBase64(envFileContent || "");
const envFilePath = join(dirname(directory), ".env");

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MariadbNested = InferResultType<
"mariadb",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMariadb = async (mariadb: MariadbNested) => {
const {
@@ -54,7 +54,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultMariadbEnv,
mariadb.project.env,
mariadb.environment.project.env,
mariadb.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MongoNested = InferResultType<
"mongo",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMongo = async (mongo: MongoNested) => {
@@ -102,7 +102,8 @@ ${command ?? "wait $MONGOD_PID"}`;
const envVariables = prepareEnvironmentVariables(
defaultMongoEnv,
mongo.project.env,
mongo.environment.project.env,
mongo.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type MysqlNested = InferResultType<
"mysql",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildMysql = async (mysql: MysqlNested) => {
@@ -60,7 +60,8 @@ export const buildMysql = async (mysql: MysqlNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultMysqlEnv,
mysql.project.env,
mysql.environment.project.env,
mysql.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type PostgresNested = InferResultType<
"postgres",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildPostgres = async (postgres: PostgresNested) => {
const {
@@ -53,7 +53,8 @@ export const buildPostgres = async (postgres: PostgresNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultPostgresEnv,
postgres.project.env,
postgres.environment.project.env,
postgres.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -13,8 +13,7 @@ import { deployPostgres } from "@dokploy/server/services/postgres";
import { deployRedis } from "@dokploy/server/services/redis";
import { eq } from "drizzle-orm";
import { removeService } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { execAsync } from "../process/execAsync";
import { execAsync, execAsyncRemote } from "../process/execAsync";
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";

View File

@@ -12,7 +12,7 @@ import { getRemoteDocker } from "../servers/remote-docker";
export type RedisNested = InferResultType<
"redis",
{ mounts: true; project: true }
{ mounts: true; environment: { with: { project: true } } }
>;
export const buildRedis = async (redis: RedisNested) => {
const {
@@ -51,7 +51,8 @@ export const buildRedis = async (redis: RedisNested) => {
});
const envVariables = prepareEnvironmentVariables(
defaultRedisEnv,
redis.project.env,
redis.environment.project.env,
redis.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);

View File

@@ -1,8 +1,14 @@
import { findComposeById } from "@dokploy/server/services/compose";
import { dump, load } from "js-yaml";
import { dump } from "js-yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network";
import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume";
import {
cloneCompose,
cloneComposeRemote,
loadDockerCompose,
loadDockerComposeRemote,
} from "./domain";
import type { ComposeSpecification } from "./types";
export const addAppNameToPreventCollision = (
@@ -24,16 +30,34 @@ export const randomizeIsolatedDeploymentComposeFile = async (
suffix?: string,
) => {
const compose = await findComposeById(composeId);
const composeFile = compose.composeFile;
const composeData = load(composeFile) as ComposeSpecification;
if (compose.serverId) {
await cloneComposeRemote(compose);
} else {
await cloneCompose(compose);
}
let composeData: ComposeSpecification | null;
if (compose.serverId) {
composeData = await loadDockerComposeRemote(compose);
} else {
composeData = await loadDockerCompose(compose);
}
if (!composeData) {
throw new Error("Compose data not found");
}
const randomSuffix = suffix || compose.appName || generateRandomHash();
const newComposeFile = addAppNameToPreventCollision(
composeData,
randomSuffix,
compose.isolatedDeploymentsVolume,
);
const newComposeFile = compose.isolatedDeployment
? addAppNameToPreventCollision(
composeData,
randomSuffix,
compose.isolatedDeploymentsVolume,
)
: composeData;
return dump(newComposeFile);
};

View File

@@ -6,6 +6,7 @@
import _ from "lodash";
import type { ComposeSpecification, DefinitionsService } from "../types";
type DependsOnObject = NonNullable<
Exclude<DefinitionsService["depends_on"], string[]> extends infer T
? { [K in keyof T]: T[K] }

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

@@ -259,20 +259,51 @@ export const removeService = async (
export const prepareEnvironmentVariables = (
serviceEnv: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const projectVars = parse(projectEnv ?? "");
const environmentVars = parse(environmentEnv ?? "");
const serviceVars = parse(serviceEnv ?? "");
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
let resolvedValue = value;
// Replace project variables
if (projectVars) {
resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
if (projectVars[ref] !== undefined) {
return projectVars[ref];
}
throw new Error(`Invalid project environment variable: project.${ref}`);
});
resolvedValue = resolvedValue.replace(
/\$\{\{project\.(.*?)\}\}/g,
(_, ref) => {
if (projectVars[ref] !== undefined) {
return projectVars[ref];
}
throw new Error(
`Invalid project environment variable: project.${ref}`,
);
},
);
}
// Replace environment variables
if (environmentVars) {
resolvedValue = resolvedValue.replace(
/\$\{\{environment\.(.*?)\}\}/g,
(_, ref) => {
if (environmentVars[ref] !== undefined) {
return environmentVars[ref];
}
throw new Error(`Invalid environment variable: environment.${ref}`);
},
);
}
// Replace self-references (service variables)
resolvedValue = resolvedValue.replace(/\$\{\{(.*?)\}\}/g, (_, ref) => {
if (serviceVars[ref] !== undefined) {
return serviceVars[ref];
}
throw new Error(`Invalid service environment variable: ${ref}`);
});
return `${key}=${resolvedValue}`;
});
@@ -293,8 +324,9 @@ export const parseEnvironmentKeyValuePair = (
export const getEnviromentVariablesObject = (
input: string | null,
projectEnv?: string | null,
environmentEnv?: string | null,
) => {
const envs = prepareEnvironmentVariables(input, projectEnv);
const envs = prepareEnvironmentVariables(input, projectEnv, environmentEnv);
const jsonObject: Record<string, string> = {};

View File

@@ -1,6 +1,5 @@
import * as fs from "node:fs/promises";
import { execAsync, sleep } from "../utils/process/execAsync";
import { execAsyncRemote } from "../utils/process/execAsync";
import { execAsync, execAsyncRemote, sleep } from "../utils/process/execAsync";
interface GPUInfo {
driverInstalled: boolean;
@@ -186,7 +185,7 @@ const checkCudaSupport = async (serverId?: string) => {
? await execAsyncRemote(serverId, cudaCommand)
: await execAsync(cudaCommand);
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d\.]+)/);
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d.]+)/);
cudaVersion = cudaMatch ? cudaMatch[1] : undefined;
cudaSupport = !!cudaVersion;
} catch (error) {

View File

@@ -42,7 +42,9 @@ export const buildDocker = async (
await mechanizeDockerContainer(application);
writeStream.write("\nDocker Deployed: ✅\n");
} catch (error) {
writeStream.write("❌ Error");
writeStream.write(
`❌ Error: ${error instanceof Error ? error.message : String(error)}`,
);
throw error;
} finally {
writeStream.end();

View File

@@ -237,10 +237,10 @@ const sanitizeRepoPathSSH = (input: string) => {
/^\s*/,
/(?:(?<proto>[a-z]+):\/\/)?/,
/(?:(?<user>[a-z_][a-z0-9_-]+)@)?/,
/(?<domain>[^\s\/\?#:]+)/,
/(?<domain>[^\s/?#:]+)/,
/(?::(?<port>[0-9]{1,5}))?/,
/(?:[\/:](?<owner>[^\s\/\?#:]+))?/,
/(?:[\/:](?<repo>(?:[^\s\?#:.]|\.(?!git\/?\s*$))+))/,
/(?:[/:](?<owner>[^\s/?#:]+))?/,
/(?:[/:](?<repo>(?:[^\s?#:.]|\.(?!git\/?\s*$))+))/,
/(?:.git)?\/?\s*$/,
]
.map((r) => r.source)

View File

@@ -1,17 +1,16 @@
import { createWriteStream } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { findGithubById, type Github } from "@dokploy/server/services/github";
import type { InferResultType } from "@dokploy/server/types/with";
import { createAppAuth } from "@octokit/auth-app";
import { TRPCError } from "@trpc/server";
import { Octokit } from "octokit";
import { recreateDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import type { apiFindGithubBranches } from "@dokploy/server/db/schema";
import type { Compose } from "@dokploy/server/services/compose";
import { type Github, findGithubById } from "@dokploy/server/services/github";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
export const authGithub = (githubProvider: Github): Octokit => {
if (!haveGithubRequirements(githubProvider)) {

View File

@@ -316,7 +316,7 @@ export const getGitlabBranches = async (input: {
while (true) {
const branchesResponse = await fetch(
`https://gitlab.com/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
`${gitlabProvider.gitlabUrl}/api/v4/projects/${input.id}/repository/branches?page=${page}&per_page=${perPage}`,
{
headers: {
Authorization: `Bearer ${gitlabProvider.accessToken}`,

View File

@@ -1,6 +1,6 @@
export { restorePostgresBackup } from "./postgres";
export { restoreMySqlBackup } from "./mysql";
export { restoreComposeBackup } from "./compose";
export { restoreMariadbBackup } from "./mariadb";
export { restoreMongoBackup } from "./mongo";
export { restoreMySqlBackup } from "./mysql";
export { restorePostgresBackup } from "./postgres";
export { restoreWebServerBackup } from "./web-server";
export { restoreComposeBackup } from "./compose";

View File

@@ -5,10 +5,10 @@ import type { Schedule } from "@dokploy/server/db/schema/schedule";
import {
createDeploymentSchedule,
updateDeployment,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule";
import { getComposeContainer, getServiceContainer } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";

View File

@@ -1,5 +1,4 @@
import fs, { writeFileSync } from "node:fs";
import { createReadStream } from "node:fs";
import fs, { createReadStream, writeFileSync } from "node:fs";
import path from "node:path";
import { createInterface } from "node:readline";
import { paths } from "@dokploy/server/constants";

View File

@@ -1,6 +1,7 @@
export * from "./backup";
export * from "./restore";
export * from "./utils";
import { volumeBackups } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { db } from "../../db/index";

View File

@@ -1,12 +1,15 @@
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduleJob, scheduledJobs } from "node-schedule";
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");