feat: enhance volume backup scheduling and management

- Added initVolumeBackupsCronJobs function to initialize scheduled volume backups on server startup.
- Updated volumeBackupsRouter to handle scheduling and removal of volume backup jobs based on user input.
- Improved create and update volume backup logic to include scheduling functionality for both cloud and local environments.
- Introduced utility functions for scheduling and removing volume backup jobs, enhancing overall backup management.
This commit is contained in:
Mauricio Siu
2025-07-02 00:36:46 -06:00
parent c5311f2a9f
commit 6521491e2f
6 changed files with 104 additions and 16 deletions

View File

@@ -6,6 +6,8 @@ import {
runVolumeBackup,
findVolumeBackupById,
restoreVolume,
scheduleVolumeBackup,
removeVolumeBackupJob,
} from "@dokploy/server";
import {
createVolumeBackupSchema,
@@ -21,6 +23,8 @@ import {
execAsyncRemote,
execAsyncStream,
} from "@dokploy/server/utils/process/execAsync";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { TRPCError } from "@trpc/server";
export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure
@@ -55,7 +59,20 @@ export const volumeBackupsRouter = createTRPCRouter({
create: protectedProcedure
.input(createVolumeBackupSchema)
.mutation(async ({ input }) => {
return await createVolumeBackup(input);
const newVolumeBackup = await createVolumeBackup(input);
if (newVolumeBackup?.enabled) {
if (IS_CLOUD) {
await schedule({
cronSchedule: newVolumeBackup.cronExpression,
volumeBackupId: newVolumeBackup.volumeBackupId,
type: "volume-backup",
});
} else {
await scheduleVolumeBackup(newVolumeBackup.volumeBackupId);
}
}
return newVolumeBackup;
}),
one: protectedProcedure
.input(
@@ -73,15 +90,46 @@ export const volumeBackupsRouter = createTRPCRouter({
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
return await removeVolumeBackup(input.volumeBackupId);
}),
update: protectedProcedure
.input(updateVolumeBackupSchema)
.mutation(async ({ input }) => {
return await updateVolumeBackup(input.volumeBackupId, input);
const updatedVolumeBackup = await updateVolumeBackup(
input.volumeBackupId,
input,
);
if (!updatedVolumeBackup) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Volume backup not found",
});
}
if (IS_CLOUD) {
if (updatedVolumeBackup.enabled) {
await updateJob({
cronSchedule: updatedVolumeBackup.cronExpression,
volumeBackupId: updatedVolumeBackup.volumeBackupId,
type: "volume-backup",
});
} else {
await removeJob({
cronSchedule: updatedVolumeBackup.cronExpression,
volumeBackupId: updatedVolumeBackup.volumeBackupId,
type: "volume-backup",
});
}
} else {
if (updatedVolumeBackup?.enabled) {
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
scheduleVolumeBackup(updatedVolumeBackup.volumeBackupId);
} else {
removeVolumeBackupJob(updatedVolumeBackup.volumeBackupId);
}
}
return updatedVolumeBackup;
}),
runManually: protectedProcedure

View File

@@ -7,6 +7,7 @@ import {
createDefaultTraefikConfig,
initCronJobs,
initSchedules,
initVolumeBackupsCronJobs,
initializeNetwork,
sendDokployRestartNotifications,
setupDirectories,
@@ -51,6 +52,7 @@ void app.prepare().then(async () => {
await migration();
await initCronJobs();
await initSchedules();
await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications();
}

View File

@@ -19,6 +19,11 @@ type QueueJob =
type: "schedule";
cronSchedule: string;
scheduleId: string;
}
| {
type: "volume-backup";
cronSchedule: string;
volumeBackupId: string;
};
export const schedule = async (job: QueueJob) => {
try {

View File

@@ -7,7 +7,6 @@ import {
import { db } from "../db";
import { TRPCError } from "@trpc/server";
import type { z } from "zod";
import { scheduleBackup } from "../utils/backups/utils";
export const findVolumeBackupById = async (volumeBackupId: string) => {
const volumeBackup = await db.query.volumeBackups.findFirst({
@@ -43,14 +42,6 @@ export const createVolumeBackup = async (
.returning()
.then((e) => e[0]);
await schedule({
cronSchedule: backup.schedule,
backupId: backup.backupId,
type: "backup",
});
scheduleBackup(backup);
return newVolumeBackup;
};
@@ -64,8 +55,10 @@ export const updateVolumeBackup = async (
volumeBackupId: string,
volumeBackup: z.infer<typeof updateVolumeBackupSchema>,
) => {
await db
return await db
.update(volumeBackups)
.set(volumeBackup)
.where(eq(volumeBackups.volumeBackupId, volumeBackupId));
.where(eq(volumeBackups.volumeBackupId, volumeBackupId))
.returning()
.then((e) => e[0]);
};

View File

@@ -1,3 +1,30 @@
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";
import { scheduleVolumeBackup } from "./utils";
export const initVolumeBackupsCronJobs = async () => {
console.log("Setting up volume backups cron jobs....");
try {
const volumeBackupsResult = await db.query.volumeBackups.findMany({
where: eq(volumeBackups.enabled, true),
with: {
application: true,
compose: true,
},
});
console.log(`Initializing ${volumeBackupsResult.length} volume backups`);
for (const volumeBackup of volumeBackupsResult) {
scheduleVolumeBackup(volumeBackup.volumeBackupId);
console.log(
`Initialized volume backup: ${volumeBackup.name} ${volumeBackup.serviceType}`,
);
}
} catch (error) {
console.log(`Error initializing volume backups: ${error}`);
}
};

View File

@@ -6,6 +6,19 @@ import {
updateDeploymentStatus,
} from "../..";
import { backupVolume } from "./backup";
import { scheduleJob, scheduledJobs } from "node-schedule";
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
await runVolumeBackup(volumeBackupId);
});
};
export const removeVolumeBackupJob = async (volumeBackupId: string) => {
const currentJob = scheduledJobs[volumeBackupId];
currentJob?.cancel();
};
export const runVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId);