mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/stack-env-support
This commit is contained in:
@@ -49,6 +49,7 @@ export const runMariadbBackup = async (
|
||||
projectName: project.name,
|
||||
databaseType: "mariadb",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -59,6 +60,7 @@ export const runMariadbBackup = async (
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
projectName: project.name,
|
||||
databaseType: "mongodb",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -56,6 +57,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
projectName: project.name,
|
||||
databaseType: "mysql",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -56,6 +57,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const runPostgresBackup = async (
|
||||
projectName: project.name,
|
||||
databaseType: "postgres",
|
||||
type: "success",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
@@ -58,6 +59,7 @@ export const runPostgresBackup = async (
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
adminId: project.adminId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
|
||||
@@ -28,9 +28,9 @@ export const removeScheduleBackup = (backupId: string) => {
|
||||
};
|
||||
|
||||
export const getS3Credentials = (destination: Destination) => {
|
||||
const { accessKey, secretAccessKey, bucket, region, endpoint } = destination;
|
||||
const { accessKey, secretAccessKey, bucket, region, endpoint, provider } =
|
||||
destination;
|
||||
const rcloneFlags = [
|
||||
// `--s3-provider=Cloudflare`,
|
||||
`--s3-access-key-id=${accessKey}`,
|
||||
`--s3-secret-access-key=${secretAccessKey}`,
|
||||
`--s3-region=${region}`,
|
||||
@@ -39,5 +39,9 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
"--s3-force-path-style",
|
||||
];
|
||||
|
||||
if (provider) {
|
||||
rcloneFlags.unshift(`--s3-provider=${provider}`);
|
||||
}
|
||||
|
||||
return rcloneFlags;
|
||||
};
|
||||
|
||||
@@ -183,7 +183,10 @@ const createEnvFile = (compose: ComposeNested) => {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
envContent,
|
||||
compose.project.env,
|
||||
).join("\n");
|
||||
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
@@ -241,7 +244,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`;
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
const envFileContent = prepareEnvironmentVariables(
|
||||
envContent,
|
||||
compose.project.env,
|
||||
).join("\n");
|
||||
|
||||
const encodedContent = encodeBase64(envFileContent);
|
||||
return `
|
||||
|
||||
@@ -20,7 +20,10 @@ export const buildCustomDocker = async (
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const dockerContextPath = getDockerContextPath(application);
|
||||
|
||||
@@ -38,7 +41,7 @@ export const buildCustomDocker = async (
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
if (!publishDirectory) {
|
||||
createEnvFile(dockerFilePath, env);
|
||||
createEnvFile(dockerFilePath, env, application.project.env);
|
||||
}
|
||||
|
||||
await spawnAsync(
|
||||
@@ -71,7 +74,10 @@ export const getDockerCommand = (
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const dockerContextPath =
|
||||
getDockerContextPath(application) || defaultContextPath;
|
||||
@@ -92,7 +98,11 @@ export const getDockerCommand = (
|
||||
*/
|
||||
let command = "";
|
||||
if (!publishDirectory) {
|
||||
command += createEnvFileCommand(dockerFilePath, env);
|
||||
command += createEnvFileCommand(
|
||||
dockerFilePath,
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
}
|
||||
|
||||
command += `
|
||||
|
||||
@@ -11,7 +11,10 @@ export const buildHeroku = async (
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
@@ -19,7 +22,7 @@ export const buildHeroku = async (
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"heroku/builder:24",
|
||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
@@ -44,7 +47,10 @@ export const getHerokuCommand = (
|
||||
const { env, appName } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const args = [
|
||||
"build",
|
||||
@@ -52,7 +58,7 @@ export const getHerokuCommand = (
|
||||
"--path",
|
||||
buildAppDirectory,
|
||||
"--builder",
|
||||
"heroku/builder:24",
|
||||
`heroku/builder:${application.herokuVersion || "24"}`,
|
||||
];
|
||||
|
||||
for (const env of envVariables) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 } from "../cluster/upload";
|
||||
import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -16,6 +17,7 @@ import { buildHeroku, getHerokuCommand } from "./heroku";
|
||||
import { buildNixpacks, getNixpacksCommand } from "./nixpacks";
|
||||
import { buildPaketo, getPaketoCommand } from "./paketo";
|
||||
import { buildStatic, getStaticCommand } from "./static";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@@ -23,8 +25,16 @@ import { buildStatic, getStaticCommand } from "./static";
|
||||
// DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile)
|
||||
export type ApplicationNested = InferResultType<
|
||||
"applications",
|
||||
{ mounts: true; security: true; redirects: true; ports: true; registry: true }
|
||||
{
|
||||
mounts: true;
|
||||
security: true;
|
||||
redirects: true;
|
||||
ports: true;
|
||||
registry: true;
|
||||
project: true;
|
||||
}
|
||||
>;
|
||||
|
||||
export const buildApplication = async (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
@@ -69,19 +79,30 @@ export const getBuildCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const { buildType } = application;
|
||||
let command = "";
|
||||
const { buildType, registry } = application;
|
||||
switch (buildType) {
|
||||
case "nixpacks":
|
||||
return getNixpacksCommand(application, logPath);
|
||||
command = getNixpacksCommand(application, logPath);
|
||||
break;
|
||||
case "heroku_buildpacks":
|
||||
return getHerokuCommand(application, logPath);
|
||||
command = getHerokuCommand(application, logPath);
|
||||
break;
|
||||
case "paketo_buildpacks":
|
||||
return getPaketoCommand(application, logPath);
|
||||
command = getPaketoCommand(application, logPath);
|
||||
break;
|
||||
case "static":
|
||||
return getStaticCommand(application, logPath);
|
||||
command = getStaticCommand(application, logPath);
|
||||
break;
|
||||
case "dockerfile":
|
||||
return getDockerCommand(application, logPath);
|
||||
command = getDockerCommand(application, logPath);
|
||||
break;
|
||||
}
|
||||
if (registry) {
|
||||
command += uploadImageRemoteCommand(application, logPath);
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
export const mechanizeDockerContainer = async (
|
||||
@@ -121,7 +142,10 @@ export const mechanizeDockerContainer = async (
|
||||
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const image = getImageName(application);
|
||||
const authConfig = getAuthConfig(application);
|
||||
@@ -186,11 +210,11 @@ const getImageName = (application: ApplicationNested) => {
|
||||
return dockerImage || "ERROR-NO-IMAGE-PROVIDED";
|
||||
}
|
||||
|
||||
const registryUrl = registry?.registryUrl || "";
|
||||
const imagePrefix = registry?.imagePrefix ? `${registry.imagePrefix}/` : "";
|
||||
return registry
|
||||
? `${registryUrl}/${imagePrefix}${appName}`
|
||||
: `${appName}:latest`;
|
||||
if (registry) {
|
||||
return join(registry.imagePrefix || "", appName);
|
||||
}
|
||||
|
||||
return `${appName}:latest`;
|
||||
};
|
||||
|
||||
const getAuthConfig = (application: ApplicationNested) => {
|
||||
|
||||
@@ -14,11 +14,14 @@ export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName, publishDirectory, serverId } = application;
|
||||
const { env, appName, publishDirectory } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const writeToStream = (data: string) => {
|
||||
if (writeStream.writable) {
|
||||
@@ -92,7 +95,10 @@ export const getNixpacksCommand = (
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ export const buildPaketo = async (
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
try {
|
||||
const args = [
|
||||
"build",
|
||||
@@ -43,7 +46,10 @@ export const getPaketoCommand = (
|
||||
const { env, appName } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
env,
|
||||
application.project.env,
|
||||
);
|
||||
|
||||
const args = [
|
||||
"build",
|
||||
|
||||
@@ -2,17 +2,29 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||
|
||||
export const createEnvFile = (directory: string, env: string | null) => {
|
||||
export const createEnvFile = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
projectEnv?: string | null,
|
||||
) => {
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
if (!existsSync(dirname(envFilePath))) {
|
||||
mkdirSync(dirname(envFilePath), { recursive: true });
|
||||
}
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
|
||||
"\n",
|
||||
);
|
||||
writeFileSync(envFilePath, envFileContent);
|
||||
};
|
||||
|
||||
export const createEnvFileCommand = (directory: string, env: string | null) => {
|
||||
const envFileContent = prepareEnvironmentVariables(env).join("\n");
|
||||
export const createEnvFileCommand = (
|
||||
directory: string,
|
||||
env: string | null,
|
||||
projectEnv?: string | null,
|
||||
) => {
|
||||
const envFileContent = prepareEnvironmentVariables(env, projectEnv).join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
const encodedContent = encodeBase64(envFileContent || "");
|
||||
const envFilePath = join(dirname(directory), ".env");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
@@ -16,23 +17,14 @@ export const uploadImage = async (
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL =
|
||||
registryType === "selfHosted"
|
||||
? process.env.NODE_ENV === "development"
|
||||
? "localhost:5000"
|
||||
: registryUrl
|
||||
: registryUrl;
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = imagePrefix
|
||||
? `${finalURL}/${imagePrefix}/${imageName}`
|
||||
: `${finalURL}/${imageName}`;
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
|
||||
try {
|
||||
console.log(finalURL, registryTag);
|
||||
writeStream.write(
|
||||
`📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`,
|
||||
);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["login", finalURL, "-u", registry.username, "-p", registry.password],
|
||||
@@ -59,7 +51,48 @@ export const uploadImage = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
// docker:
|
||||
// endpoint: "unix:///var/run/docker.sock"
|
||||
// exposedByDefault: false
|
||||
// swarmMode: true
|
||||
|
||||
export const uploadImageRemoteCommand = (
|
||||
application: ApplicationNested,
|
||||
logPath: string,
|
||||
) => {
|
||||
const registry = application.registry;
|
||||
|
||||
if (!registry) {
|
||||
throw new Error("Registry not found");
|
||||
}
|
||||
|
||||
const { registryUrl, imagePrefix } = registry;
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
|
||||
const finalURL = registryUrl;
|
||||
|
||||
const registryTag = join(imagePrefix || "", imageName);
|
||||
|
||||
try {
|
||||
const command = `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath};
|
||||
docker login ${finalURL} -u ${registry.username} -p ${registry.password} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ DockerHub Failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ DockerHub Login Success" >> ${logPath};
|
||||
docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Error tagging image" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Tagged" >> ${logPath};
|
||||
|
||||
docker push ${registryTag} 2>> ${logPath} || {
|
||||
echo "❌ Error pushing image" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
echo "✅ Image Pushed" >> ${logPath};
|
||||
`;
|
||||
return command;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MariadbNested = InferResultType<"mariadb", { mounts: true }>;
|
||||
export type MariadbNested = InferResultType<
|
||||
"mariadb",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
const {
|
||||
appName,
|
||||
@@ -37,7 +40,10 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMariadbEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMariadbEnv,
|
||||
mariadb.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mariadb);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MongoNested = InferResultType<"mongo", { mounts: true }>;
|
||||
export type MongoNested = InferResultType<
|
||||
"mongo",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
|
||||
export const buildMongo = async (mongo: MongoNested) => {
|
||||
const {
|
||||
@@ -36,7 +39,10 @@ export const buildMongo = async (mongo: MongoNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMongoEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMongoEnv,
|
||||
mongo.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mongo);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type MysqlNested = InferResultType<"mysql", { mounts: true }>;
|
||||
export type MysqlNested = InferResultType<
|
||||
"mysql",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
|
||||
export const buildMysql = async (mysql: MysqlNested) => {
|
||||
const {
|
||||
@@ -43,7 +46,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultMysqlEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultMysqlEnv,
|
||||
mysql.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, mysql);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type PostgresNested = InferResultType<"postgres", { mounts: true }>;
|
||||
export type PostgresNested = InferResultType<
|
||||
"postgres",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
const {
|
||||
appName,
|
||||
@@ -36,7 +39,10 @@ export const buildPostgres = async (postgres: PostgresNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultPostgresEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultPostgresEnv,
|
||||
postgres.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, postgres);
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type RedisNested = InferResultType<"redis", { mounts: true }>;
|
||||
export type RedisNested = InferResultType<
|
||||
"redis",
|
||||
{ mounts: true; project: true }
|
||||
>;
|
||||
export const buildRedis = async (redis: RedisNested) => {
|
||||
const {
|
||||
appName,
|
||||
@@ -34,7 +37,10 @@ export const buildRedis = async (redis: RedisNested) => {
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(defaultRedisEnv);
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultRedisEnv,
|
||||
redis.project.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, redis);
|
||||
|
||||
@@ -259,10 +259,10 @@ export const createDomainLabels = async (
|
||||
domain: Domain,
|
||||
entrypoint: "web" | "websecure",
|
||||
) => {
|
||||
const { host, port, https, uniqueConfigKey, certificateType } = domain;
|
||||
const { host, port, https, uniqueConfigKey, certificateType, path } = domain;
|
||||
const routerName = `${appName}-${uniqueConfigKey}-${entrypoint}`;
|
||||
const labels = [
|
||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)`,
|
||||
`traefik.http.routers.${routerName}.rule=Host(\`${host}\`)${path && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
`traefik.http.routers.${routerName}.entrypoints=${entrypoint}`,
|
||||
`traefik.http.services.${routerName}.loadbalancer.server.port=${port}`,
|
||||
`traefik.http.routers.${routerName}.service=${routerName}`,
|
||||
|
||||
@@ -11,12 +11,13 @@ import type { MysqlNested } from "../databases/mysql";
|
||||
import type { PostgresNested } from "../databases/postgres";
|
||||
import type { RedisNested } from "../databases/redis";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
registryUrl: string;
|
||||
}
|
||||
|
||||
export const pullImage = async (
|
||||
@@ -29,29 +30,21 @@ export const pullImage = async (
|
||||
throw new Error("Docker image not found");
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
docker.pull(dockerImage, { authconfig: authConfig }, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
docker.modem.followProgress(
|
||||
stream as Readable,
|
||||
(err: Error | null, res) => {
|
||||
if (!err) {
|
||||
resolve(res);
|
||||
}
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
(event) => {
|
||||
onData?.(event);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
if (authConfig?.username && authConfig?.password) {
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[
|
||||
"login",
|
||||
authConfig.registryUrl || "",
|
||||
"-u",
|
||||
authConfig.username,
|
||||
"-p",
|
||||
authConfig.password,
|
||||
],
|
||||
onData,
|
||||
);
|
||||
}
|
||||
await spawnAsync("docker", ["pull", dockerImage], onData);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -258,8 +251,28 @@ export const removeService = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareEnvironmentVariables = (env: string | null) =>
|
||||
Object.entries(parse(env ?? "")).map(([key, value]) => `${key}=${value}`);
|
||||
export const prepareEnvironmentVariables = (
|
||||
serviceEnv: string | null,
|
||||
projectEnv?: string | null,
|
||||
) => {
|
||||
const projectVars = parse(projectEnv ?? "");
|
||||
const serviceVars = parse(serviceEnv ?? "");
|
||||
|
||||
const resolvedVars = Object.entries(serviceVars).map(([key, value]) => {
|
||||
let resolvedValue = value;
|
||||
if (projectVars) {
|
||||
resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => {
|
||||
if (projectVars[ref] !== undefined) {
|
||||
return projectVars[ref];
|
||||
}
|
||||
throw new Error(`Invalid project environment variable: project.${ref}`);
|
||||
});
|
||||
}
|
||||
return `${key}=${resolvedValue}`;
|
||||
});
|
||||
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const prepareBuildArgs = (input: string | null) => {
|
||||
const pairs = (input ?? "").split("\n");
|
||||
|
||||
349
packages/server/src/utils/gpu-setup.ts
Normal file
349
packages/server/src/utils/gpu-setup.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { execAsync, sleep } from "../utils/process/execAsync";
|
||||
import { execAsyncRemote } from "../utils/process/execAsync";
|
||||
|
||||
interface GPUInfo {
|
||||
driverInstalled: boolean;
|
||||
driverVersion?: string;
|
||||
gpuModel?: string;
|
||||
runtimeInstalled: boolean;
|
||||
runtimeConfigured: boolean;
|
||||
cudaSupport: boolean;
|
||||
cudaVersion?: string;
|
||||
memoryInfo?: string;
|
||||
availableGPUs: number;
|
||||
swarmEnabled: boolean;
|
||||
gpuResources: number;
|
||||
}
|
||||
|
||||
export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
|
||||
try {
|
||||
const [driverInfo, runtimeInfo, swarmInfo, gpuInfo, cudaInfo] =
|
||||
await Promise.all([
|
||||
checkGpuDriver(serverId),
|
||||
checkRuntime(serverId),
|
||||
checkSwarmResources(serverId),
|
||||
checkGpuInfo(serverId),
|
||||
checkCudaSupport(serverId),
|
||||
]);
|
||||
|
||||
return {
|
||||
...driverInfo,
|
||||
...runtimeInfo,
|
||||
...swarmInfo,
|
||||
...gpuInfo,
|
||||
...cudaInfo,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in checkGPUStatus:", error);
|
||||
return {
|
||||
driverInstalled: false,
|
||||
driverVersion: undefined,
|
||||
runtimeInstalled: false,
|
||||
runtimeConfigured: false,
|
||||
cudaSupport: false,
|
||||
cudaVersion: undefined,
|
||||
gpuModel: undefined,
|
||||
memoryInfo: undefined,
|
||||
availableGPUs: 0,
|
||||
swarmEnabled: false,
|
||||
gpuResources: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const checkGpuDriver = async (serverId?: string) => {
|
||||
let driverVersion: string | undefined;
|
||||
let driverInstalled = false;
|
||||
let availableGPUs = 0;
|
||||
|
||||
try {
|
||||
const driverCommand =
|
||||
"nvidia-smi --query-gpu=driver_version --format=csv,noheader";
|
||||
const { stdout: nvidiaSmi } = serverId
|
||||
? await execAsyncRemote(serverId, driverCommand)
|
||||
: await execAsync(driverCommand);
|
||||
|
||||
driverVersion = nvidiaSmi.trim();
|
||||
if (driverVersion) {
|
||||
driverInstalled = true;
|
||||
const countCommand =
|
||||
"nvidia-smi --query-gpu=gpu_name --format=csv,noheader | wc -l";
|
||||
const { stdout: gpuCount } = serverId
|
||||
? await execAsyncRemote(serverId, countCommand)
|
||||
: await execAsync(countCommand);
|
||||
|
||||
availableGPUs = Number.parseInt(gpuCount.trim(), 10);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("GPU driver check:", error);
|
||||
}
|
||||
|
||||
return { driverVersion, driverInstalled, availableGPUs };
|
||||
};
|
||||
|
||||
const checkRuntime = async (serverId?: string) => {
|
||||
let runtimeInstalled = false;
|
||||
let runtimeConfigured = false;
|
||||
|
||||
try {
|
||||
// First check: Is nvidia-container-runtime installed?
|
||||
const checkBinaryCommand = "command -v nvidia-container-runtime";
|
||||
try {
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, checkBinaryCommand)
|
||||
: await execAsync(checkBinaryCommand);
|
||||
runtimeInstalled = !!stdout.trim();
|
||||
} catch (error) {
|
||||
console.debug("Runtime binary check:", error);
|
||||
}
|
||||
|
||||
// Second check: Is it configured in Docker?
|
||||
try {
|
||||
const runtimeCommand = 'docker info --format "{{json .Runtimes}}"';
|
||||
const { stdout: runtimeInfo } = serverId
|
||||
? await execAsyncRemote(serverId, runtimeCommand)
|
||||
: await execAsync(runtimeCommand);
|
||||
|
||||
const defaultCommand = 'docker info --format "{{.DefaultRuntime}}"';
|
||||
const { stdout: defaultRuntime } = serverId
|
||||
? await execAsyncRemote(serverId, defaultCommand)
|
||||
: await execAsync(defaultCommand);
|
||||
|
||||
const runtimes = JSON.parse(runtimeInfo);
|
||||
const hasNvidiaRuntime = "nvidia" in runtimes;
|
||||
const isDefaultRuntime = defaultRuntime.trim() === "nvidia";
|
||||
|
||||
// Only set runtimeConfigured if both conditions are met
|
||||
runtimeConfigured = hasNvidiaRuntime && isDefaultRuntime;
|
||||
} catch (error) {
|
||||
console.debug("Runtime configuration check:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("Runtime check:", error);
|
||||
}
|
||||
|
||||
return { runtimeInstalled, runtimeConfigured };
|
||||
};
|
||||
|
||||
const checkSwarmResources = async (serverId?: string) => {
|
||||
let swarmEnabled = false;
|
||||
let gpuResources = 0;
|
||||
|
||||
try {
|
||||
const nodeCommand =
|
||||
"docker node inspect self --format '{{json .Description.Resources.GenericResources}}'";
|
||||
const { stdout: resources } = serverId
|
||||
? await execAsyncRemote(serverId, nodeCommand)
|
||||
: await execAsync(nodeCommand);
|
||||
|
||||
if (resources && resources !== "null") {
|
||||
const genericResources = JSON.parse(resources);
|
||||
for (const resource of genericResources) {
|
||||
if (
|
||||
resource.DiscreteResourceSpec &&
|
||||
(resource.DiscreteResourceSpec.Kind === "GPU" ||
|
||||
resource.DiscreteResourceSpec.Kind === "gpu")
|
||||
) {
|
||||
gpuResources = resource.DiscreteResourceSpec.Value;
|
||||
swarmEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("Swarm resource check:", error);
|
||||
}
|
||||
|
||||
return { swarmEnabled, gpuResources };
|
||||
};
|
||||
|
||||
const checkGpuInfo = async (serverId?: string) => {
|
||||
let gpuModel: string | undefined;
|
||||
let memoryInfo: string | undefined;
|
||||
|
||||
try {
|
||||
const gpuInfoCommand =
|
||||
"nvidia-smi --query-gpu=gpu_name,memory.total --format=csv,noheader";
|
||||
const { stdout: gpuInfo } = serverId
|
||||
? await execAsyncRemote(serverId, gpuInfoCommand)
|
||||
: await execAsync(gpuInfoCommand);
|
||||
|
||||
[gpuModel, memoryInfo] = gpuInfo.split(",").map((s) => s.trim());
|
||||
} catch (error) {
|
||||
console.debug("GPU info check:", error);
|
||||
}
|
||||
|
||||
return { gpuModel, memoryInfo };
|
||||
};
|
||||
|
||||
const checkCudaSupport = async (serverId?: string) => {
|
||||
let cudaVersion: string | undefined;
|
||||
let cudaSupport = false;
|
||||
|
||||
try {
|
||||
const cudaCommand = 'nvidia-smi -q | grep "CUDA Version"';
|
||||
const { stdout: cudaInfo } = serverId
|
||||
? await execAsyncRemote(serverId, cudaCommand)
|
||||
: await execAsync(cudaCommand);
|
||||
|
||||
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d\.]+)/);
|
||||
cudaVersion = cudaMatch ? cudaMatch[1] : undefined;
|
||||
cudaSupport = !!cudaVersion;
|
||||
} catch (error) {
|
||||
console.debug("CUDA support check:", error);
|
||||
}
|
||||
|
||||
return { cudaVersion, cudaSupport };
|
||||
};
|
||||
|
||||
export async function setupGPUSupport(serverId?: string): Promise<void> {
|
||||
try {
|
||||
// 1. Initial status check and validation
|
||||
const initialStatus = await checkGPUStatus(serverId);
|
||||
const shouldContinue = await validatePrerequisites(initialStatus);
|
||||
if (!shouldContinue) return;
|
||||
|
||||
// 2. Get node ID
|
||||
const nodeId = await getNodeId(serverId);
|
||||
|
||||
// 3. Create daemon configuration
|
||||
const daemonConfig = createDaemonConfig(initialStatus.availableGPUs);
|
||||
|
||||
// 4. Setup server based on environment
|
||||
if (serverId) {
|
||||
await setupRemoteServer(serverId, daemonConfig);
|
||||
} else {
|
||||
await setupLocalServer(daemonConfig);
|
||||
}
|
||||
|
||||
// 5. Wait for Docker restart
|
||||
await sleep(10000);
|
||||
|
||||
// 6. Add GPU label
|
||||
await addGpuLabel(nodeId, serverId);
|
||||
|
||||
// 7. Final verification
|
||||
await sleep(5000);
|
||||
await verifySetup(nodeId, serverId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("password is required")
|
||||
) {
|
||||
throw new Error(
|
||||
"Sudo access required. Please run with appropriate permissions.",
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const validatePrerequisites = async (initialStatus: GPUInfo) => {
|
||||
if (!initialStatus.driverInstalled) {
|
||||
throw new Error(
|
||||
"NVIDIA drivers not installed. Please install appropriate NVIDIA drivers first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!initialStatus.runtimeInstalled) {
|
||||
throw new Error(
|
||||
"NVIDIA Container Runtime not installed. Please install nvidia-container-runtime first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (initialStatus.swarmEnabled && initialStatus.runtimeConfigured) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getNodeId = async (serverId?: string) => {
|
||||
const nodeIdCommand = 'docker info --format "{{.Swarm.NodeID}}"';
|
||||
const { stdout: nodeId } = serverId
|
||||
? await execAsyncRemote(serverId, nodeIdCommand)
|
||||
: await execAsync(nodeIdCommand);
|
||||
|
||||
const trimmedNodeId = nodeId.trim();
|
||||
if (!trimmedNodeId) {
|
||||
throw new Error("Setup Server before enabling GPU support");
|
||||
}
|
||||
|
||||
return trimmedNodeId;
|
||||
};
|
||||
|
||||
const createDaemonConfig = (availableGPUs: number) => ({
|
||||
runtimes: {
|
||||
nvidia: {
|
||||
path: "nvidia-container-runtime",
|
||||
runtimeArgs: [],
|
||||
},
|
||||
},
|
||||
"default-runtime": "nvidia",
|
||||
"node-generic-resources": [`GPU=${availableGPUs}`],
|
||||
});
|
||||
|
||||
const setupRemoteServer = async (serverId: string, daemonConfig: any) => {
|
||||
const setupCommands = [
|
||||
"sudo -n true",
|
||||
`echo '${JSON.stringify(daemonConfig, null, 2)}' | sudo tee /etc/docker/daemon.json`,
|
||||
"sudo mkdir -p /etc/nvidia-container-runtime",
|
||||
'sudo sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml',
|
||||
'echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" | sudo tee -a /etc/nvidia-container-runtime/config.toml',
|
||||
"sudo systemctl daemon-reload",
|
||||
"sudo systemctl restart docker",
|
||||
].join(" && ");
|
||||
|
||||
await execAsyncRemote(serverId, setupCommands);
|
||||
};
|
||||
|
||||
const setupLocalServer = async (daemonConfig: any) => {
|
||||
const configFile = `/tmp/docker-daemon-${Date.now()}.json`;
|
||||
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||
|
||||
const setupCommands = [
|
||||
`pkexec sh -c '
|
||||
cp ${configFile} /etc/docker/daemon.json &&
|
||||
mkdir -p /etc/nvidia-container-runtime &&
|
||||
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
|
||||
echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" >> /etc/nvidia-container-runtime/config.toml &&
|
||||
systemctl daemon-reload &&
|
||||
systemctl restart docker
|
||||
'`,
|
||||
`rm ${configFile}`,
|
||||
].join(" && ");
|
||||
|
||||
await execAsync(setupCommands);
|
||||
};
|
||||
|
||||
const addGpuLabel = async (nodeId: string, serverId?: string) => {
|
||||
const labelCommand = `docker node update --label-add gpu=true ${nodeId}`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, labelCommand);
|
||||
} else {
|
||||
await execAsync(labelCommand);
|
||||
}
|
||||
};
|
||||
|
||||
const verifySetup = async (nodeId: string, serverId?: string) => {
|
||||
const finalStatus = await checkGPUStatus(serverId);
|
||||
|
||||
if (!finalStatus.swarmEnabled) {
|
||||
const diagnosticCommands = [
|
||||
`docker node inspect ${nodeId}`,
|
||||
'nvidia-smi -a | grep "GPU UUID"',
|
||||
"cat /etc/docker/daemon.json",
|
||||
"cat /etc/nvidia-container-runtime/config.toml",
|
||||
].join(" && ");
|
||||
|
||||
const { stdout: diagnostics } = serverId
|
||||
? await execAsyncRemote(serverId, diagnosticCommands)
|
||||
: await execAsync(diagnosticCommands);
|
||||
|
||||
console.error("Diagnostic Information:", diagnostics);
|
||||
throw new Error("GPU support not detected in swarm after setup");
|
||||
}
|
||||
|
||||
return finalStatus;
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -16,6 +16,7 @@ interface Props {
|
||||
applicationType: string;
|
||||
errorMessage: string;
|
||||
buildLink: string;
|
||||
adminId: string;
|
||||
}
|
||||
|
||||
export const sendBuildErrorNotifications = async ({
|
||||
@@ -24,10 +25,14 @@ export const sendBuildErrorNotifications = async ({
|
||||
applicationType,
|
||||
errorMessage,
|
||||
buildLink,
|
||||
adminId,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.appBuildError, true),
|
||||
where: and(
|
||||
eq(notifications.appBuildError, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -54,31 +59,46 @@ export const sendBuildErrorNotifications = async ({
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "⚠️ Build Failed",
|
||||
color: 0xff0000,
|
||||
title: "> `⚠️` - Build Failed",
|
||||
color: 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
name: "`🛠️`・Project",
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Application",
|
||||
name: "`⚙️`・Application",
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
name: "`❔`・Type",
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Error",
|
||||
value: errorMessage,
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Build Link",
|
||||
value: buildLink,
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Failed",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⚠️`・Error Message",
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -15,6 +15,7 @@ interface Props {
|
||||
applicationName: string;
|
||||
applicationType: string;
|
||||
buildLink: string;
|
||||
adminId: string;
|
||||
}
|
||||
|
||||
export const sendBuildSuccessNotifications = async ({
|
||||
@@ -22,10 +23,14 @@ export const sendBuildSuccessNotifications = async ({
|
||||
applicationName,
|
||||
applicationType,
|
||||
buildLink,
|
||||
adminId,
|
||||
}: Props) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.appDeploy, true),
|
||||
where: and(
|
||||
eq(notifications.appDeploy, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -52,27 +57,42 @@ export const sendBuildSuccessNotifications = async ({
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "✅ Build Success",
|
||||
color: 0x00ff00,
|
||||
title: "> `✅` - Build Success",
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
name: "`🛠️`・Project",
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Application",
|
||||
name: "`⚙️`・Application",
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
name: "`❔`・Application Type",
|
||||
value: applicationType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Build Link",
|
||||
value: buildLink,
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`🧷`・Build Link",
|
||||
value: `[Click here to access build link](${buildLink})`,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -16,16 +16,21 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
databaseType,
|
||||
type,
|
||||
errorMessage,
|
||||
adminId,
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
type: "error" | "success";
|
||||
adminId: string;
|
||||
errorMessage?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.databaseBackup, true),
|
||||
where: and(
|
||||
eq(notifications.databaseBackup, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -59,39 +64,47 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? "✅ Database Backup Successful"
|
||||
: "❌ Database Backup Failed",
|
||||
color: type === "success" ? 0x00ff00 : 0xff0000,
|
||||
? "> `✅` - Database Backup Successful"
|
||||
: "> `❌` - Database Backup Failed",
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: "Project",
|
||||
name: "`🛠️`・Project",
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Application",
|
||||
name: "`⚙️`・Application",
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
name: "`❔`・Database",
|
||||
value: databaseType,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Time",
|
||||
value: date.toLocaleString(),
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
value: type,
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: "Error Message",
|
||||
value: errorMessage,
|
||||
name: "`⚠️`・Error Message",
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
@@ -11,11 +11,15 @@ import {
|
||||
} from "./utils";
|
||||
|
||||
export const sendDockerCleanupNotifications = async (
|
||||
adminId: string,
|
||||
message = "Docker cleanup for dokploy",
|
||||
) => {
|
||||
const date = new Date();
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: eq(notifications.dockerCleanup, true),
|
||||
where: and(
|
||||
eq(notifications.dockerCleanup, true),
|
||||
eq(notifications.adminId, adminId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
@@ -41,12 +45,27 @@ export const sendDockerCleanupNotifications = async (
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "✅ Docker Cleanup",
|
||||
color: 0x00ff00,
|
||||
title: "> `✅` - Docker Cleanup",
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "Message",
|
||||
value: message,
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`📜`・Message",
|
||||
value: `\`\`\`${message}\`\`\``,
|
||||
},
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
|
||||
@@ -34,12 +34,22 @@ export const sendDokployRestartNotifications = async () => {
|
||||
|
||||
if (discord) {
|
||||
await sendDiscordNotification(discord, {
|
||||
title: "✅ Dokploy Server Restarted",
|
||||
color: 0x00ff00,
|
||||
title: "> `✅` - Dokploy Server Restarted",
|
||||
color: 0x57f287,
|
||||
fields: [
|
||||
{
|
||||
name: "Time",
|
||||
value: date.toLocaleString(),
|
||||
name: "`📅`・Date",
|
||||
value: date.toLocaleDateString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`⌚`・Time",
|
||||
value: date.toLocaleTimeString(),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "`❓`・Type",
|
||||
value: "Successful",
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { pullImage } from "../docker/utils";
|
||||
interface RegistryAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
serveraddress: string;
|
||||
registryUrl: string;
|
||||
}
|
||||
|
||||
export const buildDocker = async (
|
||||
@@ -16,6 +16,7 @@ export const buildDocker = async (
|
||||
const authConfig: Partial<RegistryAuth> = {
|
||||
username: username || "",
|
||||
password: password || "",
|
||||
registryUrl: application.registryUrl || "",
|
||||
};
|
||||
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
@@ -33,7 +34,7 @@ export const buildDocker = async (
|
||||
dockerImage,
|
||||
(data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(`${data.status}\n`);
|
||||
writeStream.write(`${data}\n`);
|
||||
}
|
||||
},
|
||||
authConfig,
|
||||
@@ -41,7 +42,7 @@ export const buildDocker = async (
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("\nDocker Deployed: ✅\n");
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR: ${error}: ❌`);
|
||||
writeStream.write("❌ Error");
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -74,11 +74,22 @@ export type ApplicationWithGithub = InferResultType<
|
||||
>;
|
||||
|
||||
export type ComposeWithGithub = InferResultType<"compose", { github: true }>;
|
||||
export const cloneGithubRepository = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
|
||||
interface CloneGithubRepository {
|
||||
appName: string;
|
||||
owner: string | null;
|
||||
branch: string | null;
|
||||
githubId: string | null;
|
||||
repository: string | null;
|
||||
logPath: string;
|
||||
type?: "application" | "compose";
|
||||
}
|
||||
export const cloneGithubRepository = async ({
|
||||
logPath,
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository) => {
|
||||
const isCompose = type === "compose";
|
||||
const { APPLICATIONS_PATH, COMPOSE_PATH } = paths();
|
||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||
const { appName, repository, owner, branch, githubId } = entity;
|
||||
@@ -145,13 +156,13 @@ export const cloneGithubRepository = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const getGithubCloneCommand = async (
|
||||
entity: ApplicationWithGithub | ComposeWithGithub,
|
||||
logPath: string,
|
||||
isCompose = false,
|
||||
) => {
|
||||
export const getGithubCloneCommand = async ({
|
||||
logPath,
|
||||
type = "application",
|
||||
...entity
|
||||
}: CloneGithubRepository & { serverId: string }) => {
|
||||
const { appName, repository, owner, branch, githubId, serverId } = entity;
|
||||
|
||||
const isCompose = type === "compose";
|
||||
if (!serverId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
@@ -206,7 +217,7 @@ export const getGithubCloneCommand = async (
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
if ! git clone --branch ${branch} --depth 1 --recurse-submodules --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Fallo al clonar el repositorio ${repoclone}" >> ${logPath};
|
||||
echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath};
|
||||
exit 1;
|
||||
fi
|
||||
echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath};
|
||||
|
||||
Reference in New Issue
Block a user