diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index 862cd43f2..77c98bb11 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -4,6 +4,7 @@ import { updateVolumeBackup, removeVolumeBackup, createVolumeBackup, + runVolumeBackup, } from "@dokploy/server"; import { createVolumeBackupSchema, @@ -79,7 +80,12 @@ export const volumeBackupsRouter = createTRPCRouter({ runManually: protectedProcedure .input(z.object({ volumeBackupId: z.string().min(1) })) - .mutation(async () => { - // return await runVolumeBackupManually(input.volumeBackupId); + .mutation(async ({ input }) => { + try { + return await runVolumeBackup(input.volumeBackupId); + } catch (error) { + console.error(error); + return false; + } }), }); diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index 461d40f5e..7ffacc7ca 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -24,5 +24,6 @@ export const paths = (isServer = false) => { MONITORING_PATH: `${BASE_PATH}/monitoring`, REGISTRY_PATH: `${BASE_PATH}/registry`, SCHEDULES_PATH: `${BASE_PATH}/schedules`, + VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`, }; }; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 624af87ec..dc86c8268 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -63,6 +63,8 @@ export * from "./utils/notifications/utils"; export * from "./utils/notifications/docker-cleanup"; export * from "./utils/notifications/server-threshold"; +export * from "./utils/volume-backups/utils"; + export * from "./utils/builders/index"; export * from "./utils/builders/compose"; export * from "./utils/builders/docker-file"; diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index aff6d7f15..f03a78bfb 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -476,11 +476,11 @@ export const createDeploymentVolumeBackup = async ( "volumeBackup", serverId, ); - const { SCHEDULES_PATH } = paths(!!serverId); + const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${volumeBackup.appName}-${formattedDateTime}.log`; const logFilePath = path.join( - SCHEDULES_PATH, + VOLUME_BACKUPS_PATH, volumeBackup.appName, fileName, ); @@ -489,15 +489,18 @@ export const createDeploymentVolumeBackup = async ( const server = await findServerById(serverId); const command = ` - mkdir -p ${SCHEDULES_PATH}/${volumeBackup.appName}; + mkdir -p ${VOLUME_BACKUPS_PATH}/${volumeBackup.appName}; echo "Initializing volume backup" >> ${logFilePath}; `; await execAsyncRemote(server.serverId, command); } else { - await fsPromises.mkdir(path.join(SCHEDULES_PATH, volumeBackup.appName), { - recursive: true, - }); + await fsPromises.mkdir( + path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName), + { + recursive: true, + }, + ); await fsPromises.writeFile(logFilePath, "Initializing volume backup\n"); } diff --git a/packages/server/src/services/volume-backups.ts b/packages/server/src/services/volume-backups.ts index aad2ef225..0ae9717ca 100644 --- a/packages/server/src/services/volume-backups.ts +++ b/packages/server/src/services/volume-backups.ts @@ -19,6 +19,7 @@ export const findVolumeBackupById = async (volumeBackupId: string) => { mongo: true, redis: true, compose: true, + destination: true, }, }); diff --git a/packages/server/src/setup/config-paths.ts b/packages/server/src/setup/config-paths.ts index 95f2d30d1..3d61569e5 100644 --- a/packages/server/src/setup/config-paths.ts +++ b/packages/server/src/setup/config-paths.ts @@ -19,6 +19,7 @@ export const setupDirectories = () => { MONITORING_PATH, SSH_PATH, SCHEDULES_PATH, + VOLUME_BACKUPS_PATH, } = paths(); const directories = [ BASE_PATH, @@ -30,6 +31,7 @@ export const setupDirectories = () => { CERTIFICATES_PATH, MONITORING_PATH, SCHEDULES_PATH, + VOLUME_BACKUPS_PATH, ]; for (const dir of directories) { diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index dab4324f0..dbad12ec0 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -1,39 +1,89 @@ -import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; -import { findComposeById } from "../.."; +import { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; +import { + createDeploymentVolumeBackup, + execAsync, + execAsyncRemote, + findComposeById, + getS3Credentials, + normalizeS3Path, + paths, + updateDeploymentStatus, +} from "../.."; +import path from "node:path"; -export const createVolumeBackup = async ( - volumeBackup: Awaited>, -) => { +export const runVolumeBackup = async (volumeBackupId: string) => { + const volumeBackup = await findVolumeBackupById(volumeBackupId); const serverId = volumeBackup.application?.serverId || volumeBackup.compose?.serverId; + const deployment = await createDeploymentVolumeBackup({ + volumeBackupId: volumeBackup.volumeBackupId, + title: "Volume Backup", + description: "Volume Backup", + }); - if (serverId) { - } else { + try { + const command = await backupVolume(volumeBackup); + + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (serverId) { + await execAsyncRemote(serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + console.error(error); } }; const backupVolume = async ( volumeBackup: Awaited>, ) => { - const { serviceType, volumeName, turnOff } = volumeBackup; + const { serviceType, volumeName, turnOff, prefix } = volumeBackup; + const serverId = + volumeBackup.application?.serverId || volumeBackup.compose?.serverId; + const { VOLUME_BACKUPS_PATH } = paths(!!serverId); + const destination = volumeBackup.destination; + const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`; + const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; + const rcloneFlags = getS3Credentials(volumeBackup.destination); + const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; + const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); + + const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`; const baseCommand = ` + set -e + echo "Volume name: ${volumeName}" + echo "Backup file name: ${backupFileName}" + echo "Turning off volume backup: ${turnOff ? "Yes" : "No"}" + echo "Starting volume backup" + echo "Dir: ${volumeBackupPath}" docker run --rm \ -v ${volumeName}:/volume_data \ - -v $(pwd):/backup \ + -v ${volumeBackupPath}:/backup \ ubuntu \ - bash -c "cd /volume_data && tar cvf /backup/${volumeName}.tar ." + bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ." + echo "Volume backup done ✅" + echo "Starting upload to S3..." + ${rcloneCommand} + echo "Upload to S3 done ✅" `; - if (turnOff) { + if (!turnOff) { return baseCommand; } if (serviceType === "application") { return ` - docker service scale ${volumeBackup.application?.appName}=0 + echo "Stopping application to 0 replicas" ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}") + echo "Actual replicas: $ACTUAL_REPLICAS" + docker service scale ${volumeBackup.application?.appName}=0 ${baseCommand} + echo "Starting application to $ACTUAL_REPLICAS replicas" docker service scale ${volumeBackup.application?.appName}=$ACTUAL_REPLICAS `; } @@ -46,14 +96,20 @@ const backupVolume = async ( if (compose.composeType === "stack") { stopCommand = ` + echo "Stopping compose to 0 replicas" ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}") + echo "Actual replicas: $ACTUAL_REPLICAS" docker service scale ${compose.appName}_${volumeBackup.serviceName}=0`; - startCommand = `docker service scale ${compose.appName}_${volumeBackup.serviceName}=$ACTUAL_REPLICAS`; + startCommand = ` + echo "Starting compose to $ACTUAL_REPLICAS replicas" + docker service scale ${compose.appName}_${volumeBackup.serviceName}=$ACTUAL_REPLICAS`; } else { stopCommand = ` + echo "Stopping compose container" ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}") docker stop $ID`; startCommand = ` + echo "Starting compose container" docker start $ID`; } return ` @@ -69,11 +125,20 @@ export const restoreVolume = async ( ) => { const { serviceType, volumeName } = volumeBackup; + const serverId = + volumeBackup.application?.serverId || volumeBackup.compose?.serverId; + const { VOLUME_BACKUPS_PATH } = paths(!!serverId); + const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); + const baseCommand = ` + set -e docker volume rm ${volumeName} --force + echo "Volume name: ${volumeName}" + echo "Volume backup path: ${volumeBackupPath}" + echo "Starting volume restore" docker run --rm \ -v ${volumeName}:/volume_data \ --v $(pwd):/backup \ +-v ${volumeBackupPath}:/backup \ ubuntu \ bash -c "cd /volume_data && tar xvf /backup/${volumeName}.tar ." `;