Enhance RestoreBackup component to support compose backups by adding a database type selection and metadata handling. Update related API routes and schemas to accommodate new backup types, ensuring flexibility for various database configurations. Modify UI components to allow dynamic input for service names and database credentials based on the selected database type.

This commit is contained in:
Mauricio Siu
2025-04-28 02:17:42 -06:00
parent ddcb22dff9
commit 5055994bd3
11 changed files with 585 additions and 132 deletions

View File

@@ -21,7 +21,7 @@ export const runComposeBackup = async (
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
const command = `docker ps --filter "status=running" --filter "label=dokploy.backup.id=${backup.backupId}" --format "{{.ID}}" | head -n 1`;
const command = getFindContainerCommand(compose, backup.serviceName || "");
if (compose.serverId) {
const { stdout } = await execAsyncRemote(compose.serverId, command);
if (!stdout) {
@@ -88,3 +88,26 @@ export const runComposeBackup = async (
throw error;
}
};
export const getFindContainerCommand = (
compose: Compose,
serviceName: string,
) => {
const { appName, composeType } = compose;
const labels =
composeType === "stack"
? {
namespace: `label=com.docker.stack.namespace=${appName}`,
service: `label=com.docker.swarm.service.name=${appName}_${serviceName}`,
}
: {
project: `label=com.docker.compose.project=${appName}`,
service: `label=com.docker.compose.service=${serviceName}`,
};
const command = `docker ps --filter "status=running" \
--filter "${Object.values(labels).join('" --filter "')}" \
--format "{{.ID}}" | head -n 1`;
return command.trim();
};

View File

@@ -22,15 +22,15 @@ import { spawnAsync } from "../process/spawnAsync";
export type ComposeNested = InferResultType<
"compose",
{ project: true; mounts: true; domains: true; backups: true }
{ project: true; mounts: true; domains: true }
>;
export const buildCompose = async (compose: ComposeNested, logPath: string) => {
const writeStream = createWriteStream(logPath, { flags: "a" });
const { sourceType, appName, mounts, composeType } = compose;
const { sourceType, appName, mounts, composeType, domains } = compose;
try {
const { COMPOSE_PATH } = paths();
const command = createCommand(compose);
await writeDomainsToCompose(compose);
await writeDomainsToCompose(compose, domains);
createEnvFile(compose);
if (compose.isolatedDeployment) {

View File

@@ -38,8 +38,6 @@ import type {
PropertiesNetworks,
} from "./types";
import { encodeBase64 } from "./utils";
import type { Backup } from "@dokploy/server/services/backup";
import { createBackupLabels } from "./backup";
export const cloneCompose = async (compose: Compose) => {
if (compose.sourceType === "github") {
@@ -134,13 +132,13 @@ export const readComposeFile = async (compose: Compose) => {
};
export const writeDomainsToCompose = async (
compose: Compose & { domains: Domain[]; backups: Backup[] },
compose: Compose,
domains: Domain[],
) => {
const { domains, backups } = compose;
if (!domains.length && !backups.length) {
if (!domains.length) {
return;
}
const composeConverted = await addDomainToCompose(compose);
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
const composeString = dump(composeConverted, { lineWidth: 1000 });
@@ -152,7 +150,7 @@ export const writeDomainsToCompose = async (
};
export const writeDomainsToComposeRemote = async (
compose: Compose & { domains: Domain[]; backups: Backup[] },
compose: Compose,
domains: Domain[],
logPath: string,
) => {
@@ -161,7 +159,7 @@ export const writeDomainsToComposeRemote = async (
}
try {
const composeConverted = await addDomainToCompose(compose);
const composeConverted = await addDomainToCompose(compose, domains);
const path = getComposePath(compose);
if (!composeConverted) {
@@ -182,20 +180,22 @@ exit 1;
`;
}
};
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
export const addDomainToCompose = async (
compose: Compose & { domains: Domain[]; backups: Backup[] },
compose: Compose,
domains: Domain[],
) => {
const { appName, domains, backups } = compose;
const { appName } = compose;
let result: ComposeSpecification | null;
if (compose.serverId) {
result = await loadDockerComposeRemote(compose);
result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor
} else {
result = await loadDockerCompose(compose);
}
if (!result || (domains.length === 0 && backups.length === 0)) {
if (!result || domains.length === 0) {
return null;
}
@@ -210,7 +210,6 @@ export const addDomainToCompose = async (
result = randomized;
}
// Add domains to the compose
for (const domain of domains) {
const { serviceName, https } = domain;
if (!serviceName) {
@@ -265,38 +264,6 @@ export const addDomainToCompose = async (
}
}
// Add backups to the compose
for (const backup of backups) {
const { backupId, serviceName, enabled } = backup;
if (!enabled) {
continue;
}
if (!serviceName) {
throw new Error(
"Service name not found, please check the backups to use a valid service name",
);
}
if (!result?.services?.[serviceName]) {
throw new Error(`The service ${serviceName} not found in the compose`);
}
const backupLabels = createBackupLabels(backupId);
if (!result.services[serviceName].labels) {
result.services[serviceName].labels = [];
}
result.services[serviceName].labels = [
...(Array.isArray(result.services[serviceName].labels)
? result.services[serviceName].labels
: []),
...backupLabels,
];
}
// Add dokploy-network to the root of the compose file
if (!compose.isolatedDeployment) {
result.networks = addDokployNetworkToRoot(result.networks);

View File

@@ -0,0 +1,99 @@
import type { Destination } from "@dokploy/server/services/destination";
import type { Compose } from "@dokploy/server/services/compose";
import { getS3Credentials } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import type { Backup } from "@dokploy/server/services/backup";
import { getFindContainerCommand } from "../backups/compose";
export const restoreComposeBackup = async (
compose: Compose,
destination: Destination,
database: string,
backupFile: string,
metadata: Backup["metadata"] & { serviceName: string },
emit: (log: string) => void,
) => {
try {
console.log({ metadata });
const { serverId } = compose;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupFile}`;
const command = getFindContainerCommand(compose, metadata.serviceName);
console.log("command", command);
let containerId = "";
if (serverId) {
const { stdout, stderr } = await execAsyncRemote(serverId, command);
emit(stdout);
emit(stderr);
containerId = stdout.trim();
} else {
const { stdout, stderr } = await execAsync(command);
console.log("stdout", stdout);
console.log("stderr", stderr);
emit(stdout);
emit(stderr);
containerId = stdout.trim();
}
let restoreCommand = "";
if (metadata.postgres) {
restoreCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerId} pg_restore -U ${metadata.postgres.databaseUser} -d ${database} --clean --if-exists`;
} else if (metadata.mariadb) {
restoreCommand = `
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerId} mariadb -u ${metadata.mariadb.databaseUser} -p${metadata.mariadb.databasePassword} ${database}
`;
} else if (metadata.mysql) {
restoreCommand = `
rclone cat ${rcloneFlags.join(" ")} "${backupPath}" | gunzip | docker exec -i ${containerId} mysql -u root -p${metadata.mysql.databaseRootPassword} ${database}
`;
} else if (metadata.mongo) {
const tempDir = "/tmp/dokploy-restore";
const fileName = backupFile.split("/").pop() || "backup.dump.gz";
const decompressedName = fileName.replace(".gz", "");
restoreCommand = `\
rm -rf ${tempDir} && \
mkdir -p ${tempDir} && \
rclone copy ${rcloneFlags.join(" ")} "${backupPath}" ${tempDir} && \
cd ${tempDir} && \
gunzip -f "${fileName}" && \
docker exec -i ${containerId} mongorestore --username ${metadata.mongo.databaseUser} --password ${metadata.mongo.databasePassword} --authenticationDatabase admin --db ${database} --archive < "${decompressedName}" && \
rm -rf ${tempDir}`;
}
emit("Starting restore...");
emit(`Backup path: ${backupPath}`);
emit(`Executing command: ${restoreCommand}`);
if (serverId) {
const { stdout, stderr } = await execAsyncRemote(
serverId,
restoreCommand,
);
emit(stdout);
emit(stderr);
} else {
const { stdout, stderr } = await execAsync(restoreCommand);
console.log("stdout", stdout);
console.log("stderr", stderr);
emit(stdout);
emit(stderr);
}
emit("Restore completed successfully!");
} catch (error) {
console.error(error);
emit(
`Error: ${
error instanceof Error ? error.message : "Error restoring mongo backup"
}`,
);
throw new Error(
error instanceof Error ? error.message : "Error restoring mongo backup",
);
}
};

View File

@@ -3,3 +3,4 @@ export { restoreMySqlBackup } from "./mysql";
export { restoreMariadbBackup } from "./mariadb";
export { restoreMongoBackup } from "./mongo";
export { restoreWebServerBackup } from "./web-server";
export { restoreComposeBackup } from "./compose";