Merge branch 'Dokploy:canary' into 4053-fix-slack-notifications-content

This commit is contained in:
vincent-tarrit
2026-03-24 07:10:24 +01:00
committed by GitHub
85 changed files with 11713 additions and 411 deletions
+3 -1
View File
@@ -75,6 +75,7 @@ export const initCronJobs = async () => {
mariadb: true,
mysql: true,
mongo: true,
libsql: true,
user: true,
compose: true,
},
@@ -116,7 +117,8 @@ const getServiceAppName = (backup: BackupSchedule): string => {
backup.postgres?.appName ||
backup.mysql?.appName ||
backup.mariadb?.appName ||
backup.mongo?.appName;
backup.mongo?.appName ||
backup.libsql?.appName;
return serviceAppName || backup.appName;
};
@@ -0,0 +1,75 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findEnvironmentById } from "@dokploy/server/services/environment";
import type { Libsql } from "@dokploy/server/services/libsql";
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 runLibsqlBackup = async (
libsql: Libsql,
backup: BackupSchedule,
) => {
const { name, environmentId, appName } = libsql;
const environment = await findEnvironmentById(environmentId);
const project = await findProjectById(environment.projectId);
const deployment = await createDeploymentBackup({
backupId: backup.backupId,
title: "Initializing Backup",
description: "Initializing Backup",
});
const { prefix } = backup;
const destination = backup.destination;
const backupFileName = `${new Date().toISOString()}.sql.gz`;
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
try {
const rcloneFlags = getS3Credentials(destination);
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
const backupCommand = getBackupCommand(
backup,
rcloneCommand,
deployment.logPath,
);
if (libsql.serverId) {
await execAsyncRemote(libsql.serverId, backupCommand);
} else {
await execAsync(backupCommand, {
shell: "/bin/bash",
});
}
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "libsql",
type: "success",
organizationId: project.organizationId,
databaseName: backup.database,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
} catch (error) {
await sendDatabaseBackupNotifications({
applicationName: name,
projectName: project.name,
databaseType: "libsql",
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
organizationId: project.organizationId,
databaseName: backup.database,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
};
+30 -3
View File
@@ -4,6 +4,7 @@ import type { Destination } from "@dokploy/server/services/destination";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { keepLatestNBackups } from ".";
import { runComposeBackup } from "./compose";
import { runLibsqlBackup } from "./libsql";
import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql";
@@ -19,6 +20,7 @@ export const scheduleBackup = (backup: BackupSchedule) => {
mysql,
mongo,
mariadb,
libsql,
compose,
} = backup;
scheduleJob(backupId, schedule, async () => {
@@ -35,6 +37,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
} else if (databaseType === "mariadb" && mariadb) {
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb.serverId);
} else if (databaseType === "libsql" && libsql) {
await runLibsqlBackup(libsql, backup);
await keepLatestNBackups(backup, libsql.serverId);
} else if (databaseType === "web-server") {
await runWebServerBackup(backup);
await keepLatestNBackups(backup);
@@ -107,6 +112,10 @@ export const getMongoBackupCommand = (
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
};
export const getLibsqlBackupCommand = (database: string) => {
return `docker exec -i $CONTAINER_ID sh -c "tar cf - -C /var/lib/sqld ${database} | gzip"`;
};
export const getServiceContainerCommand = (appName: string) => {
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
};
@@ -123,12 +132,24 @@ export const getComposeContainerCommand = (
};
const getContainerSearchCommand = (backup: BackupSchedule) => {
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
backup;
const {
backupType,
postgres,
mysql,
mariadb,
mongo,
libsql,
compose,
serviceName,
} = backup;
if (backupType === "database") {
const appName =
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
postgres?.appName ||
mysql?.appName ||
mariadb?.appName ||
mongo?.appName ||
libsql?.appName;
return getServiceContainerCommand(appName || "");
}
if (backupType === "compose") {
@@ -209,6 +230,12 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
}
break;
}
case "libsql": {
if (backupType === "database") {
return getLibsqlBackupCommand(backup.database);
}
break;
}
default:
throw new Error(`Database type not supported: ${databaseType}`);
}
@@ -0,0 +1,155 @@
import type { InferResultType } from "@dokploy/server/types/with";
import type { CreateServiceOptions, PortConfig } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getRemoteDocker } from "../servers/remote-docker";
export type LibsqlNested = InferResultType<
"libsql",
{
mounts: true;
environment: { with: { project: true } };
}
>;
export const buildLibsql = async (libsql: LibsqlNested) => {
const {
appName,
env,
externalPort,
externalGRPCPort,
externalAdminPort,
memoryLimit,
memoryReservation,
databaseUser,
databasePassword,
sqldNode,
sqldPrimaryUrl,
cpuLimit,
cpuReservation,
command,
mounts,
enableNamespaces,
} = libsql;
const basicAuth = Buffer.from(
`${databaseUser}:${databasePassword}`,
"utf-8",
).toString("base64");
const defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
env ? `\n${env}` : ""
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(libsql);
const resources = calculateResources({
memoryLimit,
memoryReservation,
cpuLimit,
cpuReservation,
});
const envVariables = prepareEnvironmentVariables(
defaultLibsqlEnv,
libsql.environment.project.env,
libsql.environment.env,
);
const volumesMount = generateVolumeMounts(mounts);
const bindsMount = generateBindMounts(mounts);
const filesMount = generateFileMounts(appName, libsql);
const docker = await getRemoteDocker(libsql.serverId);
let finalCommand =
command ??
"sqld --db-path iku.db --http-listen-addr 0.0.0.0:8080 --grpc-listen-addr 0.0.0.0:5001 --admin-listen-addr 0.0.0.0:5000";
if (enableNamespaces) {
finalCommand += " --enable-namespaces";
}
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
...(finalCommand
? {
Command: ["/bin/sh"],
Args: ["-c", finalCommand],
}
: {}),
Labels,
},
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: [
...(externalPort
? [
{
Protocol: "tcp",
TargetPort: 8080,
PublishedPort: externalPort,
PublishMode: "host",
} as PortConfig,
]
: []),
...(externalGRPCPort
? [
{
Protocol: "tcp",
TargetPort: 5001,
PublishedPort: externalGRPCPort,
PublishMode: "host",
} as PortConfig,
]
: []),
...(externalAdminPort
? [
{
Protocol: "tcp",
TargetPort: 5000,
PublishedPort: externalAdminPort,
PublishMode: "host",
} as PortConfig,
]
: []),
],
},
UpdateConfig,
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
});
} catch {
await docker.createService(settings);
}
};
+34 -16
View File
@@ -1,11 +1,13 @@
import { db } from "@dokploy/server/db";
import {
libsql,
mariadb,
mongo,
mysql,
postgres,
redis,
} from "@dokploy/server/db/schema";
import { deployLibsql } from "@dokploy/server/services/libsql";
import { deployMariadb } from "@dokploy/server/services/mariadb";
import { deployMongo } from "@dokploy/server/services/mongo";
import { deployMySql } from "@dokploy/server/services/mysql";
@@ -15,7 +17,13 @@ import { eq } from "drizzle-orm";
import { removeService } from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
type DatabaseType =
| "libsql"
| "mariadb"
| "mongo"
| "mysql"
| "postgres"
| "redis";
export const rebuildDatabase = async (
databaseId: string,
@@ -41,31 +49,25 @@ export const rebuildDatabase = async (
}
}
if (type === "postgres") {
await deployPostgres(databaseId);
} else if (type === "mysql") {
await deployMySql(databaseId);
if (type === "libsql") {
await deployLibsql(databaseId);
} else if (type === "mariadb") {
await deployMariadb(databaseId);
} else if (type === "mongo") {
await deployMongo(databaseId);
} else if (type === "mysql") {
await deployMySql(databaseId);
} else if (type === "postgres") {
await deployPostgres(databaseId);
} else if (type === "redis") {
await deployRedis(databaseId);
}
};
const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
if (type === "postgres") {
return await db.query.postgres.findFirst({
where: eq(postgres.postgresId, databaseId),
with: {
mounts: true,
},
});
}
if (type === "mysql") {
return await db.query.mysql.findFirst({
where: eq(mysql.mysqlId, databaseId),
if (type === "libsql") {
return await db.query.libsql.findFirst({
where: eq(libsql.libsqlId, databaseId),
with: {
mounts: true,
},
@@ -87,6 +89,22 @@ const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
},
});
}
if (type === "mysql") {
return await db.query.mysql.findFirst({
where: eq(mysql.mysqlId, databaseId),
with: {
mounts: true,
},
});
}
if (type === "postgres") {
return await db.query.postgres.findFirst({
where: eq(postgres.postgresId, databaseId),
with: {
mounts: true,
},
});
}
if (type === "redis") {
return await db.query.redis.findFirst({
where: eq(redis.redisId, databaseId),
@@ -7,6 +7,7 @@ import type { ContainerInfo, ResourceRequirements } from "dockerode";
import { parse } from "dotenv";
import { quote } from "shell-quote";
import type { ApplicationNested } from "../builders";
import type { LibsqlNested } from "../databases/libsql";
import type { MariadbNested } from "../databases/mariadb";
import type { MongoNested } from "../databases/mongo";
import type { MysqlNested } from "../databases/mysql";
@@ -610,6 +611,7 @@ export const generateFileMounts = (
appName: string,
service:
| ApplicationNested
| LibsqlNested
| MongoNested
| MariadbNested
| MysqlNested
@@ -29,7 +29,7 @@ export const sendDatabaseBackupNotifications = async ({
}: {
projectName: string;
applicationName: string;
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
type: "error" | "success";
organizationId: string;
errorMessage?: string;
@@ -37,7 +37,8 @@ export const sendVolumeBackupNotifications = async ({
| "mongodb"
| "mariadb"
| "redis"
| "compose";
| "compose"
| "libsql";
type: "error" | "success";
organizationId: string;
errorMessage?: string;
+6 -2
View File
@@ -32,7 +32,7 @@ export const restoreComposeBackup = async (
rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
}
let credentials: DatabaseCredentials;
let credentials: DatabaseCredentials = {};
switch (backupInput.databaseType) {
case "postgres":
@@ -62,7 +62,11 @@ export const restoreComposeBackup = async (
const restoreCommand = getRestoreCommand({
appName: appName,
serviceName: backupInput.metadata?.serviceName,
type: backupInput.databaseType,
type: backupInput.databaseType as
| "postgres"
| "mariadb"
| "mysql"
| "mongo",
credentials: {
database: backupInput.databaseName,
...credentials,
@@ -1,4 +1,5 @@
export { restoreComposeBackup } from "./compose";
export { restoreLibsqlBackup } from "./libsql";
export { restoreMariadbBackup } from "./mariadb";
export { restoreMongoBackup } from "./mongo";
export { restoreMySqlBackup } from "./mysql";
@@ -0,0 +1,49 @@
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
import type { Destination } from "@dokploy/server/services/destination";
import type { Libsql } from "@dokploy/server/services/libsql";
import type { z } from "zod";
import { getS3Credentials, getServiceContainerCommand } from "../backups/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
export const restoreLibsqlBackup = async (
libsql: Libsql,
destination: Destination,
backupInput: z.infer<typeof apiRestoreBackup>,
emit: (log: string) => void,
) => {
try {
const { appName, serverId } = libsql;
const rcloneFlags = getS3Credentials(destination);
const bucketPath = `:s3:${destination.bucket}`;
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`;
emit("Starting restore...");
emit(`Backup path: ${backupPath}`);
const containerSearch = getServiceContainerCommand(appName);
const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`;
const command = `CONTAINER_ID=$(${containerSearch}) && ${rcloneCommand} | ${restoreCommand}`;
emit(`Executing command: ${command}`);
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
emit("Restore completed successfully!");
} catch (error) {
emit(
`Error: ${
error instanceof Error ? error.message : "Error restoring libsql backup"
}`,
);
throw error;
}
};
@@ -18,7 +18,8 @@ export const getVolumeServiceAppName = (
volumeBackup.mysql?.appName ||
volumeBackup.mariadb?.appName ||
volumeBackup.mongo?.appName ||
volumeBackup.redis?.appName;
volumeBackup.redis?.appName ||
volumeBackup.libsql?.appName;
return serviceAppName || volumeBackup.appName;
};
@@ -26,6 +26,7 @@ const getProjectName = (
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {
@@ -48,6 +49,7 @@ const getOrganizationId = (
volumeBackup.mariadb,
volumeBackup.mongo,
volumeBackup.redis,
volumeBackup.libsql,
];
for (const service of services) {