Merge branch 'canary' into feat/stack-env-support

This commit is contained in:
Mauricio Siu
2024-12-08 18:35:40 -06:00
322 changed files with 39371 additions and 1168 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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 `

View File

@@ -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 += `

View File

@@ -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) {

View File

@@ -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) => {

View File

@@ -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];

View File

@@ -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",

View File

@@ -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");

View File

@@ -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;
}
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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}`,

View File

@@ -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");

View 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;
};

View File

@@ -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(),

View File

@@ -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(),

View File

@@ -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}\`\`\``,
},
]
: []),

View File

@@ -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(),

View File

@@ -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,
},
],

View File

@@ -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();

View File

@@ -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};