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 { quote } from "shell-quote"; import { writeDomainsToCompose } from "../docker/domain"; import { encodeBase64, getEnviromentVariablesObject, prepareEnvironmentVariables, } from "../docker/utils"; export type ComposeNested = InferResultType< "compose", { environment: { with: { project: true } }; mounts: true; domains: true } >; export const getBuildComposeCommand = async (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(!!compose.serverId); const { sourceType, appName, mounts, composeType, domains } = compose; const command = createCommand(compose); const envCommand = getCreateEnvFileCommand(compose); const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const exportEnvCommand = getExportEnvCommand(compose); const newCompose = await writeDomainsToCompose(compose, domains); 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", }); const bashCommand = ` set -e { echo "${logBox}"; ${newCompose} ${envCommand} cd "${projectPath}"; ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""} env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } ${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""} echo "Docker Compose Deployed: ✅"; } || { echo "Error: ❌ Script execution failed"; exit 1 } `; return bashCommand; }; const sanitizeCommand = (command: string) => { const sanitizedCommand = command.trim(); const parts = sanitizedCommand.split(/\s+/); const restCommand = parts.map((arg) => arg.replace(/^"(.*)"$/, "$1")); return restCommand.join(" "); }; export const createCommand = (compose: ComposeNested) => { const { composeType, appName, sourceType } = compose; if (compose.command) { return `${sanitizeCommand(compose.command)}`; } const path = sourceType === "raw" ? "docker-compose.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 --with-registry-auth`; } return command; }; export const getCreateEnvFileCommand = (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(!!compose.serverId); 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 = `APP_NAME=${appName}\n`; envContent += env || ""; if (!envContent.includes("DOCKER_CONFIG")) { envContent += "\nDOCKER_CONFIG=/root/.docker"; } if (compose.randomize) { envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; } const envFileContent = prepareEnvironmentVariables( envContent, compose.environment.project.env, compose.environment.env, ).join("\n"); 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.environment.project.env, compose.environment.env, ); const exports = Object.entries(envVars) .map(([key, value]) => `${key}=${quote([value])}`) .join(" "); return exports ? `${exports}` : ""; };