mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-08 13:05:17 +00:00
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.
This commit is contained in:
+40
-36
@@ -47,35 +47,33 @@ import { formatBytes } from "../../database/backups/restore-backup";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type?: "application" | "compose" | "postgres" | "mariadb" | "mongo" | "mysql";
|
type: "application" | "compose";
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RestoreBackupSchema = z
|
const RestoreBackupSchema = z.object({
|
||||||
.object({
|
destinationId: z
|
||||||
destinationId: z
|
.string({
|
||||||
.string({
|
required_error: "Please select a destination",
|
||||||
required_error: "Please select a destination",
|
})
|
||||||
})
|
.min(1, {
|
||||||
.min(1, {
|
message: "Destination is required",
|
||||||
message: "Destination is required",
|
}),
|
||||||
}),
|
backupFile: z
|
||||||
backupFile: z
|
.string({
|
||||||
.string({
|
required_error: "Please select a backup file",
|
||||||
required_error: "Please select a backup file",
|
})
|
||||||
})
|
.min(1, {
|
||||||
.min(1, {
|
message: "Backup file is required",
|
||||||
message: "Backup file is required",
|
}),
|
||||||
}),
|
volumeName: z
|
||||||
volumeName: z
|
.string({
|
||||||
.string({
|
required_error: "Please enter a volume name",
|
||||||
required_error: "Please enter a volume name",
|
})
|
||||||
})
|
.min(1, {
|
||||||
.min(1, {
|
message: "Volume name is required",
|
||||||
message: "Volume name is required",
|
}),
|
||||||
}),
|
});
|
||||||
})
|
|
||||||
.superRefine((data, ctx) => {});
|
|
||||||
|
|
||||||
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -93,8 +91,9 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
|||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const destionationId = form.watch("destinationId");
|
const destinationId = form.watch("destinationId");
|
||||||
const volumeName = form.watch("volumeName");
|
const volumeName = form.watch("volumeName");
|
||||||
|
const backupFile = form.watch("backupFile");
|
||||||
|
|
||||||
const debouncedSetSearch = debounce((value: string) => {
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
setDebouncedSearchTerm(value);
|
setDebouncedSearchTerm(value);
|
||||||
@@ -107,12 +106,12 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
|||||||
|
|
||||||
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||||
{
|
{
|
||||||
destinationId: destionationId,
|
destinationId: destinationId,
|
||||||
search: debouncedSearchTerm,
|
search: debouncedSearchTerm,
|
||||||
serverId: serverId ?? "",
|
serverId: serverId ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isOpen && !!destionationId,
|
enabled: isOpen && !!destinationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -122,9 +121,12 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
|||||||
|
|
||||||
api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription(
|
api.volumeBackups.restoreVolumeBackupWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
volumeBackupId: id,
|
id,
|
||||||
destinationId: form.watch("destinationId"),
|
serviceType: type,
|
||||||
volumeName: volumeName,
|
serverId,
|
||||||
|
destinationId,
|
||||||
|
volumeName,
|
||||||
|
backupFileName: backupFile,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
@@ -146,7 +148,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof RestoreBackupSchema>) => {
|
const onSubmit = async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,10 +248,10 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
|||||||
name="backupFile"
|
name="backupFile"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="">
|
<FormItem className="">
|
||||||
<FormLabel className="flex items-center justify-between">
|
<FormLabel className="flex items-center">
|
||||||
Search Backup Files
|
Search Backup Files
|
||||||
{field.value && (
|
{field.value && (
|
||||||
<Badge variant="outline">
|
<Badge variant="outline" className="truncate w-52">
|
||||||
{field.value}
|
{field.value}
|
||||||
<Copy
|
<Copy
|
||||||
className="ml-2 size-4 cursor-pointer"
|
className="ml-2 size-4 cursor-pointer"
|
||||||
@@ -273,7 +275,9 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
|||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.value || "Search and select a backup file"}
|
<span className="truncate text-left flex-1 w-52">
|
||||||
|
{field.value || "Search and select a backup file"}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
+9
-4
@@ -29,10 +29,15 @@ import { RestoreVolumeBackups } from "./restore-volume-backups";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
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 {
|
const {
|
||||||
data: volumeBackups,
|
data: volumeBackups,
|
||||||
isLoading: isLoadingVolumeBackups,
|
isLoading: isLoadingVolumeBackups,
|
||||||
@@ -74,7 +79,7 @@ export const ShowVolumeBackups = ({ id, type = "application" }: Props) => {
|
|||||||
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RestoreVolumeBackups id={id} type={type} />
|
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,7 +233,7 @@ export const ShowVolumeBackups = ({ id, type = "application" }: Props) => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
<RestoreVolumeBackups id={id} type={type} />
|
<RestoreVolumeBackups id={id} type={type} serverId={serverId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export const RestoreBackup = ({
|
|||||||
<FormLabel className="flex items-center justify-between">
|
<FormLabel className="flex items-center justify-between">
|
||||||
Search Backup Files
|
Search Backup Files
|
||||||
{field.value && (
|
{field.value && (
|
||||||
<Badge variant="outline">
|
<Badge variant="outline" className="truncate">
|
||||||
{field.value}
|
{field.value}
|
||||||
<Copy
|
<Copy
|
||||||
className="ml-2 size-4 cursor-pointer"
|
className="ml-2 size-4 cursor-pointer"
|
||||||
@@ -439,7 +439,9 @@ export const RestoreBackup = ({
|
|||||||
!field.value && "text-muted-foreground",
|
!field.value && "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.value || "Search and select a backup file"}
|
<span className="truncate text-left flex-1 w-52">
|
||||||
|
{field.value || "Search and select a backup file"}
|
||||||
|
</span>
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
+1
@@ -338,6 +338,7 @@ const Service = (
|
|||||||
<ShowVolumeBackups
|
<ShowVolumeBackups
|
||||||
id={applicationId}
|
id={applicationId}
|
||||||
type="application"
|
type="application"
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -262,7 +262,11 @@ const Service = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="volumeBackups">
|
<TabsContent value="volumeBackups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowVolumeBackups id={composeId} type="compose" />
|
<ShowVolumeBackups
|
||||||
|
id={composeId}
|
||||||
|
type="compose"
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
findVolumeBackupById,
|
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
updateVolumeBackup,
|
updateVolumeBackup,
|
||||||
removeVolumeBackup,
|
removeVolumeBackup,
|
||||||
createVolumeBackup,
|
createVolumeBackup,
|
||||||
runVolumeBackup,
|
runVolumeBackup,
|
||||||
findDestinationById,
|
findVolumeBackupById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createVolumeBackupSchema,
|
createVolumeBackupSchema,
|
||||||
@@ -16,8 +15,12 @@ import { z } from "zod";
|
|||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { restorePostgresBackup } from "@dokploy/server/utils/restore";
|
|
||||||
import { observable } from "@trpc/server/observable";
|
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({
|
export const volumeBackupsRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
@@ -102,18 +105,76 @@ export const volumeBackupsRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
volumeBackupId: z.string().min(1),
|
backupFileName: z.string().min(1),
|
||||||
destinationId: z.string().min(1),
|
destinationId: z.string().min(1),
|
||||||
volumeName: 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 }) => {
|
.subscription(async ({ input }) => {
|
||||||
const destination = await findDestinationById(input.destinationId);
|
|
||||||
|
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
// restorePostgresBackup(postgres, destination, input, (log) => {
|
const runRestore = async () => {
|
||||||
// emit.next(log);
|
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();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import {
|
|||||||
createDeploymentVolumeBackup,
|
createDeploymentVolumeBackup,
|
||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
|
findApplicationById,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
|
findDestinationById,
|
||||||
getS3Credentials,
|
getS3Credentials,
|
||||||
normalizeS3Path,
|
normalizeS3Path,
|
||||||
paths,
|
paths,
|
||||||
@@ -122,55 +124,338 @@ const backupVolume = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const restoreVolume = async (
|
export const restoreVolume = async (
|
||||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
id: string,
|
||||||
|
destinationId: string,
|
||||||
|
volumeName: string,
|
||||||
|
backupFileName: string,
|
||||||
|
serverId: string,
|
||||||
|
serviceType: "application" | "compose",
|
||||||
) => {
|
) => {
|
||||||
const { serviceType, volumeName } = volumeBackup;
|
const destination = await findDestinationById(destinationId);
|
||||||
|
|
||||||
const serverId =
|
|
||||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
|
||||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
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
|
set -e
|
||||||
docker volume rm ${volumeName} --force
|
echo "Volume name: ${volumeName}"
|
||||||
echo "Volume name: ${volumeName}"
|
echo "Backup file name: ${backupFileName}"
|
||||||
echo "Volume backup path: ${volumeBackupPath}"
|
echo "Volume backup path: ${volumeBackupPath}"
|
||||||
echo "Starting volume restore"
|
echo "Downloading backup from S3..."
|
||||||
docker run --rm \
|
mkdir -p ${volumeBackupPath}
|
||||||
-v ${volumeName}:/volume_data \
|
${downloadCommand}
|
||||||
-v ${volumeBackupPath}:/backup \
|
echo "Download completed ✅"
|
||||||
ubuntu \
|
echo "Creating new volume and restoring data..."
|
||||||
bash -c "cd /volume_data && tar xvf /backup/${volumeName}.tar ."
|
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") {
|
if (serviceType === "application") {
|
||||||
|
const application = await findApplicationById(id);
|
||||||
return `
|
return `
|
||||||
docker service scale ${volumeBackup.application?.appName}=0
|
echo "=== VOLUME RESTORE FOR APPLICATION ==="
|
||||||
${baseCommand}
|
echo "Application: ${application.appName}"
|
||||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
${checkVolumeCommand}
|
||||||
docker service scale ${volumeBackup.application?.appName}=$ACTUAL_REPLICAS
|
`;
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serviceType === "compose") {
|
if (serviceType === "compose") {
|
||||||
const compose = await findComposeById(
|
const compose = await findComposeById(id);
|
||||||
volumeBackup.compose?.composeId || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
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 `
|
return `
|
||||||
ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}")
|
echo "=== VOLUME RESTORE FOR COMPOSE ==="
|
||||||
docker stop $ID
|
echo "Compose: ${compose.appName}"
|
||||||
${baseCommand}
|
echo "Compose Type: ${compose.composeType}"
|
||||||
docker start $ID
|
${checkVolumeCommand}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback for unknown service types
|
||||||
|
return checkVolumeCommand;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user