Merge pull request #3637 from Dokploy/3392-conflict-between-daily-docker-cleanup-and-volume-backup-turn-off-container-during-backup

3392 conflict between daily docker cleanup and volume backup turn off container during backup
This commit is contained in:
Mauricio Siu
2026-02-07 23:03:24 -06:00
committed by GitHub
6 changed files with 61 additions and 13 deletions
+5 -4
View File
@@ -1,4 +1,5 @@
import { import {
CLEANUP_CRON_JOB,
canAccessToTraefikFiles, canAccessToTraefikFiles,
checkGPUStatus, checkGPUStatus,
checkPortInUse, checkPortInUse,
@@ -298,12 +299,12 @@ export const settingsRouter = createTRPCRouter({
} }
if (IS_CLOUD) { if (IS_CLOUD) {
await schedule({ await schedule({
cronSchedule: "0 0 * * *", cronSchedule: CLEANUP_CRON_JOB,
serverId: input.serverId, serverId: input.serverId,
type: "server", type: "server",
}); });
} else { } else {
scheduleJob(server.serverId, "0 0 * * *", async () => { scheduleJob(server.serverId, CLEANUP_CRON_JOB, async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`, `Docker Cleanup ${new Date().toLocaleString()}] Running...`,
); );
@@ -316,7 +317,7 @@ export const settingsRouter = createTRPCRouter({
} else { } else {
if (IS_CLOUD) { if (IS_CLOUD) {
await removeJob({ await removeJob({
cronSchedule: "0 0 * * *", cronSchedule: CLEANUP_CRON_JOB,
serverId: input.serverId, serverId: input.serverId,
type: "server", type: "server",
}); });
@@ -331,7 +332,7 @@ export const settingsRouter = createTRPCRouter({
}); });
if (settingsUpdated?.enableDockerCleanup) { if (settingsUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`, `Docker Cleanup ${new Date().toLocaleString()}] Running...`,
); );
@@ -21,7 +21,7 @@ import {
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { removeJob, schedule, updateJob } from "@/server/utils/backup"; import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -54,6 +54,7 @@ export const volumeBackupsRouter = createTRPCRouter({
redis: true, redis: true,
compose: true, compose: true,
}, },
orderBy: [desc(volumeBackups.createdAt)],
}); });
}), }),
create: protectedProcedure create: protectedProcedure
+2 -1
View File
@@ -1,4 +1,5 @@
import { import {
CLEANUP_CRON_JOB,
cleanupAll, cleanupAll,
findBackupById, findBackupById,
findScheduleById, findScheduleById,
@@ -125,7 +126,7 @@ export const initializeJobs = async () => {
scheduleJob({ scheduleJob({
serverId, serverId,
type: "server", type: "server",
cronSchedule: "0 0 * * *", cronSchedule: CLEANUP_CRON_JOB,
}); });
} }
+2
View File
@@ -2,6 +2,7 @@ import path from "node:path";
import Docker from "dockerode"; import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true"; export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const docker = new Docker(); export const docker = new Docker();
export const BETTER_AUTH_SECRET = export const BETTER_AUTH_SECRET =
@@ -29,5 +30,6 @@ export const paths = (isServer = false) => {
REGISTRY_PATH: `${BASE_PATH}/registry`, REGISTRY_PATH: `${BASE_PATH}/registry`,
SCHEDULES_PATH: `${BASE_PATH}/schedules`, SCHEDULES_PATH: `${BASE_PATH}/schedules`,
VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`, VOLUME_BACKUPS_PATH: `${BASE_PATH}/volume-backups`,
VOLUME_BACKUP_LOCK_PATH: `${BASE_PATH}/volume-backup-lock`,
}; };
}; };
+3 -2
View File
@@ -1,4 +1,5 @@
import path from "node:path"; import path from "node:path";
import { CLEANUP_CRON_JOB } from "@dokploy/server/constants";
import { member } from "@dokploy/server/db/schema"; import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server"; import { getAllServers } from "@dokploy/server/services/server";
@@ -29,7 +30,7 @@ export const initCronJobs = async () => {
const webServerSettings = await getWebServerSettings(); const webServerSettings = await getWebServerSettings();
if (webServerSettings?.enableDockerCleanup) { if (webServerSettings?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", CLEANUP_CRON_JOB, async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
); );
@@ -45,7 +46,7 @@ export const initCronJobs = async () => {
for (const server of servers) { for (const server of servers) {
const { serverId, enableDockerCleanup, name } = server; const { serverId, enableDockerCleanup, name } = server;
if (enableDockerCleanup) { if (enableDockerCleanup) {
scheduleJob(serverId, "0 0 * * *", async () => { scheduleJob(serverId, CLEANUP_CRON_JOB, async () => {
console.log( console.log(
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`, `SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
); );
@@ -10,7 +10,7 @@ export const backupVolume = async (
const { serviceType, volumeName, turnOff, prefix } = volumeBackup; const { serviceType, volumeName, turnOff, prefix } = volumeBackup;
const serverId = const serverId =
volumeBackup.application?.serverId || volumeBackup.compose?.serverId; volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
const { VOLUME_BACKUPS_PATH } = paths(!!serverId); const { VOLUME_BACKUPS_PATH, VOLUME_BACKUP_LOCK_PATH } = paths(!!serverId);
const destination = volumeBackup.destination; const destination = volumeBackup.destination;
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`; const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
@@ -45,8 +45,48 @@ export const backupVolume = async (
return baseCommand; return baseCommand;
} }
const serviceLockId =
serviceType === "application"
? volumeBackup.application?.appName
: `${volumeBackup.compose?.appName}_${volumeBackup.serviceName}`;
const lockPath = `${VOLUME_BACKUP_LOCK_PATH}-${serviceLockId}`;
const lockWrapper = (body: string) => `
set -e
LOCK_PATH="${lockPath}"
echo "Waiting for volume backup lock: $LOCK_PATH"
if command -v flock >/dev/null 2>&1; then
exec 9>"$LOCK_PATH"
flock 9
else
LOCK_DIR="$LOCK_PATH.dir"
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
echo "Waiting for volume backup lock: $LOCK_PATH"
sleep 5
done
trap 'rm -rf "$LOCK_DIR"' EXIT
fi
echo "Volume backup lock acquired"
${body}
echo "Volume backup lock released"
`;
console.log(
lockWrapper(`
echo "Volume backup lock acquired"
echo "Volume backup lock released"
`),
);
if (serviceType === "application") { if (serviceType === "application") {
return ` return lockWrapper(`
echo "Stopping application to 0 replicas" echo "Stopping application to 0 replicas"
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}") ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
echo "Actual replicas: $ACTUAL_REPLICAS" echo "Actual replicas: $ACTUAL_REPLICAS"
@@ -54,7 +94,7 @@ export const backupVolume = async (
${baseCommand} ${baseCommand}
echo "Starting application to $ACTUAL_REPLICAS replicas" echo "Starting application to $ACTUAL_REPLICAS replicas"
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName} docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
`; `);
} }
if (serviceType === "compose") { if (serviceType === "compose") {
const compose = await findComposeById( const compose = await findComposeById(
@@ -70,6 +110,7 @@ export const backupVolume = async (
ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}") ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}")
echo "Actual replicas: $ACTUAL_REPLICAS" echo "Actual replicas: $ACTUAL_REPLICAS"
docker service update --replicas=0 ${compose.appName}_${volumeBackup.serviceName}`; docker service update --replicas=0 ${compose.appName}_${volumeBackup.serviceName}`;
startCommand = ` startCommand = `
echo "Starting compose to $ACTUAL_REPLICAS replicas" echo "Starting compose to $ACTUAL_REPLICAS replicas"
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${compose.appName}_${volumeBackup.serviceName}`; docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${compose.appName}_${volumeBackup.serviceName}`;
@@ -78,16 +119,17 @@ export const backupVolume = async (
echo "Stopping compose container" echo "Stopping compose container"
ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}") ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}")
docker stop $ID`; docker stop $ID`;
startCommand = ` startCommand = `
echo "Starting compose container" echo "Starting compose container"
docker start $ID docker start $ID
echo "Compose container started" echo "Compose container started"
`; `;
} }
return ` return lockWrapper(`
${stopCommand} ${stopCommand}
${baseCommand} ${baseCommand}
${startCommand} ${startCommand}
`; `);
} }
}; };