From c6d760a90485a573ffd95a841398134edc3eb8f8 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Tue, 1 Jul 2025 01:12:20 -0600
Subject: [PATCH] fix: update RestoreVolumeBackups and ShowVolumeBackups
components for improved functionality
- Refactored the RestoreVolumeBackups component to ensure the type prop is required and added serverId handling for better integration.
- Corrected variable naming for destinationId in the form handling to prevent potential issues.
- Enhanced the ShowVolumeBackups component to pass serverId to the RestoreVolumeBackups component, ensuring consistent data flow.
- Improved user interface elements for backup file selection, ensuring better usability and clarity.
---
.../volume-backups/restore-volume-backups.tsx | 76 ++--
.../volume-backups/show-volume-backups.tsx | 13 +-
.../database/backups/restore-backup.tsx | 6 +-
.../services/application/[applicationId].tsx | 1 +
.../services/compose/[composeId].tsx | 6 +-
.../server/api/routers/volume-backups.ts | 79 +++-
.../server/src/utils/volume-backups/utils.ts | 361 ++++++++++++++++--
7 files changed, 452 insertions(+), 90 deletions(-)
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;
};