From 94786c738bb255378a581e591c798aa780341239 Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:27:00 +0100 Subject: [PATCH 01/13] feat: added env support for dokploy --- .../services/compose/[composeId].tsx | 10 +--- packages/server/src/utils/builders/compose.ts | 48 ++++++++++++++++++- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 60ddfeab..bcac11ed 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -185,18 +185,12 @@ const Service = (
General - {data?.composeType === "docker-compose" && ( - Environment - )} + Environment {!data?.serverId && ( Monitoring )} diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 7d3ce0ec..c2924897 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -2,12 +2,14 @@ import { createWriteStream, existsSync, mkdirSync, + readFileSync, writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; +import dotenv from "dotenv"; import { writeDomainsToCompose, writeDomainsToComposeRemote, @@ -28,6 +30,7 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => { const command = createCommand(compose); await writeDomainsToCompose(compose, domains); createEnvFile(compose); + processComposeFile(compose); const logContent = ` App Name: ${appName} @@ -84,6 +87,7 @@ export const getBuildComposeCommand = async ( const command = createCommand(compose); const envCommand = getCreateEnvFileCommand(compose); const projectPath = join(COMPOSE_PATH, compose.appName, "code"); + const processComposeFileCommand = getProcessComposeFileCommand(compose); const newCompose = await writeDomainsToComposeRemote( compose, @@ -119,6 +123,8 @@ Compose Type: ${composeType} ✅`; cd "${projectPath}"; + ${processComposeFileCommand} + docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; } echo "Docker Compose Deployed: ✅" >> "${logPath}" @@ -145,7 +151,7 @@ export const createCommand = (compose: ComposeNested) => { const { composeType, appName, sourceType } = compose; const path = - sourceType === "raw" ? "docker-compose.yml" : compose.composePath; + sourceType === "raw" ? "docker-compose.processed.yml" : compose.composePath; let command = ""; if (composeType === "docker-compose") { @@ -188,6 +194,46 @@ const createEnvFile = (compose: ComposeNested) => { writeFileSync(envFilePath, envFileContent); }; +export const processComposeFile = (compose: ComposeNested) => { + const { COMPOSE_PATH } = paths(); + const { env, appName, sourceType, composeType } = compose; + + if (composeType === "stack") { + const inputPath = + sourceType === "raw" ? "docker-compose.yml" : compose.composePath; + const composeInputFilePath = + join(COMPOSE_PATH, appName, "code", inputPath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); + + const outputPath = "docker-compose.processed.yml"; + const composeOutputFilePath = + join(COMPOSE_PATH, appName, "code", outputPath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.processed.yml"); + + const envContent = prepareEnvironmentVariables(env || "").join("\n"); + const envVariables = dotenv.parse(envContent); + + let templateContent = readFileSync(composeInputFilePath, "utf8"); + + templateContent = templateContent.replace( + /\$\{([^}]+)\}/g, + (_, varName) => { + return envVariables[varName] || ""; + }, + ); + + writeFileSync(composeOutputFilePath, templateContent); + } +}; + +export const getProcessComposeFileCommand = (compose: ComposeNested) => { + const { composeType } = compose; + if (composeType === "stack") { + return "set -a; source .env; set +a; envsubst < docker-compose.yml > docker-compose.processed.yml"; + } + return "cp docker-compose.yml docker-compose.processed.yml"; +}; + export const getCreateEnvFileCommand = (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(true); const { env, composePath, appName } = compose; From cb02deb837065438cc6de1f431d9297485b1279a Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:15:12 +0100 Subject: [PATCH 02/13] fix(builder): fixed docker-compose issue --- packages/server/src/utils/builders/compose.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index c2924897..fd9824eb 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -198,32 +198,32 @@ export const processComposeFile = (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(); const { env, appName, sourceType, composeType } = compose; + const inputPath = + sourceType === "raw" ? "docker-compose.yml" : compose.composePath; + const composeInputFilePath = + join(COMPOSE_PATH, appName, "code", inputPath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); + + const outputPath = "docker-compose.processed.yml"; + const composeOutputFilePath = + join(COMPOSE_PATH, appName, "code", outputPath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.processed.yml"); + + let templateContent = readFileSync(composeInputFilePath, "utf8"); + if (composeType === "stack") { - const inputPath = - sourceType === "raw" ? "docker-compose.yml" : compose.composePath; - const composeInputFilePath = - join(COMPOSE_PATH, appName, "code", inputPath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - - const outputPath = "docker-compose.processed.yml"; - const composeOutputFilePath = - join(COMPOSE_PATH, appName, "code", outputPath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.processed.yml"); - const envContent = prepareEnvironmentVariables(env || "").join("\n"); const envVariables = dotenv.parse(envContent); - let templateContent = readFileSync(composeInputFilePath, "utf8"); - templateContent = templateContent.replace( /\$\{([^}]+)\}/g, (_, varName) => { return envVariables[varName] || ""; }, ); - - writeFileSync(composeOutputFilePath, templateContent); } + + writeFileSync(composeOutputFilePath, templateContent); }; export const getProcessComposeFileCommand = (compose: ComposeNested) => { From dc1e12d6ed19ae1ec9d2e7b69b97d6a35464bd8b Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Fri, 1 Nov 2024 23:35:42 +0100 Subject: [PATCH 03/13] feat(compose): added stop functionality for stack --- .../components/dashboard/compose/general/actions.tsx | 2 +- packages/server/src/services/compose.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index 365e37f5..37f8ffa8 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -71,7 +71,7 @@ export const ComposeActions = ({ composeId }: Props) => { Autodeploy {data?.autoDeploy && } - {data?.composeType === "docker-compose" && ( + {["running", "done"].includes(data?.composeStatus || "") && ( )} diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 61d7e5fc..7382fa99 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -476,6 +476,17 @@ export const stopCompose = async (composeId: string) => { } } + if (compose.composeType === "stack") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `docker stack rm ${compose.appName}`, + ); + } else { + await execAsync(`docker stack rm ${compose.appName}`); + } + } + await updateCompose(composeId, { composeStatus: "idle", }); From 06081627e85fec85198f9bc7c17ae1b0239251b0 Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:35:38 +0100 Subject: [PATCH 04/13] refactor: used docker stack config --- packages/server/src/utils/builders/compose.ts | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index fd9824eb..8d521d61 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -15,7 +15,7 @@ import { writeDomainsToComposeRemote, } from "../docker/domain"; import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils"; -import { execAsyncRemote } from "../process/execAsync"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< @@ -30,7 +30,7 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => { const command = createCommand(compose); await writeDomainsToCompose(compose, domains); createEnvFile(compose); - processComposeFile(compose); + await processComposeFile(compose); const logContent = ` App Name: ${appName} @@ -194,44 +194,34 @@ const createEnvFile = (compose: ComposeNested) => { writeFileSync(envFilePath, envFileContent); }; -export const processComposeFile = (compose: ComposeNested) => { +export const processComposeFile = async (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(); - const { env, appName, sourceType, composeType } = compose; + let command = getProcessComposeFileCommand(compose); - const inputPath = - sourceType === "raw" ? "docker-compose.yml" : compose.composePath; - const composeInputFilePath = - join(COMPOSE_PATH, appName, "code", inputPath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - - const outputPath = "docker-compose.processed.yml"; - const composeOutputFilePath = - join(COMPOSE_PATH, appName, "code", outputPath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.processed.yml"); - - let templateContent = readFileSync(composeInputFilePath, "utf8"); - - if (composeType === "stack") { - const envContent = prepareEnvironmentVariables(env || "").join("\n"); - const envVariables = dotenv.parse(envContent); - - templateContent = templateContent.replace( - /\$\{([^}]+)\}/g, - (_, varName) => { - return envVariables[varName] || ""; - }, - ); + if (compose.serverId) { + command = `cd ${join(COMPOSE_PATH, compose.appName, "code")} && ${command}`; + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command, { + cwd: join(COMPOSE_PATH, compose.appName, "code"), + }); } - - writeFileSync(composeOutputFilePath, templateContent); }; export const getProcessComposeFileCommand = (compose: ComposeNested) => { const { composeType } = compose; + + let command = ""; + if (composeType === "stack") { - return "set -a; source .env; set +a; envsubst < docker-compose.yml > docker-compose.processed.yml"; + command = `export $(grep -v '^#' .env | xargs) && docker stack config -c docker-compose.yml > docker-compose.processed.yml`; } - return "cp docker-compose.yml docker-compose.processed.yml"; + + if (composeType === "docker-compose") { + command = "cp docker-compose.yml docker-compose.processed.yml"; + } + + return command; }; export const getCreateEnvFileCommand = (compose: ComposeNested) => { From 9c355bcfb7426fc31b0f0b57d46989f96256c0c2 Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Sat, 2 Nov 2024 12:42:53 +0100 Subject: [PATCH 05/13] refactor(builder): removed unused and redundant code --- packages/server/src/utils/builders/compose.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 8d521d61..dd48e0c2 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -2,14 +2,12 @@ import { createWriteStream, existsSync, mkdirSync, - readFileSync, writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; -import dotenv from "dotenv"; import { writeDomainsToCompose, writeDomainsToComposeRemote, @@ -196,10 +194,9 @@ const createEnvFile = (compose: ComposeNested) => { export const processComposeFile = async (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(); - let command = getProcessComposeFileCommand(compose); + const command = getProcessComposeFileCommand(compose); if (compose.serverId) { - command = `cd ${join(COMPOSE_PATH, compose.appName, "code")} && ${command}`; await execAsyncRemote(compose.serverId, command); } else { await execAsync(command, { From 06cbd1fce1a96692917e7c6b88b87155174e77d7 Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Sun, 3 Nov 2024 15:26:54 +0100 Subject: [PATCH 06/13] refactor(bundler): removed redundant code --- packages/server/src/utils/builders/compose.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index dd48e0c2..514d7bd8 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -148,8 +148,17 @@ const sanitizeCommand = (command: string) => { export const createCommand = (compose: ComposeNested) => { const { composeType, appName, sourceType } = compose; - const path = - sourceType === "raw" ? "docker-compose.processed.yml" : compose.composePath; + let path = ""; + + if (sourceType !== "raw") { + path = compose.composePath; + } else { + path = + composeType === "stack" + ? "docker-compose.processed.yml" + : "docker-compose.yml"; + } + let command = ""; if (composeType === "docker-compose") { @@ -196,13 +205,9 @@ export const processComposeFile = async (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(); const command = getProcessComposeFileCommand(compose); - if (compose.serverId) { - await execAsyncRemote(compose.serverId, command); - } else { - await execAsync(command, { - cwd: join(COMPOSE_PATH, compose.appName, "code"), - }); - } + await execAsync(command, { + cwd: join(COMPOSE_PATH, compose.appName, "code"), + }); }; export const getProcessComposeFileCommand = (compose: ComposeNested) => { @@ -214,10 +219,6 @@ export const getProcessComposeFileCommand = (compose: ComposeNested) => { command = `export $(grep -v '^#' .env | xargs) && docker stack config -c docker-compose.yml > docker-compose.processed.yml`; } - if (composeType === "docker-compose") { - command = "cp docker-compose.yml docker-compose.processed.yml"; - } - return command; }; From f772fec407d937b92812d092ac00001cb7744a44 Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:02:46 +0100 Subject: [PATCH 07/13] fix(bundler): docker-compose bug --- packages/server/src/utils/builders/compose.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 514d7bd8..8941f4e1 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -150,15 +150,17 @@ export const createCommand = (compose: ComposeNested) => { let path = ""; - if (sourceType !== "raw") { - path = compose.composePath; - } else { + if (sourceType === "raw") { path = composeType === "stack" ? "docker-compose.processed.yml" : "docker-compose.yml"; + } else { + path = compose.composePath; } + console.log(path); + let command = ""; if (composeType === "docker-compose") { @@ -203,11 +205,12 @@ const createEnvFile = (compose: ComposeNested) => { export const processComposeFile = async (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(); - const command = getProcessComposeFileCommand(compose); - - await execAsync(command, { - cwd: join(COMPOSE_PATH, compose.appName, "code"), - }); + if (compose.composeType === "stack") { + const command = getProcessComposeFileCommand(compose); + await execAsync(command, { + cwd: join(COMPOSE_PATH, compose.appName, "code"), + }); + } }; export const getProcessComposeFileCommand = (compose: ComposeNested) => { From dafed3096f80c282e1033c2c6e51393db1fdd16f Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:47:58 +0100 Subject: [PATCH 08/13] refactor(builder): removed path log --- packages/server/src/utils/builders/compose.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 8941f4e1..38d94ab1 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -159,8 +159,6 @@ export const createCommand = (compose: ComposeNested) => { path = compose.composePath; } - console.log(path); - let command = ""; if (composeType === "docker-compose") { From c9b570e4697607141e246f88b194eb1539d94bd4 Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Sat, 9 Nov 2024 13:00:15 +0100 Subject: [PATCH 09/13] fix(builder): fixed issues on non-raw compose and external servers --- packages/server/src/utils/builders/compose.ts | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 38d94ab1..233e236b 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -148,32 +148,22 @@ const sanitizeCommand = (command: string) => { export const createCommand = (compose: ComposeNested) => { const { composeType, appName, sourceType } = compose; - let path = ""; - - if (sourceType === "raw") { - path = - composeType === "stack" + const path = + sourceType === "raw" + ? composeType === "stack" ? "docker-compose.processed.yml" - : "docker-compose.yml"; - } else { - path = compose.composePath; - } + : "docker-compose.yml" + : composeType === "stack" + ? "docker-compose.processed.yml" + : compose.composePath; - let command = ""; - - if (composeType === "docker-compose") { - command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; - } else if (composeType === "stack") { - command = `stack deploy -c ${path} ${appName} --prune`; - } + const baseCommand = + composeType === "docker-compose" + ? `compose -p ${appName} -f ${path} up -d --build --remove-orphans` + : `stack deploy -c ${path} ${appName} --prune`; const customCommand = sanitizeCommand(compose.command); - - if (customCommand) { - command = `${command} ${customCommand}`; - } - - return command; + return customCommand ? `${baseCommand} ${customCommand}` : baseCommand; }; const createEnvFile = (compose: ComposeNested) => { @@ -212,12 +202,16 @@ export const processComposeFile = async (compose: ComposeNested) => { }; export const getProcessComposeFileCommand = (compose: ComposeNested) => { - const { composeType } = compose; + const { composeType, sourceType } = compose; + const composePath = + sourceType === "raw" ? "docker-compose.yml" : compose.composePath; let command = ""; - if (composeType === "stack") { - command = `export $(grep -v '^#' .env | xargs) && docker stack config -c docker-compose.yml > docker-compose.processed.yml`; + command = [ + "export $(grep -v '^#' .env | xargs)", + `docker stack config -c ${composePath} > docker-compose.processed.yml`, + ].join(" && "); } return command; From 65ee0a3e2264d9325cbb800e533d5f45ce9b889d Mon Sep 17 00:00:00 2001 From: xenonwellz <59710311+xenonwellz@users.noreply.github.com> Date: Mon, 11 Nov 2024 02:22:48 +0100 Subject: [PATCH 10/13] fix(builder): created processed file in the same directory as main stack.yml --- packages/server/src/utils/builders/compose.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 233e236b..b09333ba 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -154,7 +154,7 @@ export const createCommand = (compose: ComposeNested) => { ? "docker-compose.processed.yml" : "docker-compose.yml" : composeType === "stack" - ? "docker-compose.processed.yml" + ? join(dirname(compose.composePath), "docker-compose.processed.yml") : compose.composePath; const baseCommand = @@ -206,11 +206,17 @@ export const getProcessComposeFileCommand = (compose: ComposeNested) => { const composePath = sourceType === "raw" ? "docker-compose.yml" : compose.composePath; + + const destinationPath = + sourceType === "raw" + ? "docker-compose.processed.yml" + : join(dirname(compose.composePath), "docker-compose.processed.yml"); + let command = ""; if (composeType === "stack") { command = [ "export $(grep -v '^#' .env | xargs)", - `docker stack config -c ${composePath} > docker-compose.processed.yml`, + `docker stack config -c ${composePath} > ${destinationPath}`, ].join(" && "); } From f7a29accb1d8335b200dca58cc6d911cc7158b16 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:40:01 -0600 Subject: [PATCH 11/13] refactor: lint --- apps/dokploy/templates/listmonk/index.ts | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/apps/dokploy/templates/listmonk/index.ts b/apps/dokploy/templates/listmonk/index.ts index f0dad544..2a25efca 100644 --- a/apps/dokploy/templates/listmonk/index.ts +++ b/apps/dokploy/templates/listmonk/index.ts @@ -1,30 +1,30 @@ import { - type DomainSchema, - type Schema, - type Template, - generateRandomDomain, + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, } from "../utils"; export function generate(schema: Schema): Template { - const randomDomain = generateRandomDomain(schema); + const randomDomain = generateRandomDomain(schema); - const domains: DomainSchema[] = [ - { - host: randomDomain, - port: 9000, - serviceName: "app", - }, - ]; + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 9000, + serviceName: "app", + }, + ]; - const envs = [ - "# visit the page to setup your super admin user", - "# check config.toml in Advanced / Volumes for more options", - ]; + const envs = [ + "# visit the page to setup your super admin user", + "# check config.toml in Advanced / Volumes for more options", + ]; - const mounts: Template["mounts"] = [ - { - filePath: "config.toml", - content: `[app] + const mounts: Template["mounts"] = [ + { + filePath: "config.toml", + content: `[app] address = "0.0.0.0:9000" [db] @@ -41,12 +41,12 @@ max_lifetime = "300s" params = "" `, - }, - ]; + }, + ]; - return { - envs, - mounts, - domains, - }; + return { + envs, + mounts, + domains, + }; } From 009859faa9dfffc755da97c95c10f5acb3105b47 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 31 Jan 2025 01:20:10 -0600 Subject: [PATCH 12/13] refactor: add .env docker stack --- .../services/compose/[composeId].tsx | 6 +- packages/server/src/utils/builders/compose.ts | 339 ++++---- packages/server/src/utils/docker/utils.ts | 815 +++++++++--------- 3 files changed, 571 insertions(+), 589 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index 9f4e9086..f9ece2e7 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -211,12 +211,12 @@ const Service = ( diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 2573626a..03da7858 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -1,99 +1,105 @@ import { - createWriteStream, - existsSync, - mkdirSync, - writeFileSync, + createWriteStream, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; import { - writeDomainsToCompose, - writeDomainsToComposeRemote, + writeDomainsToCompose, + writeDomainsToComposeRemote, } from "../docker/domain"; -import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils"; +import { + encodeBase64, + getEnviromentVariablesObject, + prepareEnvironmentVariables, +} from "../docker/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< - "compose", - { project: true; mounts: true; domains: true } + "compose", + { project: true; mounts: true; domains: true } >; export const buildCompose = async (compose: ComposeNested, logPath: string) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { sourceType, appName, mounts, composeType, domains } = compose; - try { - const { COMPOSE_PATH } = paths(); - const command = createCommand(compose); - await writeDomainsToCompose(compose, domains); - createEnvFile(compose); - await processComposeFile(compose); + const writeStream = createWriteStream(logPath, { flags: "a" }); + const { sourceType, appName, mounts, composeType, domains } = compose; + try { + const { COMPOSE_PATH } = paths(); + const command = createCommand(compose); + await writeDomainsToCompose(compose, domains); + createEnvFile(compose); - const logContent = ` -App Name: ${appName} -Build Compose 🐳 -Detected: ${mounts.length} mounts 📂 -Command: docker ${command} -Source Type: docker ${sourceType} ✅ -Compose Type: ${composeType} ✅`; - const logBox = boxen(logContent, { - padding: { - left: 1, - right: 1, - bottom: 1, - }, - width: 80, - borderStyle: "double", - }); - writeStream.write(`\n${logBox}\n`); + const logContent = ` + App Name: ${appName} + Build Compose 🐳 + Detected: ${mounts.length} mounts 📂 + Command: docker ${command} + Source Type: docker ${sourceType} ✅ + Compose Type: ${composeType} ✅`; + const logBox = boxen(logContent, { + padding: { + left: 1, + right: 1, + bottom: 1, + }, + width: 80, + borderStyle: "double", + }); + writeStream.write(`\n${logBox}\n`); + const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); + await spawnAsync( + "docker", + [...command.split(" ")], + (data) => { + if (writeStream.writable) { + writeStream.write(data.toString()); + } + }, + { + cwd: projectPath, + env: { + NODE_ENV: process.env.NODE_ENV, + PATH: process.env.PATH, + ...(composeType === "stack" && { + ...getEnviromentVariablesObject(compose.env, compose.project.env), + }), + }, + } + ); - await spawnAsync( - "docker", - [...command.split(" ")], - (data) => { - if (writeStream.writable) { - writeStream.write(data.toString()); - } - }, - { - cwd: projectPath, - env: { - NODE_ENV: process.env.NODE_ENV, - PATH: process.env.PATH, - }, - }, - ); - - writeStream.write("Docker Compose Deployed: ✅"); - } catch (error) { - writeStream.write(`Error ❌ ${(error as Error).message}`); - throw error; - } finally { - writeStream.end(); - } + writeStream.write("Docker Compose Deployed: ✅"); + } catch (error) { + writeStream.write(`Error ❌ ${(error as Error).message}`); + throw error; + } finally { + writeStream.end(); + } }; export const getBuildComposeCommand = async ( - compose: ComposeNested, - logPath: string, + compose: ComposeNested, + logPath: string ) => { - const { COMPOSE_PATH } = paths(true); - const { sourceType, appName, mounts, composeType, domains, composePath } = - compose; - const command = createCommand(compose); - const envCommand = getCreateEnvFileCommand(compose); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - const processComposeFileCommand = getProcessComposeFileCommand(compose); + const { COMPOSE_PATH } = paths(true); + const { sourceType, appName, mounts, composeType, domains, composePath } = + compose; + const command = createCommand(compose); + const envCommand = getCreateEnvFileCommand(compose); + const projectPath = join(COMPOSE_PATH, compose.appName, "code"); + const exportEnvCommand = getExportEnvCommand(compose); - const newCompose = await writeDomainsToComposeRemote( - compose, - domains, - logPath, - ); - const logContent = ` + const newCompose = await writeDomainsToComposeRemote( + compose, + domains, + logPath + ); + const logContent = ` App Name: ${appName} Build Compose 🐳 Detected: ${mounts.length} mounts 📂 @@ -101,17 +107,17 @@ Command: docker ${command} Source Type: docker ${sourceType} ✅ Compose Type: ${composeType} ✅`; - const logBox = boxen(logContent, { - padding: { - left: 1, - right: 1, - bottom: 1, - }, - width: 80, - borderStyle: "double", - }); + const logBox = boxen(logContent, { + padding: { + left: 1, + right: 1, + bottom: 1, + }, + width: 80, + borderStyle: "double", + }); - const bashCommand = ` + const bashCommand = ` set -e { echo "${logBox}" >> "${logPath}" @@ -122,7 +128,7 @@ Compose Type: ${composeType} ✅`; cd "${projectPath}"; - ${processComposeFileCommand} + ${exportEnvCommand} docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; } @@ -133,129 +139,102 @@ Compose Type: ${composeType} ✅`; } `; - return await execAsyncRemote(compose.serverId, bashCommand); + return await execAsyncRemote(compose.serverId, bashCommand); }; const sanitizeCommand = (command: string) => { - const sanitizedCommand = command.trim(); + const sanitizedCommand = command.trim(); - const parts = sanitizedCommand.split(/\s+/); + const parts = sanitizedCommand.split(/\s+/); - const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); + const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); - return restCommand.join(" "); + return restCommand.join(" "); }; export const createCommand = (compose: ComposeNested) => { - const { composeType, appName, sourceType } = compose; + const { composeType, appName, sourceType } = compose; - if (compose.command) { - return `${sanitizeCommand(compose.command)}`; - } + if (compose.command) { + return `${sanitizeCommand(compose.command)}`; + } - const path = - sourceType === "raw" - ? composeType === "stack" - ? "docker-compose.processed.yml" - : "docker-compose.yml" - : composeType === "stack" - ? join(dirname(compose.composePath), "docker-compose.processed.yml") - : compose.composePath; + const path = + sourceType === "raw" ? "docker-compose.yml" : compose.composePath; + let command = ""; - const baseCommand = - composeType === "docker-compose" - ? `compose -p ${appName} -f ${path} up -d --build --remove-orphans` - : `stack deploy -c ${path} ${appName} --prune`; - const customCommand = sanitizeCommand(compose.command); - return customCommand ? `${baseCommand} ${customCommand}` : baseCommand; + if (composeType === "stack") { + command = `stack deploy -c ${path} ${appName} --prune`; + } + + return command; }; const createEnvFile = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(); - const { env, composePath, appName } = compose; - const composeFilePath = - join(COMPOSE_PATH, appName, "code", composePath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); + const { COMPOSE_PATH } = paths(); + const { env, composePath, appName } = compose; + const composeFilePath = + join(COMPOSE_PATH, appName, "code", composePath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = env || ""; - if (!envContent.includes("DOCKER_CONFIG")) { - envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; - } + const envFilePath = join(dirname(composeFilePath), ".env"); + let envContent = env || ""; + if (!envContent.includes("DOCKER_CONFIG")) { + envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; + } - if (compose.randomize) { - envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; - } + if (compose.randomize) { + envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; + } - const envFileContent = prepareEnvironmentVariables( - envContent, - compose.project.env, - ).join("\n"); + const envFileContent = prepareEnvironmentVariables( + envContent, + compose.project.env + ).join("\n"); - if (!existsSync(dirname(envFilePath))) { - mkdirSync(dirname(envFilePath), { recursive: true }); - } - writeFileSync(envFilePath, envFileContent); -}; - -export const processComposeFile = async (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(); - if (compose.composeType === "stack") { - const command = getProcessComposeFileCommand(compose); - await execAsync(command, { - cwd: join(COMPOSE_PATH, compose.appName, "code"), - }); - } -}; - -export const getProcessComposeFileCommand = (compose: ComposeNested) => { - const { composeType, sourceType } = compose; - - const composePath = - sourceType === "raw" ? "docker-compose.yml" : compose.composePath; - - const destinationPath = - sourceType === "raw" - ? "docker-compose.processed.yml" - : join(dirname(compose.composePath), "docker-compose.processed.yml"); - - let command = ""; - if (composeType === "stack") { - command = [ - "export $(grep -v '^#' .env | xargs)", - `docker stack config -c ${composePath} > ${destinationPath}`, - ].join(" && "); - } - - return command; + if (!existsSync(dirname(envFilePath))) { + mkdirSync(dirname(envFilePath), { recursive: true }); + } + writeFileSync(envFilePath, envFileContent); }; export const getCreateEnvFileCommand = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(true); - const { env, composePath, appName } = compose; - const composeFilePath = - join(COMPOSE_PATH, appName, "code", composePath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); + const { COMPOSE_PATH } = paths(true); + const { env, composePath, appName } = compose; + const composeFilePath = + join(COMPOSE_PATH, appName, "code", composePath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - const envFilePath = join(dirname(composeFilePath), ".env"); + const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = env || ""; - if (!envContent.includes("DOCKER_CONFIG")) { - envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; - } + let envContent = env || ""; + if (!envContent.includes("DOCKER_CONFIG")) { + envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; + } - if (compose.randomize) { - envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; - } + if (compose.randomize) { + envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; + } - const envFileContent = prepareEnvironmentVariables( - envContent, - compose.project.env, - ).join("\n"); + const envFileContent = prepareEnvironmentVariables( + envContent, + compose.project.env + ).join("\n"); - const encodedContent = encodeBase64(envFileContent); - return ` + const encodedContent = encodeBase64(envFileContent); + return ` touch ${envFilePath}; echo "${encodedContent}" | base64 -d > "${envFilePath}"; `; }; + +const getExportEnvCommand = (compose: ComposeNested) => { + if (compose.composeType !== "stack") return ""; + + const envVars = getEnviromentVariablesObject(compose.env, compose.project.env); + const exports = Object.entries(envVars) + .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) + .join("\n"); + + return exports ? `\n# Export environment variables\n${exports}\n` : ""; +}; diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 64e98306..96b2e08b 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -15,526 +15,529 @@ import { spawnAsync } from "../process/spawnAsync"; import { getRemoteDocker } from "../servers/remote-docker"; interface RegistryAuth { - username: string; - password: string; - registryUrl: string; + username: string; + password: string; + registryUrl: string; } export const pullImage = async ( - dockerImage: string, - onData?: (data: any) => void, - authConfig?: Partial, + dockerImage: string, + onData?: (data: any) => void, + authConfig?: Partial ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } - 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; - } + 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; + } }; export const pullRemoteImage = async ( - dockerImage: string, - serverId: string, - onData?: (data: any) => void, - authConfig?: Partial, + dockerImage: string, + serverId: string, + onData?: (data: any) => void, + authConfig?: Partial ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } - const remoteDocker = await getRemoteDocker(serverId); + const remoteDocker = await getRemoteDocker(serverId); - await new Promise((resolve, reject) => { - remoteDocker.pull( - dockerImage, - { authconfig: authConfig }, - (err, stream) => { - if (err) { - reject(err); - return; - } + await new Promise((resolve, reject) => { + remoteDocker.pull( + dockerImage, + { authconfig: authConfig }, + (err, stream) => { + if (err) { + reject(err); + return; + } - remoteDocker.modem.followProgress( - stream as Readable, - (err: Error | null, res) => { - if (!err) { - resolve(res); - } - if (err) { - reject(err); - } - }, - (event) => { - onData?.(event); - }, - ); - }, - ); - }); - } catch (error) { - throw error; - } + remoteDocker.modem.followProgress( + stream as Readable, + (err: Error | null, res) => { + if (!err) { + resolve(res); + } + if (err) { + reject(err); + } + }, + (event) => { + onData?.(event); + } + ); + } + ); + }); + } catch (error) { + throw error; + } }; export const containerExists = async (containerName: string) => { - const container = docker.getContainer(containerName); - try { - await container.inspect(); - return true; - } catch (error) { - return false; - } + const container = docker.getContainer(containerName); + try { + await container.inspect(); + return true; + } catch (error) { + return false; + } }; export const stopService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsync(`docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const stopServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const getContainerByName = (name: string): Promise => { - const opts = { - limit: 1, - filters: { - name: [name], - }, - }; - return new Promise((resolve, reject) => { - docker.listContainers(opts, (err, containers) => { - if (err) { - reject(err); - } else if (containers?.length === 0) { - reject(new Error(`No container found with name: ${name}`)); - } else if (containers && containers?.length > 0 && containers[0]) { - resolve(containers[0]); - } - }); - }); + const opts = { + limit: 1, + filters: { + name: [name], + }, + }; + return new Promise((resolve, reject) => { + docker.listContainers(opts, (err, containers) => { + if (err) { + reject(err); + } else if (containers?.length === 0) { + reject(new Error(`No container found with name: ${name}`)); + } else if (containers && containers?.length > 0 && containers[0]) { + resolve(containers[0]); + } + }); + }); }; export const cleanUpUnusedImages = async (serverId?: string) => { - try { - const command = "docker image prune --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - console.error(error); - throw error; - } + try { + const command = "docker image prune --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanStoppedContainers = async (serverId?: string) => { - try { - const command = "docker container prune --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - console.error(error); - throw error; - } + try { + const command = "docker container prune --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpUnusedVolumes = async (serverId?: string) => { - try { - const command = "docker volume prune --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - console.error(error); - throw error; - } + try { + const command = "docker volume prune --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpInactiveContainers = async () => { - try { - const containers = await docker.listContainers({ all: true }); - const inactiveContainers = containers.filter( - (container) => container.State !== "running", - ); + try { + const containers = await docker.listContainers({ all: true }); + const inactiveContainers = containers.filter( + (container) => container.State !== "running" + ); - for (const container of inactiveContainers) { - await docker.getContainer(container.Id).remove({ force: true }); - console.log(`Cleaning up inactive container: ${container.Id}`); - } - } catch (error) { - console.error("Error cleaning up inactive containers:", error); - throw error; - } + for (const container of inactiveContainers) { + await docker.getContainer(container.Id).remove({ force: true }); + console.log(`Cleaning up inactive container: ${container.Id}`); + } + } catch (error) { + console.error("Error cleaning up inactive containers:", error); + throw error; + } }; export const cleanUpDockerBuilder = async (serverId?: string) => { - const command = "docker builder prune --all --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } + const command = "docker builder prune --all --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } }; export const cleanUpSystemPrune = async (serverId?: string) => { - const command = "docker system prune --all --force --volumes"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } + const command = "docker system prune --all --force --volumes"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } }; export const startService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsync(`docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const startServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const removeService = async ( - appName: string, - serverId?: string | null, - deleteVolumes = false, + appName: string, + serverId?: string | null, + deleteVolumes = false ) => { - try { - const command = `docker service rm ${appName}`; + try { + const command = `docker service rm ${appName}`; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - return error; - } + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + return error; + } }; export const prepareEnvironmentVariables = ( - serviceEnv: string | null, - projectEnv?: string | null, + serviceEnv: string | null, + projectEnv?: string | null ) => { - const projectVars = parse(projectEnv ?? ""); - const serviceVars = parse(serviceEnv ?? ""); + 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}`; - }); + 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; + return resolvedVars; }; -export const prepareBuildArgs = (input: string | null) => { - const pairs = (input ?? "").split("\n"); +export const getEnviromentVariablesObject = ( + input: string | null, + projectEnv?: string | null +) => { + const envs = prepareEnvironmentVariables(input, projectEnv); - const jsonObject: Record = {}; + const jsonObject: Record = {}; - for (const pair of pairs) { - const [key, value] = pair.split("="); - if (key && value) { - jsonObject[key] = value; - } - } + for (const pair of envs) { + const [key, value] = pair.split("="); + if (key && value) { + jsonObject[key] = value; + } + } - return jsonObject; + return jsonObject; }; export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } + if (!mounts || mounts.length === 0) { + return []; + } - return mounts - .filter((mount) => mount.type === "volume") - .map((mount) => ({ - Type: "volume" as const, - Source: mount.volumeName || "", - Target: mount.mountPath, - })); + return mounts + .filter((mount) => mount.type === "volume") + .map((mount) => ({ + Type: "volume" as const, + Source: mount.volumeName || "", + Target: mount.mountPath, + })); }; type Resources = { - memoryLimit: string | null; - memoryReservation: string | null; - cpuLimit: string | null; - cpuReservation: string | null; + memoryLimit: string | null; + memoryReservation: string | null; + cpuLimit: string | null; + cpuReservation: string | null; }; export const calculateResources = ({ - memoryLimit, - memoryReservation, - cpuLimit, - cpuReservation, + memoryLimit, + memoryReservation, + cpuLimit, + cpuReservation, }: Resources): ResourceRequirements => { - return { - Limits: { - MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined, - NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined, - }, - Reservations: { - MemoryBytes: memoryReservation - ? Number.parseInt(memoryReservation) - : undefined, - NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined, - }, - }; + return { + Limits: { + MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined, + NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined, + }, + Reservations: { + MemoryBytes: memoryReservation + ? Number.parseInt(memoryReservation) + : undefined, + NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined, + }, + }; }; export const generateConfigContainer = (application: ApplicationNested) => { - const { - healthCheckSwarm, - restartPolicySwarm, - placementSwarm, - updateConfigSwarm, - rollbackConfigSwarm, - modeSwarm, - labelsSwarm, - replicas, - mounts, - networkSwarm, - } = application; + const { + healthCheckSwarm, + restartPolicySwarm, + placementSwarm, + updateConfigSwarm, + rollbackConfigSwarm, + modeSwarm, + labelsSwarm, + replicas, + mounts, + networkSwarm, + } = application; - const haveMounts = mounts.length > 0; + const haveMounts = mounts.length > 0; - return { - ...(healthCheckSwarm && { - HealthCheck: healthCheckSwarm, - }), - ...(restartPolicySwarm - ? { - RestartPolicy: restartPolicySwarm, - } - : {}), - ...(placementSwarm - ? { - Placement: placementSwarm, - } - : { - // if app have mounts keep manager as constraint - Placement: { - Constraints: haveMounts ? ["node.role==manager"] : [], - }, - }), - ...(labelsSwarm && { - Labels: labelsSwarm, - }), - ...(modeSwarm - ? { - Mode: modeSwarm, - } - : { - // use replicas value if no modeSwarm provided - Mode: { - Replicated: { - Replicas: replicas, - }, - }, - }), - ...(rollbackConfigSwarm && { - RollbackConfig: rollbackConfigSwarm, - }), - ...(updateConfigSwarm - ? { UpdateConfig: updateConfigSwarm } - : { - // default config if no updateConfigSwarm provided - UpdateConfig: { - Parallelism: 1, - Order: "start-first", - }, - }), - ...(networkSwarm - ? { - Networks: networkSwarm, - } - : { - Networks: [{ Target: "dokploy-network" }], - }), - }; + return { + ...(healthCheckSwarm && { + HealthCheck: healthCheckSwarm, + }), + ...(restartPolicySwarm + ? { + RestartPolicy: restartPolicySwarm, + } + : {}), + ...(placementSwarm + ? { + Placement: placementSwarm, + } + : { + // if app have mounts keep manager as constraint + Placement: { + Constraints: haveMounts ? ["node.role==manager"] : [], + }, + }), + ...(labelsSwarm && { + Labels: labelsSwarm, + }), + ...(modeSwarm + ? { + Mode: modeSwarm, + } + : { + // use replicas value if no modeSwarm provided + Mode: { + Replicated: { + Replicas: replicas, + }, + }, + }), + ...(rollbackConfigSwarm && { + RollbackConfig: rollbackConfigSwarm, + }), + ...(updateConfigSwarm + ? { UpdateConfig: updateConfigSwarm } + : { + // default config if no updateConfigSwarm provided + UpdateConfig: { + Parallelism: 1, + Order: "start-first", + }, + }), + ...(networkSwarm + ? { + Networks: networkSwarm, + } + : { + Networks: [{ Target: "dokploy-network" }], + }), + }; }; export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } + if (!mounts || mounts.length === 0) { + return []; + } - return mounts - .filter((mount) => mount.type === "bind") - .map((mount) => ({ - Type: "bind" as const, - Source: mount.hostPath || "", - Target: mount.mountPath, - })); + return mounts + .filter((mount) => mount.type === "bind") + .map((mount) => ({ + Type: "bind" as const, + Source: mount.hostPath || "", + Target: mount.mountPath, + })); }; export const generateFileMounts = ( - appName: string, - service: - | ApplicationNested - | MongoNested - | MariadbNested - | MysqlNested - | PostgresNested - | RedisNested, + appName: string, + service: + | ApplicationNested + | MongoNested + | MariadbNested + | MysqlNested + | PostgresNested + | RedisNested ) => { - const { mounts } = service; - const { APPLICATIONS_PATH } = paths(!!service.serverId); - if (!mounts || mounts.length === 0) { - return []; - } + const { mounts } = service; + const { APPLICATIONS_PATH } = paths(!!service.serverId); + if (!mounts || mounts.length === 0) { + return []; + } - return mounts - .filter((mount) => mount.type === "file") - .map((mount) => { - const fileName = mount.filePath; - const absoluteBasePath = path.resolve(APPLICATIONS_PATH); - const directory = path.join(absoluteBasePath, appName, "files"); - const sourcePath = path.join(directory, fileName || ""); - return { - Type: "bind" as const, - Source: sourcePath, - Target: mount.mountPath, - }; - }); + return mounts + .filter((mount) => mount.type === "file") + .map((mount) => { + const fileName = mount.filePath; + const absoluteBasePath = path.resolve(APPLICATIONS_PATH); + const directory = path.join(absoluteBasePath, appName, "files"); + const sourcePath = path.join(directory, fileName || ""); + return { + Type: "bind" as const, + Source: sourcePath, + Target: mount.mountPath, + }; + }); }; export const createFile = async ( - outputPath: string, - filePath: string, - content: string, + outputPath: string, + filePath: string, + content: string ) => { - try { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - fs.mkdirSync(fullPath, { recursive: true }); - return; - } + try { + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + fs.mkdirSync(fullPath, { recursive: true }); + return; + } - const directory = path.dirname(fullPath); - fs.mkdirSync(directory, { recursive: true }); - fs.writeFileSync(fullPath, content || ""); - } catch (error) { - throw error; - } + const directory = path.dirname(fullPath); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(fullPath, content || ""); + } catch (error) { + throw error; + } }; export const encodeBase64 = (content: string) => - Buffer.from(content, "utf-8").toString("base64"); + Buffer.from(content, "utf-8").toString("base64"); export const getCreateFileCommand = ( - outputPath: string, - filePath: string, - content: string, + outputPath: string, + filePath: string, + content: string ) => { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - return `mkdir -p ${fullPath};`; - } + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + return `mkdir -p ${fullPath};`; + } - const directory = path.dirname(fullPath); - const encodedContent = encodeBase64(content); - return ` + const directory = path.dirname(fullPath); + const encodedContent = encodeBase64(content); + return ` mkdir -p ${directory}; echo "${encodedContent}" | base64 -d > "${fullPath}"; `; }; export const getServiceContainer = async (appName: string) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } - const container = containers[0]; + const container = containers[0]; - return container; - } catch (error) { - throw error; - } + return container; + } catch (error) { + throw error; + } }; export const getRemoteServiceContainer = async ( - serverId: string, - appName: string, + serverId: string, + appName: string ) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; - const remoteDocker = await getRemoteDocker(serverId); - const containers = await remoteDocker.listContainers({ - filters: JSON.stringify(filter), - }); + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + const remoteDocker = await getRemoteDocker(serverId); + const containers = await remoteDocker.listContainers({ + filters: JSON.stringify(filter), + }); - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } - const container = containers[0]; + const container = containers[0]; - return container; - } catch (error) { - throw error; - } + return container; + } catch (error) { + throw error; + } }; From 7369b54f322c324d04a5296a64338d61e26432ee Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 31 Jan 2025 01:20:52 -0600 Subject: [PATCH 13/13] refactor: update --- .../services/compose/[composeId].tsx | 2 - packages/server/src/utils/builders/compose.ts | 295 +++---- packages/server/src/utils/docker/utils.ts | 814 +++++++++--------- 3 files changed, 556 insertions(+), 555 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index f9ece2e7..67c9c7b1 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -221,9 +221,7 @@ const Service = ( )} > General - {/* {data?.composeType === "docker-compose" && ( */} Environment - {/* )} */} {!data?.serverId && ( Monitoring )} diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 03da7858..af560728 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -1,105 +1,105 @@ import { - createWriteStream, - existsSync, - mkdirSync, - readFileSync, - writeFileSync, + createWriteStream, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; import { - writeDomainsToCompose, - writeDomainsToComposeRemote, + writeDomainsToCompose, + writeDomainsToComposeRemote, } from "../docker/domain"; import { - encodeBase64, - getEnviromentVariablesObject, - prepareEnvironmentVariables, + encodeBase64, + getEnviromentVariablesObject, + prepareEnvironmentVariables, } from "../docker/utils"; import { execAsync, execAsyncRemote } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< - "compose", - { project: true; mounts: true; domains: true } + "compose", + { project: true; mounts: true; domains: true } >; export const buildCompose = async (compose: ComposeNested, logPath: string) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { sourceType, appName, mounts, composeType, domains } = compose; - try { - const { COMPOSE_PATH } = paths(); - const command = createCommand(compose); - await writeDomainsToCompose(compose, domains); - createEnvFile(compose); + const writeStream = createWriteStream(logPath, { flags: "a" }); + const { sourceType, appName, mounts, composeType, domains } = compose; + try { + const { COMPOSE_PATH } = paths(); + const command = createCommand(compose); + await writeDomainsToCompose(compose, domains); + createEnvFile(compose); - const logContent = ` + const logContent = ` App Name: ${appName} Build Compose 🐳 Detected: ${mounts.length} mounts 📂 Command: docker ${command} Source Type: docker ${sourceType} ✅ Compose Type: ${composeType} ✅`; - const logBox = boxen(logContent, { - padding: { - left: 1, - right: 1, - bottom: 1, - }, - width: 80, - borderStyle: "double", - }); - writeStream.write(`\n${logBox}\n`); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); + const logBox = boxen(logContent, { + padding: { + left: 1, + right: 1, + bottom: 1, + }, + width: 80, + borderStyle: "double", + }); + writeStream.write(`\n${logBox}\n`); + const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - await spawnAsync( - "docker", - [...command.split(" ")], - (data) => { - if (writeStream.writable) { - writeStream.write(data.toString()); - } - }, - { - cwd: projectPath, - env: { - NODE_ENV: process.env.NODE_ENV, - PATH: process.env.PATH, - ...(composeType === "stack" && { - ...getEnviromentVariablesObject(compose.env, compose.project.env), - }), - }, - } - ); + await spawnAsync( + "docker", + [...command.split(" ")], + (data) => { + if (writeStream.writable) { + writeStream.write(data.toString()); + } + }, + { + cwd: projectPath, + env: { + NODE_ENV: process.env.NODE_ENV, + PATH: process.env.PATH, + ...(composeType === "stack" && { + ...getEnviromentVariablesObject(compose.env, compose.project.env), + }), + }, + }, + ); - writeStream.write("Docker Compose Deployed: ✅"); - } catch (error) { - writeStream.write(`Error ❌ ${(error as Error).message}`); - throw error; - } finally { - writeStream.end(); - } + writeStream.write("Docker Compose Deployed: ✅"); + } catch (error) { + writeStream.write(`Error ❌ ${(error as Error).message}`); + throw error; + } finally { + writeStream.end(); + } }; export const getBuildComposeCommand = async ( - compose: ComposeNested, - logPath: string + compose: ComposeNested, + logPath: string, ) => { - const { COMPOSE_PATH } = paths(true); - const { sourceType, appName, mounts, composeType, domains, composePath } = - compose; - const command = createCommand(compose); - const envCommand = getCreateEnvFileCommand(compose); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - const exportEnvCommand = getExportEnvCommand(compose); + const { COMPOSE_PATH } = paths(true); + const { sourceType, appName, mounts, composeType, domains, composePath } = + compose; + const command = createCommand(compose); + const envCommand = getCreateEnvFileCommand(compose); + const projectPath = join(COMPOSE_PATH, compose.appName, "code"); + const exportEnvCommand = getExportEnvCommand(compose); - const newCompose = await writeDomainsToComposeRemote( - compose, - domains, - logPath - ); - const logContent = ` + const newCompose = await writeDomainsToComposeRemote( + compose, + domains, + logPath, + ); + const logContent = ` App Name: ${appName} Build Compose 🐳 Detected: ${mounts.length} mounts 📂 @@ -107,17 +107,17 @@ Command: docker ${command} Source Type: docker ${sourceType} ✅ Compose Type: ${composeType} ✅`; - const logBox = boxen(logContent, { - padding: { - left: 1, - right: 1, - bottom: 1, - }, - width: 80, - borderStyle: "double", - }); + const logBox = boxen(logContent, { + padding: { + left: 1, + right: 1, + bottom: 1, + }, + width: 80, + borderStyle: "double", + }); - const bashCommand = ` + const bashCommand = ` set -e { echo "${logBox}" >> "${logPath}" @@ -139,102 +139,105 @@ Compose Type: ${composeType} ✅`; } `; - return await execAsyncRemote(compose.serverId, bashCommand); + return await execAsyncRemote(compose.serverId, bashCommand); }; const sanitizeCommand = (command: string) => { - const sanitizedCommand = command.trim(); + const sanitizedCommand = command.trim(); - const parts = sanitizedCommand.split(/\s+/); + const parts = sanitizedCommand.split(/\s+/); - const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); + const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); - return restCommand.join(" "); + return restCommand.join(" "); }; export const createCommand = (compose: ComposeNested) => { - const { composeType, appName, sourceType } = compose; + const { composeType, appName, sourceType } = compose; - if (compose.command) { - return `${sanitizeCommand(compose.command)}`; - } + if (compose.command) { + return `${sanitizeCommand(compose.command)}`; + } - const path = - sourceType === "raw" ? "docker-compose.yml" : compose.composePath; - let command = ""; + const path = + sourceType === "raw" ? "docker-compose.yml" : compose.composePath; + let command = ""; - if (composeType === "stack") { - command = `stack deploy -c ${path} ${appName} --prune`; - } + if (composeType === "stack") { + command = `stack deploy -c ${path} ${appName} --prune`; + } - return command; + return command; }; const createEnvFile = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(); - const { env, composePath, appName } = compose; - const composeFilePath = - join(COMPOSE_PATH, appName, "code", composePath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); + const { COMPOSE_PATH } = paths(); + const { env, composePath, appName } = compose; + const composeFilePath = + join(COMPOSE_PATH, appName, "code", composePath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = env || ""; - if (!envContent.includes("DOCKER_CONFIG")) { - envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; - } + const envFilePath = join(dirname(composeFilePath), ".env"); + let envContent = env || ""; + if (!envContent.includes("DOCKER_CONFIG")) { + envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; + } - if (compose.randomize) { - envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; - } + if (compose.randomize) { + envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; + } - const envFileContent = prepareEnvironmentVariables( - envContent, - compose.project.env - ).join("\n"); + const envFileContent = prepareEnvironmentVariables( + envContent, + compose.project.env, + ).join("\n"); - if (!existsSync(dirname(envFilePath))) { - mkdirSync(dirname(envFilePath), { recursive: true }); - } - writeFileSync(envFilePath, envFileContent); + if (!existsSync(dirname(envFilePath))) { + mkdirSync(dirname(envFilePath), { recursive: true }); + } + writeFileSync(envFilePath, envFileContent); }; export const getCreateEnvFileCommand = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(true); - const { env, composePath, appName } = compose; - const composeFilePath = - join(COMPOSE_PATH, appName, "code", composePath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); + const { COMPOSE_PATH } = paths(true); + const { env, composePath, appName } = compose; + const composeFilePath = + join(COMPOSE_PATH, appName, "code", composePath) || + join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - const envFilePath = join(dirname(composeFilePath), ".env"); + const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = env || ""; - if (!envContent.includes("DOCKER_CONFIG")) { - envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; - } + let envContent = env || ""; + if (!envContent.includes("DOCKER_CONFIG")) { + envContent += "\nDOCKER_CONFIG=/root/.docker/config.json"; + } - if (compose.randomize) { - envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; - } + if (compose.randomize) { + envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; + } - const envFileContent = prepareEnvironmentVariables( - envContent, - compose.project.env - ).join("\n"); + const envFileContent = prepareEnvironmentVariables( + envContent, + compose.project.env, + ).join("\n"); - const encodedContent = encodeBase64(envFileContent); - return ` + const encodedContent = encodeBase64(envFileContent); + return ` touch ${envFilePath}; echo "${encodedContent}" | base64 -d > "${envFilePath}"; `; }; const getExportEnvCommand = (compose: ComposeNested) => { - if (compose.composeType !== "stack") return ""; - - const envVars = getEnviromentVariablesObject(compose.env, compose.project.env); - const exports = Object.entries(envVars) - .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) - .join("\n"); - - return exports ? `\n# Export environment variables\n${exports}\n` : ""; + if (compose.composeType !== "stack") return ""; + + const envVars = getEnviromentVariablesObject( + compose.env, + compose.project.env, + ); + const exports = Object.entries(envVars) + .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) + .join("\n"); + + return exports ? `\n# Export environment variables\n${exports}\n` : ""; }; diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 96b2e08b..062e0722 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -15,529 +15,529 @@ import { spawnAsync } from "../process/spawnAsync"; import { getRemoteDocker } from "../servers/remote-docker"; interface RegistryAuth { - username: string; - password: string; - registryUrl: string; + username: string; + password: string; + registryUrl: string; } export const pullImage = async ( - dockerImage: string, - onData?: (data: any) => void, - authConfig?: Partial + dockerImage: string, + onData?: (data: any) => void, + authConfig?: Partial, ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } - 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; - } + 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; + } }; export const pullRemoteImage = async ( - dockerImage: string, - serverId: string, - onData?: (data: any) => void, - authConfig?: Partial + dockerImage: string, + serverId: string, + onData?: (data: any) => void, + authConfig?: Partial, ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } - const remoteDocker = await getRemoteDocker(serverId); + const remoteDocker = await getRemoteDocker(serverId); - await new Promise((resolve, reject) => { - remoteDocker.pull( - dockerImage, - { authconfig: authConfig }, - (err, stream) => { - if (err) { - reject(err); - return; - } + await new Promise((resolve, reject) => { + remoteDocker.pull( + dockerImage, + { authconfig: authConfig }, + (err, stream) => { + if (err) { + reject(err); + return; + } - remoteDocker.modem.followProgress( - stream as Readable, - (err: Error | null, res) => { - if (!err) { - resolve(res); - } - if (err) { - reject(err); - } - }, - (event) => { - onData?.(event); - } - ); - } - ); - }); - } catch (error) { - throw error; - } + remoteDocker.modem.followProgress( + stream as Readable, + (err: Error | null, res) => { + if (!err) { + resolve(res); + } + if (err) { + reject(err); + } + }, + (event) => { + onData?.(event); + }, + ); + }, + ); + }); + } catch (error) { + throw error; + } }; export const containerExists = async (containerName: string) => { - const container = docker.getContainer(containerName); - try { - await container.inspect(); - return true; - } catch (error) { - return false; - } + const container = docker.getContainer(containerName); + try { + await container.inspect(); + return true; + } catch (error) { + return false; + } }; export const stopService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsync(`docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const stopServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const getContainerByName = (name: string): Promise => { - const opts = { - limit: 1, - filters: { - name: [name], - }, - }; - return new Promise((resolve, reject) => { - docker.listContainers(opts, (err, containers) => { - if (err) { - reject(err); - } else if (containers?.length === 0) { - reject(new Error(`No container found with name: ${name}`)); - } else if (containers && containers?.length > 0 && containers[0]) { - resolve(containers[0]); - } - }); - }); + const opts = { + limit: 1, + filters: { + name: [name], + }, + }; + return new Promise((resolve, reject) => { + docker.listContainers(opts, (err, containers) => { + if (err) { + reject(err); + } else if (containers?.length === 0) { + reject(new Error(`No container found with name: ${name}`)); + } else if (containers && containers?.length > 0 && containers[0]) { + resolve(containers[0]); + } + }); + }); }; export const cleanUpUnusedImages = async (serverId?: string) => { - try { - const command = "docker image prune --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - console.error(error); - throw error; - } + try { + const command = "docker image prune --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanStoppedContainers = async (serverId?: string) => { - try { - const command = "docker container prune --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - console.error(error); - throw error; - } + try { + const command = "docker container prune --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpUnusedVolumes = async (serverId?: string) => { - try { - const command = "docker volume prune --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - console.error(error); - throw error; - } + try { + const command = "docker volume prune --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpInactiveContainers = async () => { - try { - const containers = await docker.listContainers({ all: true }); - const inactiveContainers = containers.filter( - (container) => container.State !== "running" - ); + try { + const containers = await docker.listContainers({ all: true }); + const inactiveContainers = containers.filter( + (container) => container.State !== "running", + ); - for (const container of inactiveContainers) { - await docker.getContainer(container.Id).remove({ force: true }); - console.log(`Cleaning up inactive container: ${container.Id}`); - } - } catch (error) { - console.error("Error cleaning up inactive containers:", error); - throw error; - } + for (const container of inactiveContainers) { + await docker.getContainer(container.Id).remove({ force: true }); + console.log(`Cleaning up inactive container: ${container.Id}`); + } + } catch (error) { + console.error("Error cleaning up inactive containers:", error); + throw error; + } }; export const cleanUpDockerBuilder = async (serverId?: string) => { - const command = "docker builder prune --all --force"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } + const command = "docker builder prune --all --force"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } }; export const cleanUpSystemPrune = async (serverId?: string) => { - const command = "docker system prune --all --force --volumes"; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } + const command = "docker system prune --all --force --volumes"; + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } }; export const startService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsync(`docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const startServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const removeService = async ( - appName: string, - serverId?: string | null, - deleteVolumes = false + appName: string, + serverId?: string | null, + deleteVolumes = false, ) => { - try { - const command = `docker service rm ${appName}`; + try { + const command = `docker service rm ${appName}`; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - return error; - } + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + return error; + } }; export const prepareEnvironmentVariables = ( - serviceEnv: string | null, - projectEnv?: string | null + serviceEnv: string | null, + projectEnv?: string | null, ) => { - const projectVars = parse(projectEnv ?? ""); - const serviceVars = parse(serviceEnv ?? ""); + 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}`; - }); + 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; + return resolvedVars; }; export const getEnviromentVariablesObject = ( - input: string | null, - projectEnv?: string | null + input: string | null, + projectEnv?: string | null, ) => { - const envs = prepareEnvironmentVariables(input, projectEnv); + const envs = prepareEnvironmentVariables(input, projectEnv); - const jsonObject: Record = {}; + const jsonObject: Record = {}; - for (const pair of envs) { - const [key, value] = pair.split("="); - if (key && value) { - jsonObject[key] = value; - } - } + for (const pair of envs) { + const [key, value] = pair.split("="); + if (key && value) { + jsonObject[key] = value; + } + } - return jsonObject; + return jsonObject; }; export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } + if (!mounts || mounts.length === 0) { + return []; + } - return mounts - .filter((mount) => mount.type === "volume") - .map((mount) => ({ - Type: "volume" as const, - Source: mount.volumeName || "", - Target: mount.mountPath, - })); + return mounts + .filter((mount) => mount.type === "volume") + .map((mount) => ({ + Type: "volume" as const, + Source: mount.volumeName || "", + Target: mount.mountPath, + })); }; type Resources = { - memoryLimit: string | null; - memoryReservation: string | null; - cpuLimit: string | null; - cpuReservation: string | null; + memoryLimit: string | null; + memoryReservation: string | null; + cpuLimit: string | null; + cpuReservation: string | null; }; export const calculateResources = ({ - memoryLimit, - memoryReservation, - cpuLimit, - cpuReservation, + memoryLimit, + memoryReservation, + cpuLimit, + cpuReservation, }: Resources): ResourceRequirements => { - return { - Limits: { - MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined, - NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined, - }, - Reservations: { - MemoryBytes: memoryReservation - ? Number.parseInt(memoryReservation) - : undefined, - NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined, - }, - }; + return { + Limits: { + MemoryBytes: memoryLimit ? Number.parseInt(memoryLimit) : undefined, + NanoCPUs: cpuLimit ? Number.parseInt(cpuLimit) : undefined, + }, + Reservations: { + MemoryBytes: memoryReservation + ? Number.parseInt(memoryReservation) + : undefined, + NanoCPUs: cpuReservation ? Number.parseInt(cpuReservation) : undefined, + }, + }; }; export const generateConfigContainer = (application: ApplicationNested) => { - const { - healthCheckSwarm, - restartPolicySwarm, - placementSwarm, - updateConfigSwarm, - rollbackConfigSwarm, - modeSwarm, - labelsSwarm, - replicas, - mounts, - networkSwarm, - } = application; + const { + healthCheckSwarm, + restartPolicySwarm, + placementSwarm, + updateConfigSwarm, + rollbackConfigSwarm, + modeSwarm, + labelsSwarm, + replicas, + mounts, + networkSwarm, + } = application; - const haveMounts = mounts.length > 0; + const haveMounts = mounts.length > 0; - return { - ...(healthCheckSwarm && { - HealthCheck: healthCheckSwarm, - }), - ...(restartPolicySwarm - ? { - RestartPolicy: restartPolicySwarm, - } - : {}), - ...(placementSwarm - ? { - Placement: placementSwarm, - } - : { - // if app have mounts keep manager as constraint - Placement: { - Constraints: haveMounts ? ["node.role==manager"] : [], - }, - }), - ...(labelsSwarm && { - Labels: labelsSwarm, - }), - ...(modeSwarm - ? { - Mode: modeSwarm, - } - : { - // use replicas value if no modeSwarm provided - Mode: { - Replicated: { - Replicas: replicas, - }, - }, - }), - ...(rollbackConfigSwarm && { - RollbackConfig: rollbackConfigSwarm, - }), - ...(updateConfigSwarm - ? { UpdateConfig: updateConfigSwarm } - : { - // default config if no updateConfigSwarm provided - UpdateConfig: { - Parallelism: 1, - Order: "start-first", - }, - }), - ...(networkSwarm - ? { - Networks: networkSwarm, - } - : { - Networks: [{ Target: "dokploy-network" }], - }), - }; + return { + ...(healthCheckSwarm && { + HealthCheck: healthCheckSwarm, + }), + ...(restartPolicySwarm + ? { + RestartPolicy: restartPolicySwarm, + } + : {}), + ...(placementSwarm + ? { + Placement: placementSwarm, + } + : { + // if app have mounts keep manager as constraint + Placement: { + Constraints: haveMounts ? ["node.role==manager"] : [], + }, + }), + ...(labelsSwarm && { + Labels: labelsSwarm, + }), + ...(modeSwarm + ? { + Mode: modeSwarm, + } + : { + // use replicas value if no modeSwarm provided + Mode: { + Replicated: { + Replicas: replicas, + }, + }, + }), + ...(rollbackConfigSwarm && { + RollbackConfig: rollbackConfigSwarm, + }), + ...(updateConfigSwarm + ? { UpdateConfig: updateConfigSwarm } + : { + // default config if no updateConfigSwarm provided + UpdateConfig: { + Parallelism: 1, + Order: "start-first", + }, + }), + ...(networkSwarm + ? { + Networks: networkSwarm, + } + : { + Networks: [{ Target: "dokploy-network" }], + }), + }; }; export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } + if (!mounts || mounts.length === 0) { + return []; + } - return mounts - .filter((mount) => mount.type === "bind") - .map((mount) => ({ - Type: "bind" as const, - Source: mount.hostPath || "", - Target: mount.mountPath, - })); + return mounts + .filter((mount) => mount.type === "bind") + .map((mount) => ({ + Type: "bind" as const, + Source: mount.hostPath || "", + Target: mount.mountPath, + })); }; export const generateFileMounts = ( - appName: string, - service: - | ApplicationNested - | MongoNested - | MariadbNested - | MysqlNested - | PostgresNested - | RedisNested + appName: string, + service: + | ApplicationNested + | MongoNested + | MariadbNested + | MysqlNested + | PostgresNested + | RedisNested, ) => { - const { mounts } = service; - const { APPLICATIONS_PATH } = paths(!!service.serverId); - if (!mounts || mounts.length === 0) { - return []; - } + const { mounts } = service; + const { APPLICATIONS_PATH } = paths(!!service.serverId); + if (!mounts || mounts.length === 0) { + return []; + } - return mounts - .filter((mount) => mount.type === "file") - .map((mount) => { - const fileName = mount.filePath; - const absoluteBasePath = path.resolve(APPLICATIONS_PATH); - const directory = path.join(absoluteBasePath, appName, "files"); - const sourcePath = path.join(directory, fileName || ""); - return { - Type: "bind" as const, - Source: sourcePath, - Target: mount.mountPath, - }; - }); + return mounts + .filter((mount) => mount.type === "file") + .map((mount) => { + const fileName = mount.filePath; + const absoluteBasePath = path.resolve(APPLICATIONS_PATH); + const directory = path.join(absoluteBasePath, appName, "files"); + const sourcePath = path.join(directory, fileName || ""); + return { + Type: "bind" as const, + Source: sourcePath, + Target: mount.mountPath, + }; + }); }; export const createFile = async ( - outputPath: string, - filePath: string, - content: string + outputPath: string, + filePath: string, + content: string, ) => { - try { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - fs.mkdirSync(fullPath, { recursive: true }); - return; - } + try { + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + fs.mkdirSync(fullPath, { recursive: true }); + return; + } - const directory = path.dirname(fullPath); - fs.mkdirSync(directory, { recursive: true }); - fs.writeFileSync(fullPath, content || ""); - } catch (error) { - throw error; - } + const directory = path.dirname(fullPath); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(fullPath, content || ""); + } catch (error) { + throw error; + } }; export const encodeBase64 = (content: string) => - Buffer.from(content, "utf-8").toString("base64"); + Buffer.from(content, "utf-8").toString("base64"); export const getCreateFileCommand = ( - outputPath: string, - filePath: string, - content: string + outputPath: string, + filePath: string, + content: string, ) => { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - return `mkdir -p ${fullPath};`; - } + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + return `mkdir -p ${fullPath};`; + } - const directory = path.dirname(fullPath); - const encodedContent = encodeBase64(content); - return ` + const directory = path.dirname(fullPath); + const encodedContent = encodeBase64(content); + return ` mkdir -p ${directory}; echo "${encodedContent}" | base64 -d > "${fullPath}"; `; }; export const getServiceContainer = async (appName: string) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } - const container = containers[0]; + const container = containers[0]; - return container; - } catch (error) { - throw error; - } + return container; + } catch (error) { + throw error; + } }; export const getRemoteServiceContainer = async ( - serverId: string, - appName: string + serverId: string, + appName: string, ) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; - const remoteDocker = await getRemoteDocker(serverId); - const containers = await remoteDocker.listContainers({ - filters: JSON.stringify(filter), - }); + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + const remoteDocker = await getRemoteDocker(serverId); + const containers = await remoteDocker.listContainers({ + filters: JSON.stringify(filter), + }); - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } - const container = containers[0]; + const container = containers[0]; - return container; - } catch (error) { - throw error; - } + return container; + } catch (error) { + throw error; + } };