diff --git a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx index 4c1bc1658..295833a39 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx @@ -47,35 +47,33 @@ import { formatBytes } from "../../database/backups/restore-backup"; interface Props { id: string; - type?: "application" | "compose" | "postgres" | "mariadb" | "mongo" | "mysql"; + type: "application" | "compose"; serverId?: string; } -const RestoreBackupSchema = z - .object({ - destinationId: z - .string({ - required_error: "Please select a destination", - }) - .min(1, { - message: "Destination is required", - }), - backupFile: z - .string({ - required_error: "Please select a backup file", - }) - .min(1, { - message: "Backup file is required", - }), - volumeName: z - .string({ - required_error: "Please enter a volume name", - }) - .min(1, { - message: "Volume name is required", - }), - }) - .superRefine((data, ctx) => {}); +const RestoreBackupSchema = z.object({ + destinationId: z + .string({ + required_error: "Please select a destination", + }) + .min(1, { + message: "Destination is required", + }), + backupFile: z + .string({ + required_error: "Please select a backup file", + }) + .min(1, { + message: "Backup file is required", + }), + volumeName: z + .string({ + required_error: "Please enter a volume name", + }) + .min(1, { + message: "Volume name is required", + }), +}); export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { const [isOpen, setIsOpen] = useState(false); @@ -93,8 +91,9 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { resolver: zodResolver(RestoreBackupSchema), }); - const destionationId = form.watch("destinationId"); + const destinationId = form.watch("destinationId"); const volumeName = form.watch("volumeName"); + const backupFile = form.watch("backupFile"); const debouncedSetSearch = debounce((value: string) => { setDebouncedSearchTerm(value); @@ -107,12 +106,12 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery( { - destinationId: destionationId, + destinationId: destinationId, search: debouncedSearchTerm, serverId: serverId ?? "", }, { - enabled: isOpen && !!destionationId, + enabled: isOpen && !!destinationId, }, ); @@ -122,9 +121,12 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription( { - volumeBackupId: id, - destinationId: form.watch("destinationId"), - volumeName: volumeName, + id, + serviceType: type, + serverId, + destinationId, + volumeName, + backupFileName: backupFile, }, { enabled: isDeploying, @@ -146,7 +148,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { }, ); - const onSubmit = async (data: z.infer) => { + const onSubmit = async () => { setIsDeploying(true); }; @@ -246,10 +248,10 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { name="backupFile" render={({ field }) => ( - + Search Backup Files {field.value && ( - + {field.value} { !field.value && "text-muted-foreground", )} > - {field.value || "Search and select a backup file"} + + {field.value || "Search and select a backup file"} + diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx index 8318b0170..56223f3bf 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx @@ -29,10 +29,15 @@ import { RestoreVolumeBackups } from "./restore-volume-backups"; interface Props { id: string; - type?: "application" | "compose" | "postgres" | "mariadb" | "mongo" | "mysql"; + type?: "application" | "compose"; + serverId?: string; } -export const ShowVolumeBackups = ({ id, type = "application" }: Props) => { +export const ShowVolumeBackups = ({ + id, + type = "application", + serverId, +}: Props) => { const { data: volumeBackups, isLoading: isLoadingVolumeBackups, @@ -74,7 +79,7 @@ export const ShowVolumeBackups = ({ id, type = "application" }: Props) => { )}
- +
@@ -228,7 +233,7 @@ export const ShowVolumeBackups = ({ id, type = "application" }: Props) => {

- +
)} diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index b404204ab..76ab7b6cf 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -415,7 +415,7 @@ export const RestoreBackup = ({ Search Backup Files {field.value && ( - + {field.value} - {field.value || "Search and select a backup file"} + + {field.value || "Search and select a backup file"} + diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index b396837b1..9817668ab 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -338,6 +338,7 @@ const Service = ( diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index de9ab94d5..40b946d20 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -262,7 +262,11 @@ const Service = (
- +
diff --git a/apps/dokploy/server/api/routers/volume-backups.ts b/apps/dokploy/server/api/routers/volume-backups.ts index 62187a9cb..f1eb59b92 100644 --- a/apps/dokploy/server/api/routers/volume-backups.ts +++ b/apps/dokploy/server/api/routers/volume-backups.ts @@ -1,11 +1,10 @@ import { - findVolumeBackupById, IS_CLOUD, updateVolumeBackup, removeVolumeBackup, createVolumeBackup, runVolumeBackup, - findDestinationById, + findVolumeBackupById, } from "@dokploy/server"; import { createVolumeBackupSchema, @@ -16,8 +15,12 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { db } from "@dokploy/server/db"; import { eq } from "drizzle-orm"; -import { restorePostgresBackup } from "@dokploy/server/utils/restore"; import { observable } from "@trpc/server/observable"; +import { restoreVolume } from "@dokploy/server/utils/volume-backups/utils"; +import { + execAsyncRemote, + execAsyncStream, +} from "@dokploy/server/utils/process/execAsync"; export const volumeBackupsRouter = createTRPCRouter({ list: protectedProcedure @@ -102,18 +105,76 @@ export const volumeBackupsRouter = createTRPCRouter({ }) .input( z.object({ - volumeBackupId: z.string().min(1), + backupFileName: z.string().min(1), destinationId: z.string().min(1), volumeName: z.string().min(1), + id: z.string().min(1), + serviceType: z.enum(["application", "compose"]), + serverId: z.string().optional(), }), ) .subscription(async ({ input }) => { - const destination = await findDestinationById(input.destinationId); - return observable((emit) => { - // restorePostgresBackup(postgres, destination, input, (log) => { - // emit.next(log); - // }); + const runRestore = async () => { + try { + emit.next("🚀 Starting volume restore process..."); + emit.next(`📂 Backup File: ${input.backupFileName}`); + emit.next(`🔧 Volume Name: ${input.volumeName}`); + emit.next(`🏷️ Service Type: ${input.serviceType}`); + emit.next(""); // Empty line for better readability + + // Generate the restore command + const restoreCommand = await restoreVolume( + input.id, + input.destinationId, + input.volumeName, + input.backupFileName, + input.serverId || "", + input.serviceType, + ); + + emit.next("📋 Generated restore command:"); + emit.next("▶️ Executing restore..."); + emit.next(""); // Empty line + + // Execute the restore command with real-time output + if (input.serverId) { + emit.next(`🌐 Executing on remote server: ${input.serverId}`); + await execAsyncRemote(input.serverId, restoreCommand, (data) => { + emit.next(data); + }); + } else { + emit.next("🖥️ Executing on local server"); + await execAsyncStream(restoreCommand, (data) => { + emit.next(data); + }); + } + + emit.next(""); + emit.next("✅ Volume restore completed successfully!"); + emit.next( + "🎉 All containers/services have been restarted with the restored volume.", + ); + } catch (error) { + emit.next(""); + emit.next("❌ Volume restore failed!"); + emit.next( + `💥 Error: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + + if (error instanceof Error && error.stack) { + emit.next("📋 Stack trace:"); + for (const line of error.stack.split("\n")) { + if (line.trim()) emit.next(` ${line}`); + } + } + } finally { + emit.complete(); + } + }; + + // Start the restore process + runRestore(); }); }), }); diff --git a/packages/server/src/utils/volume-backups/utils.ts b/packages/server/src/utils/volume-backups/utils.ts index 99b2f7c82..5fb2292a3 100644 --- a/packages/server/src/utils/volume-backups/utils.ts +++ b/packages/server/src/utils/volume-backups/utils.ts @@ -3,7 +3,9 @@ import { createDeploymentVolumeBackup, execAsync, execAsyncRemote, + findApplicationById, findComposeById, + findDestinationById, getS3Credentials, normalizeS3Path, paths, @@ -122,55 +124,338 @@ const backupVolume = async ( }; export const restoreVolume = async ( - volumeBackup: Awaited>, + id: string, + destinationId: string, + volumeName: string, + backupFileName: string, + serverId: string, + serviceType: "application" | "compose", ) => { - const { serviceType, volumeName } = volumeBackup; - - const serverId = - volumeBackup.application?.serverId || volumeBackup.compose?.serverId; + const destination = await findDestinationById(destinationId); const { VOLUME_BACKUPS_PATH } = paths(!!serverId); - const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName); + const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeName); + const rcloneFlags = getS3Credentials(destination); + const bucketPath = `:s3:${destination.bucket}`; + const backupPath = `${bucketPath}/${backupFileName}`; - const baseCommand = ` + // Command to download backup file from S3 + const downloadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${backupPath}" "${volumeBackupPath}/${backupFileName}"`; + + // Base restore command that creates the volume and restores data + const baseRestoreCommand = ` 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 ${volumeBackupPath}:/backup \ -ubuntu \ -bash -c "cd /volume_data && tar xvf /backup/${volumeName}.tar ." - `; + echo "Volume name: ${volumeName}" + echo "Backup file name: ${backupFileName}" + echo "Volume backup path: ${volumeBackupPath}" + echo "Downloading backup from S3..." + mkdir -p ${volumeBackupPath} + ${downloadCommand} + echo "Download completed ✅" + echo "Creating new volume and restoring data..." + docker run --rm \ + -v ${volumeName}:/volume_data \ + -v ${volumeBackupPath}:/backup \ + ubuntu \ + bash -c "cd /volume_data && tar xvf /backup/${backupFileName} ." + echo "Volume restore completed ✅" + `; + + // Function to check if volume exists and get containers using it + const checkVolumeCommand = ` + # Check if volume exists + VOLUME_EXISTS=$(docker volume ls -q --filter name="^${volumeName}$" | wc -l) + echo "Volume exists: $VOLUME_EXISTS" + + if [ "$VOLUME_EXISTS" = "0" ]; then + echo "Volume doesn't exist, proceeding with direct restore" + ${baseRestoreCommand} + else + echo "Volume exists, checking for containers using it (including stopped ones)..." + + # Get ALL containers (running and stopped) using this volume - much simpler with native filter! + CONTAINERS_USING_VOLUME=$(docker ps -a --filter "volume=${volumeName}" --format "{{.ID}}|{{.Names}}|{{.State}}|{{.Labels}}") + + if [ -z "$CONTAINERS_USING_VOLUME" ]; then + echo "Volume exists but no containers are using it" + echo "Removing existing volume and proceeding with restore" + docker volume rm ${volumeName} --force + ${baseRestoreCommand} + else + echo "Found containers using the volume:" + echo "$CONTAINERS_USING_VOLUME" + + # Collect unique services and containers to stop + echo "=== PHASE 1: Stopping all containers/services using the volume ===" + + SERVICES_TO_RESTORE="" + CONTAINERS_TO_RESTORE="" + + echo "$CONTAINERS_USING_VOLUME" | while IFS='|' read container_id container_name container_state labels; do + echo "Analyzing container: $container_id ($container_name) - State: $container_state" + + # Check if it's a swarm service + if echo "$labels" | grep -q "com.docker.swarm.service.name="; then + SERVICE_NAME=$(echo "$labels" | grep -o "com.docker.swarm.service.name=[^,]*" | cut -d'=' -f2) + + # Check if we already processed this service + if ! echo "$SERVICES_TO_RESTORE" | grep -q "SWARM:$SERVICE_NAME"; then + echo "SWARM:$SERVICE_NAME" >> /tmp/dokploy_services_to_stop + echo "Will stop swarm service: $SERVICE_NAME" + fi + + # Check if it's a compose container + elif echo "$labels" | grep -q "com.docker.compose.project="; then + PROJECT_NAME=$(echo "$labels" | grep -o "com.docker.compose.project=[^,]*" | cut -d'=' -f2) + SERVICE_NAME_COMPOSE=$(echo "$labels" | grep -o "com.docker.compose.service=[^,]*" | cut -d'=' -f2 || echo "") + + # Check if it's a compose stack (swarm mode) or regular compose + if echo "$labels" | grep -q "com.docker.stack.namespace="; then + STACK_SERVICE_NAME="$PROJECT_NAME"_"$SERVICE_NAME_COMPOSE" + + # Check if we already processed this stack service + if ! echo "$SERVICES_TO_RESTORE" | grep -q "STACK:$STACK_SERVICE_NAME"; then + echo "STACK:$STACK_SERVICE_NAME" >> /tmp/dokploy_services_to_stop + echo "Will stop compose stack service: $STACK_SERVICE_NAME" + fi + else + echo "COMPOSE:$container_id|$container_name|$container_state" >> /tmp/dokploy_services_to_stop + echo "Will stop compose container: $container_id ($container_name)" + fi + else + echo "REGULAR:$container_id|$container_name|$container_state" >> /tmp/dokploy_services_to_stop + echo "Will stop regular container: $container_id ($container_name)" + fi + done + + # Now stop all services and containers + if [ -f /tmp/dokploy_services_to_stop ]; then + echo "" + echo "=== STOPPING ALL SERVICES/CONTAINERS ===" + + while read line; do + TYPE=$(echo "$line" | cut -d':' -f1) + DATA=$(echo "$line" | cut -d':' -f2-) + + if [ "$TYPE" = "SWARM" ]; then + SERVICE_NAME="$DATA" + echo "Stopping swarm service: $SERVICE_NAME" + + # Get current replicas and store for later + ACTUAL_REPLICAS=$(docker service inspect "$SERVICE_NAME" --format "{{.Spec.Mode.Replicated.Replicas}}" 2>/dev/null || echo "0") + echo "SWARM:$SERVICE_NAME:$ACTUAL_REPLICAS" >> /tmp/dokploy_services_to_restore + + # Scale down to 0 if not already + if [ "$ACTUAL_REPLICAS" != "0" ]; then + echo "Scaling service $SERVICE_NAME to 0 replicas (was $ACTUAL_REPLICAS)" + docker service scale "$SERVICE_NAME=0" + else + echo "Service $SERVICE_NAME is already scaled to 0" + fi + + elif [ "$TYPE" = "STACK" ]; then + STACK_SERVICE_NAME="$DATA" + echo "Stopping compose stack service: $STACK_SERVICE_NAME" + + # Get current replicas and store for later + ACTUAL_REPLICAS=$(docker service inspect "$STACK_SERVICE_NAME" --format "{{.Spec.Mode.Replicated.Replicas}}" 2>/dev/null || echo "0") + echo "STACK:$STACK_SERVICE_NAME:$ACTUAL_REPLICAS" >> /tmp/dokploy_services_to_restore + + # Scale down to 0 if not already + if [ "$ACTUAL_REPLICAS" != "0" ]; then + echo "Scaling stack service $STACK_SERVICE_NAME to 0 replicas (was $ACTUAL_REPLICAS)" + docker service scale "$STACK_SERVICE_NAME=0" + else + echo "Stack service $STACK_SERVICE_NAME is already scaled to 0" + fi + + elif [ "$TYPE" = "COMPOSE" ]; then + container_id=$(echo "$DATA" | cut -d'|' -f1) + container_name=$(echo "$DATA" | cut -d'|' -f2) + container_state=$(echo "$DATA" | cut -d'|' -f3) + + echo "COMPOSE:$container_id|$container_name|$container_state" >> /tmp/dokploy_services_to_restore + + # Stop the container if running + if [ "$container_state" = "running" ]; then + echo "Stopping compose container: $container_id ($container_name)" + docker stop "$container_id" + else + echo "Compose container $container_id is already stopped" + fi + + elif [ "$TYPE" = "REGULAR" ]; then + container_id=$(echo "$DATA" | cut -d'|' -f1) + container_name=$(echo "$DATA" | cut -d'|' -f2) + container_state=$(echo "$DATA" | cut -d'|' -f3) + + echo "REGULAR:$container_id|$container_name|$container_state" >> /tmp/dokploy_services_to_restore + + # Stop the container if running + if [ "$container_state" = "running" ]; then + echo "Stopping regular container: $container_id ($container_name)" + docker stop "$container_id" + else + echo "Regular container $container_id is already stopped" + fi + fi + done < /tmp/dokploy_services_to_stop + + # Wait for all services to scale down + echo "" + echo "=== WAITING FOR ALL SERVICES TO STOP ===" + echo "Waiting for all containers to be fully removed..." + sleep 10 + + # Verify all swarm services are scaled down + if grep -q "SWARM:" /tmp/dokploy_services_to_restore 2>/dev/null; then + echo "Verifying swarm services are scaled down..." + grep "SWARM:" /tmp/dokploy_services_to_restore | while read line; do + SERVICE_NAME=$(echo "$line" | cut -d':' -f2) + while [ $(docker service ps "$SERVICE_NAME" --filter "desired-state=running" -q 2>/dev/null | wc -l) -gt 0 ]; do + echo "Still waiting for swarm service $SERVICE_NAME to scale down..." + sleep 3 + done + echo "Swarm service $SERVICE_NAME is fully scaled down" + done + fi + + # Verify all stack services are scaled down + if grep -q "STACK:" /tmp/dokploy_services_to_restore 2>/dev/null; then + echo "Verifying stack services are scaled down..." + grep "STACK:" /tmp/dokploy_services_to_restore | while read line; do + STACK_SERVICE_NAME=$(echo "$line" | cut -d':' -f2) + while [ $(docker service ps "$STACK_SERVICE_NAME" --filter "desired-state=running" -q 2>/dev/null | wc -l) -gt 0 ]; do + echo "Still waiting for stack service $STACK_SERVICE_NAME to scale down..." + sleep 3 + done + echo "Stack service $STACK_SERVICE_NAME is fully scaled down" + done + fi + + echo "" + echo "=== REMOVING STOPPED SWARM CONTAINERS USING THE VOLUME ===" + + # Only remove stopped containers that belong to Swarm services + # Regular containers and compose containers should NOT be removed since they can't be recreated automatically + SWARM_STOPPED_CONTAINERS=$(docker ps -a --filter "status=exited" --format "{{.ID}} {{.Names}} {{.Labels}}" | while read line; do + container_id=$(echo "$line" | awk '{print $1}') + container_name=$(echo "$line" | awk '{print $2}') + labels=$(echo "$line" | cut -d' ' -f3-) + + # Check if this container uses the volume + volume_used=$(docker inspect "$container_id" --format '{{range .Mounts}}{{.Name}} {{end}}' 2>/dev/null | grep -w "${volumeName}" || true) + + # Only include if it uses the volume AND belongs to a Swarm service + if [ -n "$volume_used" ] && echo "$labels" | grep -q "com.docker.swarm.service.name="; then + echo "$container_id" + fi + done) + + if [ -n "$SWARM_STOPPED_CONTAINERS" ]; then + echo "Found stopped Swarm containers using the volume that need to be removed:" + echo "$SWARM_STOPPED_CONTAINERS" + + echo "$SWARM_STOPPED_CONTAINERS" | while read container_id; do + if [ -n "$container_id" ]; then + echo "Removing stopped Swarm container: $container_id" + docker rm "$container_id" --force + fi + done + + echo "All stopped Swarm containers using the volume have been removed" + else + echo "No stopped Swarm containers using the volume found" + fi + + echo "Note: Regular containers and compose containers are preserved and will be restarted later" + + echo "" + echo "=== PHASE 2: All containers/services stopped - Proceeding with volume restore ===" + + # Remove volume and restore - ONLY ONCE! + echo "Removing existing volume: ${volumeName}" + docker volume rm ${volumeName} --force + + ${baseRestoreCommand} + + echo "" + echo "=== PHASE 3: Restarting all services/containers ===" + + # Restore all services and containers + while read line; do + TYPE=$(echo "$line" | cut -d':' -f1) + DATA=$(echo "$line" | cut -d':' -f2-) + + if [ "$TYPE" = "SWARM" ]; then + SERVICE_NAME=$(echo "$DATA" | cut -d':' -f1) + REPLICAS=$(echo "$DATA" | cut -d':' -f2) + + if [ "$REPLICAS" != "0" ]; then + echo "Scaling swarm service $SERVICE_NAME back to $REPLICAS replicas" + docker service scale "$SERVICE_NAME=$REPLICAS" + else + echo "Leaving swarm service $SERVICE_NAME at 0 replicas (was already 0)" + fi + + elif [ "$TYPE" = "STACK" ]; then + STACK_SERVICE_NAME=$(echo "$DATA" | cut -d':' -f1) + REPLICAS=$(echo "$DATA" | cut -d':' -f2) + + if [ "$REPLICAS" != "0" ]; then + echo "Scaling stack service $STACK_SERVICE_NAME back to $REPLICAS replicas" + docker service scale "$STACK_SERVICE_NAME=$REPLICAS" + else + echo "Leaving stack service $STACK_SERVICE_NAME at 0 replicas (was already 0)" + fi + + elif [ "$TYPE" = "COMPOSE" ]; then + container_id=$(echo "$DATA" | cut -d'|' -f1) + container_name=$(echo "$DATA" | cut -d'|' -f2) + + echo "Starting compose container: $container_id ($container_name)" + docker start "$container_id" + + elif [ "$TYPE" = "REGULAR" ]; then + container_id=$(echo "$DATA" | cut -d'|' -f1) + container_name=$(echo "$DATA" | cut -d'|' -f2) + + echo "Starting regular container: $container_id ($container_name)" + docker start "$container_id" + fi + done < /tmp/dokploy_services_to_restore + + echo "" + echo "✅ Volume restore completed successfully!" + echo "✅ All services and containers have been restarted!" + + # Clean up temp files + rm -f /tmp/dokploy_services_to_stop + rm -f /tmp/dokploy_services_to_restore + fi + fi + fi + `; if (serviceType === "application") { + const application = await findApplicationById(id); return ` - docker service scale ${volumeBackup.application?.appName}=0 - ${baseCommand} - ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}") - docker service scale ${volumeBackup.application?.appName}=$ACTUAL_REPLICAS - `; + echo "=== VOLUME RESTORE FOR APPLICATION ===" + echo "Application: ${application.appName}" + ${checkVolumeCommand} + `; } if (serviceType === "compose") { - const compose = await findComposeById( - volumeBackup.compose?.composeId || "", - ); + const compose = await findComposeById(id); - if (compose.composeType === "stack") { - return ` - ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}") - docker service scale ${compose.appName}_${volumeBackup.serviceName}=0 - ${baseCommand} - docker service scale ${compose.appName}_${volumeBackup.serviceName}=$ACTUAL_REPLICAS - `; - } return ` - ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}") - docker stop $ID - ${baseCommand} - docker start $ID - `; + echo "=== VOLUME RESTORE FOR COMPOSE ===" + echo "Compose: ${compose.appName}" + echo "Compose Type: ${compose.composeType}" + ${checkVolumeCommand} + `; } + + // Fallback for unknown service types + return checkVolumeCommand; };