mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-18 12:25:25 +02:00
Merge branch 'canary' into 1853-external-volume-should-not-be-isolated
This commit is contained in:
@@ -37,7 +37,8 @@ export const startLogCleanup = async (
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
console.error("Error starting log cleanup:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runComposeBackup = async (
|
||||
compose: Compose,
|
||||
@@ -15,7 +15,7 @@ export const runComposeBackup = async (
|
||||
) => {
|
||||
const { projectId, name } = compose;
|
||||
const project = await findProjectById(projectId);
|
||||
const { prefix } = backup;
|
||||
const { prefix, databaseType } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
@@ -46,9 +46,10 @@ export const runComposeBackup = async (
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
databaseType: getDatabaseType(databaseType),
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -57,14 +58,31 @@ export const runComposeBackup = async (
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
databaseType: getDatabaseType(databaseType),
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getDatabaseType = (databaseType: BackupSchedule["databaseType"]) => {
|
||||
if (databaseType === "mongo") {
|
||||
return "mongodb";
|
||||
}
|
||||
if (databaseType === "postgres") {
|
||||
return "postgres";
|
||||
}
|
||||
if (databaseType === "mariadb") {
|
||||
return "mariadb";
|
||||
}
|
||||
if (databaseType === "mysql") {
|
||||
return "mysql";
|
||||
}
|
||||
return "mongodb";
|
||||
};
|
||||
|
||||
@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
import { member } from "@dokploy/server/db/schema";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
|
||||
export const initCronJobs = async () => {
|
||||
console.log("Setting up cron jobs....");
|
||||
@@ -88,6 +88,7 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
@@ -48,6 +48,7 @@ export const runMariadbBackup = async (
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
@@ -60,6 +61,7 @@ export const runMariadbBackup = async (
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
const { projectId, name } = mongo;
|
||||
@@ -46,6 +46,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
databaseType: "mongodb",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
@@ -58,6 +59,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { MySql } from "@dokploy/server/services/mysql";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
const { projectId, name } = mysql;
|
||||
@@ -47,6 +47,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
databaseType: "mysql",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
@@ -59,6 +60,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
|
||||
export const runPostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
@@ -50,6 +50,7 @@ export const runPostgresBackup = async (
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
@@ -62,6 +63,7 @@ export const runPostgresBackup = async (
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { logger } from "@dokploy/server/lib/logger";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
import { keepLatestNBackups } from ".";
|
||||
import { runComposeBackup } from "./compose";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
import { runPostgresBackup } from "./postgres";
|
||||
import { runWebServerBackup } from "./web-server";
|
||||
import { runComposeBackup } from "./compose";
|
||||
|
||||
export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
const {
|
||||
@@ -222,6 +223,17 @@ export const getBackupCommand = (
|
||||
) => {
|
||||
const containerSearch = getContainerSearchCommand(backup);
|
||||
const backupCommand = generateBackupCommand(backup);
|
||||
|
||||
logger.info(
|
||||
{
|
||||
containerSearch,
|
||||
backupCommand,
|
||||
rcloneCommand,
|
||||
logPath,
|
||||
},
|
||||
`Executing backup command: ${backup.databaseType} ${backup.backupType}`,
|
||||
);
|
||||
|
||||
return `
|
||||
set -eo pipefail;
|
||||
echo "[$(date)] Starting backup process..." >> ${logPath};
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
if (IS_CLOUD) {
|
||||
@@ -51,13 +51,23 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
|
||||
const postgresContainerId = containerId.trim();
|
||||
|
||||
const postgresCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`;
|
||||
// First dump the database inside the container
|
||||
const dumpCommand = `docker exec ${postgresContainerId} pg_dump -v -Fc -U dokploy -d dokploy -f /tmp/database.sql`;
|
||||
writeStream.write(`Running dump command: ${dumpCommand}\n`);
|
||||
await execAsync(dumpCommand);
|
||||
|
||||
writeStream.write(`Running command: ${postgresCommand}\n`);
|
||||
await execAsync(postgresCommand);
|
||||
// Then copy the file from the container to host
|
||||
const copyCommand = `docker cp ${postgresContainerId}:/tmp/database.sql ${tempDir}/database.sql`;
|
||||
writeStream.write(`Copying database dump: ${copyCommand}\n`);
|
||||
await execAsync(copyCommand);
|
||||
|
||||
// Clean up the temp file in the container
|
||||
const cleanupCommand = `docker exec ${postgresContainerId} rm -f /tmp/database.sql`;
|
||||
writeStream.write(`Cleaning up temp file: ${cleanupCommand}\n`);
|
||||
await execAsync(cleanupCommand);
|
||||
|
||||
await execAsync(
|
||||
`rsync -av --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||
);
|
||||
|
||||
writeStream.write("Copied filesystem to temp directory\n");
|
||||
@@ -77,7 +87,11 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
return true;
|
||||
} finally {
|
||||
await execAsync(`rm -rf ${tempDir}`);
|
||||
try {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
} catch (cleanupError) {
|
||||
console.error("Cleanup error:", cleanupError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Backup error:", error);
|
||||
|
||||
@@ -190,9 +190,10 @@ const createEnvFile = (compose: ComposeNested) => {
|
||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
let envContent = env || "";
|
||||
let envContent = `APP_NAME=${appName}\n`;
|
||||
envContent += env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
@@ -219,9 +220,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
|
||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||
|
||||
let envContent = env || "";
|
||||
let envContent = `APP_NAME=${appName}\n`;
|
||||
envContent += env || "";
|
||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||
envContent += "\nDOCKER_CONFIG=/root/.docker";
|
||||
}
|
||||
|
||||
if (compose.randomize) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
@@ -184,6 +183,7 @@ export const mechanizeDockerContainer = async (
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Ports: ports.map((port) => ({
|
||||
PublishMode: port.publishMode,
|
||||
Protocol: port.protocol,
|
||||
TargetPort: port.targetPort,
|
||||
PublishedPort: port.publishedPort,
|
||||
@@ -204,26 +204,30 @@ export const mechanizeDockerContainer = async (
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (_error: unknown) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
const getImageName = (application: ApplicationNested) => {
|
||||
const { appName, sourceType, dockerImage, registry } = application;
|
||||
|
||||
const imageName = `${appName}:latest`;
|
||||
if (sourceType === "docker") {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
}
|
||||
|
||||
if (registry) {
|
||||
return join(registry.registryUrl, registry.imagePrefix || "", appName);
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
return registryTag;
|
||||
}
|
||||
|
||||
return `${appName}:latest`;
|
||||
return imageName;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
export const getAuthConfig = (application: ApplicationNested) => {
|
||||
const { registry, username, password, sourceType, registryUrl } = application;
|
||||
|
||||
if (sourceType === "docker") {
|
||||
|
||||
@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
@@ -81,10 +84,10 @@ export const buildRailpack = async (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const env: { [key: string]: string } = {};
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const exportEnvs = [];
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
exportEnvs.push(`export ${key}=${value}`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
exportEnvs.push(`export ${key}='${value}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,21 +7,58 @@ import type { ApplicationNested } from ".";
|
||||
import { createFile, getCreateFileCommand } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
|
||||
const nginxSpaConfig = `
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stderr;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const buildStatic = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { publishDirectory } = application;
|
||||
const { publishDirectory, isStaticSpa } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
if (isStaticSpa) {
|
||||
createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig);
|
||||
}
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
".dockerignore",
|
||||
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||
);
|
||||
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
'CMD ["nginx", "-g", "daemon off;"]',
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import path, { join } from "node:path";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
@@ -13,19 +12,22 @@ export const uploadImage = async (
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
||||
// For ghcr.io: ghcr.io/username/image:tag
|
||||
// For docker.io: docker.io/username/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`,
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL} | ${registryTag}\n`,
|
||||
);
|
||||
const loginCommand = spawnAsync(
|
||||
"docker",
|
||||
@@ -67,15 +69,16 @@ export const uploadImageRemoteCommand = (
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = path
|
||||
.join(registryUrl, join(imagePrefix || "", imageName))
|
||||
.replace(/\/+/g, "/");
|
||||
// Build registry tag in correct format: registry.com/owner/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
const command = `
|
||||
|
||||
@@ -98,7 +98,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -152,7 +152,7 @@ ${command ?? "wait $MONGOD_PID"}`;
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ export const loadDockerComposeRemote = async (
|
||||
if (!stdout) return null;
|
||||
const parsedConfig = load(stdout) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -301,6 +301,8 @@ export const createDomainLabels = (
|
||||
certificateType,
|
||||
path,
|
||||
customCertResolver,
|
||||
stripPath,
|
||||
internalPath,
|
||||
} = domain;
|
||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||
const labels = [
|
||||
@@ -310,6 +312,34 @@ export const createDomainLabels = (
|
||||
`traefik.http.routers.${routerName}.service=${routerName}`,
|
||||
];
|
||||
|
||||
// Validate stripPath - it should only be used when path is defined and not "/"
|
||||
if (stripPath) {
|
||||
if (!path || path === "/") {
|
||||
console.warn(
|
||||
`stripPath is enabled but path is not defined or is "/" for domain ${host}`,
|
||||
);
|
||||
} else {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.stripprefix.prefixes=${path}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate internalPath - ensure it's a valid path format
|
||||
if (internalPath && internalPath !== "/") {
|
||||
if (!internalPath.startsWith("/")) {
|
||||
console.warn(
|
||||
`internalPath "${internalPath}" should start with "/" and not be empty for domain ${host}`,
|
||||
);
|
||||
} else {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
labels.push(
|
||||
`traefik.http.middlewares.${middlewareName}.addprefix.prefix=${internalPath}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (entrypoint === "web" && https) {
|
||||
labels.push(
|
||||
`traefik.http.routers.${routerName}.middlewares=redirect-to-https@file`,
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Readable } from "node:stream";
|
||||
import { docker, paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import { parse } from "dotenv";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
@@ -13,7 +14,6 @@ import type { RedisNested } from "../databases/redis";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
@@ -101,7 +101,7 @@ export const containerExists = async (containerName: string) => {
|
||||
try {
|
||||
await container.inspect();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -212,7 +212,7 @@ export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||
};
|
||||
|
||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||
const command = "docker system prune --all --force --volumes";
|
||||
const command = "docker system prune --force --volumes";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
@@ -279,6 +279,17 @@ export const prepareEnvironmentVariables = (
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const parseEnvironmentKeyValuePair = (
|
||||
pair: string,
|
||||
): [string, string] => {
|
||||
const [key, ...valueParts] = pair.split("=");
|
||||
if (!key || !valueParts.length) {
|
||||
throw new Error(`Invalid environment variable pair: ${pair}`);
|
||||
}
|
||||
|
||||
return [key, valueParts.join("=")];
|
||||
};
|
||||
|
||||
export const getEnviromentVariablesObject = (
|
||||
input: string | null,
|
||||
projectEnv?: string | null,
|
||||
@@ -288,7 +299,7 @@ export const getEnviromentVariablesObject = (
|
||||
const jsonObject: Record<string, string> = {};
|
||||
|
||||
for (const pair of envs) {
|
||||
const [key, value] = pair.split("=");
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
jsonObject[key] = value;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||
...gpuInfo,
|
||||
...cudaInfo,
|
||||
};
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
return {
|
||||
driverInstalled: false,
|
||||
driverVersion: undefined,
|
||||
@@ -315,7 +315,7 @@ const setupLocalServer = async (daemonConfig: any) => {
|
||||
|
||||
try {
|
||||
await execAsync(setupCommands);
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
type,
|
||||
errorMessage,
|
||||
organizationId,
|
||||
databaseName,
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
@@ -26,6 +27,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
databaseName: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
@@ -90,6 +92,11 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📂`", "Database Name"),
|
||||
value: databaseName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
@@ -136,6 +143,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("❔", `Type: ${databaseType}`)}` +
|
||||
`${decorate("📂", `Database Name: ${databaseName}`)}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
@@ -150,7 +158,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Database Name:</b> ${databaseName}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
@@ -191,6 +199,10 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
value: databaseType,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Database Name",
|
||||
value: databaseName,
|
||||
},
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
|
||||
@@ -73,7 +73,7 @@ export const cloneBitbucketRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -155,7 +155,7 @@ export const getGiteaCloneCommand = async (
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
@@ -232,7 +232,7 @@ export const cloneGiteaRepository = async (
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -149,7 +149,7 @@ export const cloneGithubRepository = async ({
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -132,7 +132,7 @@ export const cloneGitlabRepository = async (
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
@@ -152,7 +152,7 @@ export const cloneGitlabRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
@@ -246,34 +246,18 @@ export const getGitlabRepositories = async (gitlabId?: string) => {
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const allProjects = await validateGitlabProvider(gitlabProvider);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const filteredRepos = allProjects.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
const groupName = gitlabProvider.groupName?.toLowerCase();
|
||||
|
||||
if (groupName) {
|
||||
const isIncluded = groupName
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) => full_path.toLowerCase().includes(name));
|
||||
|
||||
return isIncluded && kind === "group";
|
||||
.some((name) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
@@ -432,23 +416,7 @@ export const testGitlabConnection = async (
|
||||
|
||||
const gitlabProvider = await findGitlabById(gitlabId);
|
||||
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${0}&per_page=${100}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const repositories = await response.json();
|
||||
const repositories = await validateGitlabProvider(gitlabProvider);
|
||||
|
||||
const filteredRepos = repositories.filter((repo: any) => {
|
||||
const { full_path, kind } = repo.namespace;
|
||||
@@ -456,10 +424,56 @@ export const testGitlabConnection = async (
|
||||
if (groupName) {
|
||||
return groupName
|
||||
.split(",")
|
||||
.some((name) => full_path.toLowerCase().includes(name));
|
||||
.some((name) =>
|
||||
full_path.toLowerCase().startsWith(name.trim().toLowerCase()),
|
||||
);
|
||||
}
|
||||
return kind === "user";
|
||||
});
|
||||
|
||||
return filteredRepos.length;
|
||||
};
|
||||
|
||||
export const validateGitlabProvider = async (gitlabProvider: Gitlab) => {
|
||||
try {
|
||||
const allProjects = [];
|
||||
let page = 1;
|
||||
const perPage = 100; // GitLab's max per page is 100
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${gitlabProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Failed to fetch repositories: ${response.statusText}`,
|
||||
});
|
||||
}
|
||||
|
||||
const projects = await response.json();
|
||||
|
||||
if (projects.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allProjects.push(...projects);
|
||||
page++;
|
||||
|
||||
const total = response.headers.get("x-total");
|
||||
if (total && allProjects.length >= Number.parseInt(total)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allProjects;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
interface DatabaseCredentials {
|
||||
databaseUser?: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restoreMariadbBackup = async (
|
||||
mariadb: Mariadb,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restoreMongoBackup = async (
|
||||
mongo: Mongo,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { MySql } from "@dokploy/server/services/mysql";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restoreMySqlBackup = async (
|
||||
mysql: MySql,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getRestoreCommand } from "./utils";
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { z } from "zod";
|
||||
|
||||
export const restorePostgresBackup = async (
|
||||
postgres: Postgres,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
|
||||
database: string,
|
||||
databaseUser: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} --clean --if-exists"`;
|
||||
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
|
||||
};
|
||||
|
||||
export const getMariadbRestoreCommand = (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { paths, IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
export const restoreWebServerBackup = async (
|
||||
destination: Destination,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from "../../db/index";
|
||||
import { schedules } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db/index";
|
||||
import { scheduleJob } from "./utils";
|
||||
|
||||
export const initSchedules = async () => {
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Schedule } from "@dokploy/server/db/schema/schedule";
|
||||
import {
|
||||
createDeploymentSchedule,
|
||||
updateDeployment,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
|
||||
import { findScheduleById } from "@dokploy/server/services/schedule";
|
||||
import { scheduledJobs, scheduleJob as scheduleJobNode } from "node-schedule";
|
||||
import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
|
||||
import { getComposeContainer, getServiceContainer } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import path from "node:path";
|
||||
|
||||
export const scheduleJob = (schedule: Schedule) => {
|
||||
const { cronExpression, scheduleId } = schedule;
|
||||
@@ -113,8 +116,16 @@ export const runCommand = async (scheduleId: string) => {
|
||||
await spawnAsync(
|
||||
"bash",
|
||||
["-c", "./script.sh"],
|
||||
(data) => {
|
||||
async (data) => {
|
||||
if (writeStream.writable) {
|
||||
// we need to extract the PID and Schedule ID from the data
|
||||
const pid = data?.match(/PID: (\d+)/)?.[1];
|
||||
|
||||
if (pid) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
pid,
|
||||
});
|
||||
}
|
||||
writeStream.write(data);
|
||||
}
|
||||
},
|
||||
@@ -133,13 +144,21 @@ export const runCommand = async (scheduleId: string) => {
|
||||
const command = `
|
||||
set -e
|
||||
echo "Running script" >> ${deployment.logPath};
|
||||
bash -c ${fullPath}/script.sh >> ${deployment.logPath} 2>> ${deployment.logPath} || {
|
||||
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
|
||||
echo "❌ Command failed" >> ${deployment.logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Command executed successfully" >> ${deployment.logPath};
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
await execAsyncRemote(serverId, command, async (data) => {
|
||||
// we need to extract the PID and Schedule ID from the data
|
||||
const pid = data?.match(/PID: (\d+)/)?.[1];
|
||||
if (pid) {
|
||||
await updateDeployment(deployment.deploymentId, {
|
||||
pid,
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
throw error;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fs, { writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { dump, load } from "js-yaml";
|
||||
@@ -67,7 +69,7 @@ export const removeTraefikConfig = async (
|
||||
if (fs.existsSync(configPath)) {
|
||||
await fs.promises.unlink(configPath);
|
||||
}
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const removeTraefikConfigRemote = async (
|
||||
@@ -78,7 +80,7 @@ export const removeTraefikConfigRemote = async (
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
await execAsyncRemote(serverId, `rm ${configPath}`);
|
||||
} catch (_error) {}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
export const loadOrCreateConfig = (appName: string): FileConfig => {
|
||||
@@ -110,7 +112,7 @@ export const loadOrCreateConfigRemote = async (
|
||||
http: { routers: {}, services: {} },
|
||||
};
|
||||
return parsedConfig;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return fileConfig;
|
||||
}
|
||||
};
|
||||
@@ -132,44 +134,45 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
|
||||
const { stdout } = await execAsyncRemote(serverId, `cat ${configPath}`);
|
||||
if (!stdout) return null;
|
||||
return stdout;
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const readMonitoringConfig = (readAll = false) => {
|
||||
export const readMonitoringConfig = async (readAll = false) => {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||
if (fs.existsSync(configPath)) {
|
||||
if (!readAll) {
|
||||
// Read first 500 lines
|
||||
// Read first 500 lines using streams
|
||||
let content = "";
|
||||
let chunk = "";
|
||||
let validCount = 0;
|
||||
|
||||
for (const char of fs.readFileSync(configPath, "utf8")) {
|
||||
chunk += char;
|
||||
if (char === "\n") {
|
||||
try {
|
||||
const trimmed = chunk.trim();
|
||||
if (
|
||||
trimmed !== "" &&
|
||||
trimmed.startsWith("{") &&
|
||||
trimmed.endsWith("}")
|
||||
) {
|
||||
const log = JSON.parse(trimmed);
|
||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||
content += chunk;
|
||||
validCount++;
|
||||
if (validCount >= 500) {
|
||||
break;
|
||||
}
|
||||
const fileStream = createReadStream(configPath, { encoding: "utf8" });
|
||||
const readline = createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
|
||||
for await (const line of readline) {
|
||||
try {
|
||||
const trimmed = line.trim();
|
||||
if (
|
||||
trimmed !== "" &&
|
||||
trimmed.startsWith("{") &&
|
||||
trimmed.endsWith("}")
|
||||
) {
|
||||
const log = JSON.parse(trimmed);
|
||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||
content += `${line}\n`;
|
||||
validCount++;
|
||||
if (validCount >= 500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid JSON
|
||||
}
|
||||
chunk = "";
|
||||
} catch {
|
||||
// Ignore invalid JSON
|
||||
}
|
||||
}
|
||||
return content;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
writeTraefikConfigRemote,
|
||||
} from "./application";
|
||||
import type { FileConfig, HttpRouter } from "./file-types";
|
||||
import { createPathMiddlewares, removePathMiddlewares } from "./middleware";
|
||||
|
||||
export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
const { appName } = app;
|
||||
@@ -46,6 +47,8 @@ export const manageDomain = async (app: ApplicationNested, domain: Domain) => {
|
||||
|
||||
config.http.services[serviceName] = createServiceConfig(appName, domain);
|
||||
|
||||
await createPathMiddlewares(app, domain);
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, appName, app.serverId);
|
||||
} else {
|
||||
@@ -80,6 +83,8 @@ export const removeDomain = async (
|
||||
delete config.http.services[serviceKey];
|
||||
}
|
||||
|
||||
await removePathMiddlewares(application, uniqueKey);
|
||||
|
||||
// verify if is the last router if so we delete the router
|
||||
if (
|
||||
config?.http?.routers &&
|
||||
@@ -107,7 +112,8 @@ export const createRouterConfig = async (
|
||||
const { appName, redirects, security } = app;
|
||||
const { certificateType } = domain;
|
||||
|
||||
const { host, path, https, uniqueConfigKey } = domain;
|
||||
const { host, path, https, uniqueConfigKey, internalPath, stripPath } =
|
||||
domain;
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${host}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
service: `${appName}-service-${uniqueConfigKey}`,
|
||||
@@ -115,6 +121,17 @@ export const createRouterConfig = async (
|
||||
entryPoints: [entryPoint],
|
||||
};
|
||||
|
||||
// Add path rewriting middleware if needed
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const pathMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(pathMiddleware);
|
||||
}
|
||||
|
||||
if (stripPath && path && path !== "/") {
|
||||
const stripMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
routerConfig.middlewares?.push(stripMiddleware);
|
||||
}
|
||||
|
||||
if (entryPoint === "web" && https) {
|
||||
routerConfig.middlewares = ["redirect-to-https"];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ApplicationNested } from "../builders";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { writeTraefikConfigRemote } from "./application";
|
||||
import type { FileConfig } from "./file-types";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
|
||||
export const addMiddleware = (config: FileConfig, middlewareName: string) => {
|
||||
if (config.http?.routers) {
|
||||
@@ -105,3 +106,97 @@ export const writeMiddleware = <T>(config: T) => {
|
||||
const newYamlContent = dump(config);
|
||||
writeFileSync(configPath, newYamlContent, "utf8");
|
||||
};
|
||||
|
||||
export const createPathMiddlewares = async (
|
||||
app: ApplicationNested,
|
||||
domain: Domain,
|
||||
) => {
|
||||
let config: FileConfig;
|
||||
|
||||
if (app.serverId) {
|
||||
try {
|
||||
config = await loadRemoteMiddlewares(app.serverId);
|
||||
} catch {
|
||||
config = { http: { middlewares: {} } };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
config = loadMiddlewares<FileConfig>();
|
||||
} catch {
|
||||
config = { http: { middlewares: {} } };
|
||||
}
|
||||
}
|
||||
|
||||
const { appName } = app;
|
||||
const { uniqueConfigKey, internalPath, stripPath, path } = domain;
|
||||
|
||||
if (!config.http) {
|
||||
config.http = { middlewares: {} };
|
||||
}
|
||||
if (!config.http.middlewares) {
|
||||
config.http.middlewares = {};
|
||||
}
|
||||
|
||||
// Add internal path prefix middleware
|
||||
if (internalPath && internalPath !== "/" && internalPath !== path) {
|
||||
const middlewareName = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
config.http.middlewares[middlewareName] = {
|
||||
addPrefix: {
|
||||
prefix: internalPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Strip external path middleware if needed
|
||||
if (stripPath && path && path !== "/") {
|
||||
const middlewareName = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
config.http.middlewares[middlewareName] = {
|
||||
stripPrefix: {
|
||||
prefixes: [path],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", app.serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
};
|
||||
|
||||
export const removePathMiddlewares = async (
|
||||
app: ApplicationNested,
|
||||
uniqueConfigKey: number,
|
||||
) => {
|
||||
let config: FileConfig;
|
||||
|
||||
if (app.serverId) {
|
||||
try {
|
||||
config = await loadRemoteMiddlewares(app.serverId);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
config = loadMiddlewares<FileConfig>();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { appName } = app;
|
||||
|
||||
if (config.http?.middlewares) {
|
||||
const addPrefixMiddleware = `addprefix-${appName}-${uniqueConfigKey}`;
|
||||
const stripPrefixMiddleware = `stripprefix-${appName}-${uniqueConfigKey}`;
|
||||
|
||||
delete config.http.middlewares[addPrefixMiddleware];
|
||||
delete config.http.middlewares[stripPrefixMiddleware];
|
||||
}
|
||||
|
||||
if (app.serverId) {
|
||||
await writeTraefikConfigRemote(config, "middlewares", app.serverId);
|
||||
} else {
|
||||
writeMiddleware(config);
|
||||
}
|
||||
};
|
||||
|
||||
91
packages/server/src/utils/volume-backups/backup.ts
Normal file
91
packages/server/src/utils/volume-backups/backup.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import { normalizeS3Path } from "../backups/utils";
|
||||
import { getS3Credentials } from "../backups/utils";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
|
||||
export const backupVolume = async (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
) => {
|
||||
const { serviceType, volumeName, turnOff, prefix } = volumeBackup;
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const destination = volumeBackup.destination;
|
||||
const backupFileName = `${volumeName}-${new Date().toISOString()}.tar`;
|
||||
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
const rcloneFlags = getS3Credentials(volumeBackup.destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeBackup.appName);
|
||||
|
||||
const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`;
|
||||
|
||||
const baseCommand = `
|
||||
set -e
|
||||
echo "Volume name: ${volumeName}"
|
||||
echo "Backup file name: ${backupFileName}"
|
||||
echo "Turning off volume backup: ${turnOff ? "Yes" : "No"}"
|
||||
echo "Starting volume backup"
|
||||
echo "Dir: ${volumeBackupPath}"
|
||||
docker run --rm \
|
||||
-v ${volumeName}:/volume_data \
|
||||
-v ${volumeBackupPath}:/backup \
|
||||
ubuntu \
|
||||
bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ."
|
||||
echo "Volume backup done ✅"
|
||||
echo "Starting upload to S3..."
|
||||
${rcloneCommand}
|
||||
echo "Upload to S3 done ✅"
|
||||
`;
|
||||
|
||||
if (!turnOff) {
|
||||
return baseCommand;
|
||||
}
|
||||
|
||||
if (serviceType === "application") {
|
||||
return `
|
||||
echo "Stopping application to 0 replicas"
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service scale ${volumeBackup.application?.appName}=0
|
||||
${baseCommand}
|
||||
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
||||
docker service scale ${volumeBackup.application?.appName}=$ACTUAL_REPLICAS
|
||||
`;
|
||||
}
|
||||
if (serviceType === "compose") {
|
||||
const compose = await findComposeById(
|
||||
volumeBackup.compose?.composeId || "",
|
||||
);
|
||||
let stopCommand = "";
|
||||
let startCommand = "";
|
||||
|
||||
if (compose.composeType === "stack") {
|
||||
stopCommand = `
|
||||
echo "Stopping compose to 0 replicas"
|
||||
echo "Service name: ${compose.appName}_${volumeBackup.serviceName}"
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${compose.appName}_${volumeBackup.serviceName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service scale ${compose.appName}_${volumeBackup.serviceName}=0`;
|
||||
startCommand = `
|
||||
echo "Starting compose to $ACTUAL_REPLICAS replicas"
|
||||
docker service scale ${compose.appName}_${volumeBackup.serviceName}=$ACTUAL_REPLICAS`;
|
||||
} else {
|
||||
stopCommand = `
|
||||
echo "Stopping compose container"
|
||||
ID=$(docker ps -q --filter "label=com.docker.compose.project=${compose.appName}" --filter "label=com.docker.compose.service=${volumeBackup.serviceName}")
|
||||
docker stop $ID`;
|
||||
startCommand = `
|
||||
echo "Starting compose container"
|
||||
docker start $ID
|
||||
echo "Compose container started"
|
||||
`;
|
||||
}
|
||||
return `
|
||||
${stopCommand}
|
||||
${baseCommand}
|
||||
${startCommand}
|
||||
`;
|
||||
}
|
||||
};
|
||||
30
packages/server/src/utils/volume-backups/index.ts
Normal file
30
packages/server/src/utils/volume-backups/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export * from "./backup";
|
||||
export * from "./restore";
|
||||
export * from "./utils";
|
||||
import { volumeBackups } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db/index";
|
||||
import { scheduleVolumeBackup } from "./utils";
|
||||
|
||||
export const initVolumeBackupsCronJobs = async () => {
|
||||
console.log("Setting up volume backups cron jobs....");
|
||||
try {
|
||||
const volumeBackupsResult = await db.query.volumeBackups.findMany({
|
||||
where: eq(volumeBackups.enabled, true),
|
||||
with: {
|
||||
application: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Initializing ${volumeBackupsResult.length} volume backups`);
|
||||
for (const volumeBackup of volumeBackupsResult) {
|
||||
scheduleVolumeBackup(volumeBackup.volumeBackupId);
|
||||
console.log(
|
||||
`Initialized volume backup: ${volumeBackup.name} ${volumeBackup.serviceType} ✅`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error initializing volume backups: ${error}`);
|
||||
}
|
||||
};
|
||||
126
packages/server/src/utils/volume-backups/restore.ts
Normal file
126
packages/server/src/utils/volume-backups/restore.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findDestinationById,
|
||||
getS3Credentials,
|
||||
paths,
|
||||
} from "../..";
|
||||
import path from "node:path";
|
||||
|
||||
export const restoreVolume = async (
|
||||
id: string,
|
||||
destinationId: string,
|
||||
volumeName: string,
|
||||
backupFileName: string,
|
||||
serverId: string,
|
||||
serviceType: "application" | "compose",
|
||||
) => {
|
||||
const destination = await findDestinationById(destinationId);
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, volumeName);
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
const backupPath = `${bucketPath}/${backupFileName}`;
|
||||
|
||||
// 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
|
||||
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 ""
|
||||
echo "⚠️ WARNING: Cannot restore volume as it is currently in use!"
|
||||
echo ""
|
||||
echo "📋 The following containers are using volume '${volumeName}':"
|
||||
echo ""
|
||||
|
||||
echo "$CONTAINERS_USING_VOLUME" | while IFS='|' read container_id container_name container_state labels; do
|
||||
echo " 🐳 Container: $container_name ($container_id)"
|
||||
echo " Status: $container_state"
|
||||
|
||||
# Determine container type
|
||||
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)
|
||||
echo " Type: Docker Swarm Service ($SERVICE_NAME)"
|
||||
elif echo "$labels" | grep -q "com.docker.compose.project="; then
|
||||
PROJECT_NAME=$(echo "$labels" | grep -o "com.docker.compose.project=[^,]*" | cut -d'=' -f2)
|
||||
echo " Type: Docker Compose ($PROJECT_NAME)"
|
||||
else
|
||||
echo " Type: Regular Container"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "🔧 To restore this volume, please:"
|
||||
echo " 1. Stop all containers/services using this volume"
|
||||
echo " 2. Remove the existing volume: docker volume rm ${volumeName}"
|
||||
echo " 3. Run the restore operation again"
|
||||
echo ""
|
||||
echo "❌ Volume restore aborted - volume is in use"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
`;
|
||||
|
||||
if (serviceType === "application") {
|
||||
const application = await findApplicationById(id);
|
||||
return `
|
||||
echo "=== VOLUME RESTORE FOR APPLICATION ==="
|
||||
echo "Application: ${application.appName}"
|
||||
${checkVolumeCommand}
|
||||
`;
|
||||
}
|
||||
|
||||
if (serviceType === "compose") {
|
||||
const compose = await findComposeById(id);
|
||||
|
||||
return `
|
||||
echo "=== VOLUME RESTORE FOR COMPOSE ==="
|
||||
echo "Compose: ${compose.appName}"
|
||||
echo "Compose Type: ${compose.composeType}"
|
||||
${checkVolumeCommand}
|
||||
`;
|
||||
}
|
||||
|
||||
// Fallback for unknown service types
|
||||
return checkVolumeCommand;
|
||||
};
|
||||
48
packages/server/src/utils/volume-backups/utils.ts
Normal file
48
packages/server/src/utils/volume-backups/utils.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import {
|
||||
createDeploymentVolumeBackup,
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
updateDeploymentStatus,
|
||||
} from "../..";
|
||||
import { backupVolume } from "./backup";
|
||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
||||
|
||||
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
|
||||
await runVolumeBackup(volumeBackupId);
|
||||
});
|
||||
};
|
||||
|
||||
export const removeVolumeBackupJob = async (volumeBackupId: string) => {
|
||||
const currentJob = scheduledJobs[volumeBackupId];
|
||||
currentJob?.cancel();
|
||||
};
|
||||
|
||||
export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
const serverId =
|
||||
volumeBackup.application?.serverId || volumeBackup.compose?.serverId;
|
||||
const deployment = await createDeploymentVolumeBackup({
|
||||
volumeBackupId: volumeBackup.volumeBackupId,
|
||||
title: "Volume Backup",
|
||||
description: "Volume Backup",
|
||||
});
|
||||
|
||||
try {
|
||||
const command = await backupVolume(volumeBackup);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
} else {
|
||||
await execAsync(commandWithLog);
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user