diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 31f015c0c..6ad90a93b 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -13,6 +13,7 @@ import { import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { + ExecError, execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; @@ -28,6 +29,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeployment, @@ -228,7 +230,16 @@ export const deployApplication = async ({ environmentName: application.environment.name, }); } catch (error) { - const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`; + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; if (application.serverId) { await execAsyncRemote(application.serverId, command); } else { @@ -317,6 +328,21 @@ export const rebuildApplication = async ({ environmentName: application.environment.name, }); } catch (error) { + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (application.serverId) { + await execAsyncRemote(application.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); throw error; diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 1d4a7e5c9..89a12a156 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -18,6 +18,7 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { + ExecError, execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; @@ -32,6 +33,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeploymentCompose, @@ -270,6 +272,21 @@ export const deployCompose = async ({ environmentName: compose.environment.name, }); } catch (error) { + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateCompose(composeId, { composeStatus: "error", @@ -342,6 +359,21 @@ export const rebuildCompose = async ({ composeStatus: "done", }); } catch (error) { + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateCompose(composeId, { composeStatus: "error", diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 996aec352..23d11b09b 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise => { try { currentDigest = await getServiceImageDigest(); } catch (error) { - console.error(error); - // Docker service might not exist locally - // You can run the # Installation command for docker service create mentioned in the below docs to test it locally: - // https://docs.dokploy.com/docs/core/manual-installation + // TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version. return DEFAULT_UPDATE_DATA; } diff --git a/packages/server/src/utils/process/ExecError.ts b/packages/server/src/utils/process/ExecError.ts new file mode 100644 index 000000000..773968b5c --- /dev/null +++ b/packages/server/src/utils/process/ExecError.ts @@ -0,0 +1,55 @@ +export interface ExecErrorDetails { + command: string; + stdout?: string; + stderr?: string; + exitCode?: number; + originalError?: Error; + serverId?: string | null; +} + +export class ExecError extends Error { + public readonly command: string; + public readonly stdout?: string; + public readonly stderr?: string; + public readonly exitCode?: number; + public readonly originalError?: Error; + public readonly serverId?: string | null; + + constructor(message: string, details: ExecErrorDetails) { + super(message); + this.name = "ExecError"; + this.command = details.command; + this.stdout = details.stdout; + this.stderr = details.stderr; + this.exitCode = details.exitCode; + this.originalError = details.originalError; + this.serverId = details.serverId; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ExecError); + } + } + + /** + * Get a formatted error message with all details + */ + getDetailedMessage(): string { + const parts = [ + `Command: ${this.command}`, + this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null, + this.serverId ? `Server ID: ${this.serverId}` : "Location: Local", + this.stderr ? `Stderr: ${this.stderr}` : null, + this.stdout ? `Stdout: ${this.stdout}` : null, + ].filter(Boolean); + + return `${this.message}\n${parts.join("\n")}`; + } + + /** + * Check if this error is from a remote execution + */ + isRemote(): boolean { + return !!this.serverId; + } +} diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index 13b06c6c4..cd0249000 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; +import { ExecError } from "./ExecError"; -export const execAsync = util.promisify(exec); +// Re-export ExecError for easier imports +export { ExecError } from "./ExecError"; + +const execAsyncBase = util.promisify(exec); + +export const execAsync = async ( + command: string, + options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string }, +): Promise<{ stdout: string; stderr: string }> => { + try { + const result = await execAsyncBase(command, options); + return { + stdout: result.stdout.toString(), + stderr: result.stderr.toString(), + }; + } catch (error) { + if (error instanceof Error) { + // @ts-ignore - exec error has these properties + const exitCode = error.code; + // @ts-ignore + const stdout = error.stdout?.toString() || ""; + // @ts-ignore + const stderr = error.stderr?.toString() || ""; + + throw new ExecError(`Command execution failed: ${error.message}`, { + command, + stdout, + stderr, + exitCode, + originalError: error, + }); + } + throw error; + } +}; interface ExecOptions { cwd?: string; @@ -21,7 +56,16 @@ export const execAsyncStream = ( const childProcess = exec(command, options, (error) => { if (error) { - reject(error); + reject( + new ExecError(`Command execution failed: ${error.message}`, { + command, + stdout: stdoutComplete, + stderr: stderrComplete, + // @ts-ignore + exitCode: error.code, + originalError: error, + }), + ); return; } resolve({ stdout: stdoutComplete, stderr: stderrComplete }); @@ -45,7 +89,14 @@ export const execAsyncStream = ( childProcess.on("error", (error) => { console.log(error); - reject(error); + reject( + new ExecError(`Command execution error: ${error.message}`, { + command, + stdout: stdoutComplete, + stderr: stderrComplete, + originalError: error, + }), + ); }); }); }; @@ -108,7 +159,14 @@ export const execAsyncRemote = async ( conn.exec(command, (err, stream) => { if (err) { onData?.(err.message); - throw err; + reject( + new ExecError(`Remote command execution failed: ${err.message}`, { + command, + serverId, + originalError: err, + }), + ); + return; } stream .on("close", (code: number, _signal: string) => { @@ -116,7 +174,18 @@ export const execAsyncRemote = async ( if (code === 0) { resolve({ stdout, stderr }); } else { - reject(new Error(`Error occurred ❌: ${stderr}`)); + reject( + new ExecError( + `Remote command failed with exit code ${code}`, + { + command, + stdout, + stderr, + exitCode: code, + serverId, + }, + ), + ); } }) .on("data", (data: string) => { @@ -132,17 +201,25 @@ export const execAsyncRemote = async ( .on("error", (err) => { conn.end(); if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ); + const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`; + onData?.(errorMsg); reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), + new ExecError(errorMsg, { + command, + serverId, + originalError: err, + }), ); } else { - onData?.(`SSH connection error: ${err.message}`); - reject(new Error(`SSH connection error: ${err.message}`)); + const errorMsg = `SSH connection error: ${err.message}`; + onData?.(errorMsg); + reject( + new ExecError(errorMsg, { + command, + serverId, + originalError: err, + }), + ); } }) .connect({ diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index c0f1b651a..5b7763df7 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({ const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; - // await recreateDirectory(outputPath); command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; const cloneUrl = `https://oauth2:${token}@${repoclone}`;