mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
feat: add runVolumeBackup functionality and update volume backup paths
- Implemented the runVolumeBackup function to facilitate manual execution of volume backups, enhancing user control over backup processes. - Updated various components to utilize the new VOLUME_BACKUPS_PATH for improved organization and clarity in backup file management. - Enhanced error handling in the runManually mutation to ensure robust execution and logging of backup operations.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ReturnType<typeof findVolumeBackupById>>,
|
||||
) => {
|
||||
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<ReturnType<typeof findVolumeBackupById>>,
|
||||
) => {
|
||||
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 ."
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user