Skip to content

Commit e316bea

Browse files
authored
Merge pull request #3104 from Dokploy/fix/error-parsing
feat: enhance error handling in deployment processes
2 parents e136934 + 8aff1e7 commit e316bea

File tree

6 files changed

+205
-19
lines changed

6 files changed

+205
-19
lines changed

packages/server/src/services/application.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
1414
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
1515
import {
16+
ExecError,
1617
execAsync,
1718
execAsyncRemote,
1819
} from "@dokploy/server/utils/process/execAsync";
@@ -28,6 +29,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
2829
import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
2930
import { TRPCError } from "@trpc/server";
3031
import { eq } from "drizzle-orm";
32+
import { encodeBase64 } from "../utils/docker/utils";
3133
import { getDokployUrl } from "./admin";
3234
import {
3335
createDeployment,
@@ -228,7 +230,16 @@ export const deployApplication = async ({
228230
environmentName: application.environment.name,
229231
});
230232
} catch (error) {
231-
const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`;
233+
let command = "";
234+
235+
// Only log details for non-ExecError errors
236+
if (!(error instanceof ExecError)) {
237+
const message = error instanceof Error ? error.message : String(error);
238+
const encodedMessage = encodeBase64(message);
239+
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
240+
}
241+
242+
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
232243
if (application.serverId) {
233244
await execAsyncRemote(application.serverId, command);
234245
} else {
@@ -317,6 +328,21 @@ export const rebuildApplication = async ({
317328
environmentName: application.environment.name,
318329
});
319330
} catch (error) {
331+
let command = "";
332+
333+
// Only log details for non-ExecError errors
334+
if (!(error instanceof ExecError)) {
335+
const message = error instanceof Error ? error.message : String(error);
336+
const encodedMessage = encodeBase64(message);
337+
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
338+
}
339+
340+
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
341+
if (application.serverId) {
342+
await execAsyncRemote(application.serverId, command);
343+
} else {
344+
await execAsync(command);
345+
}
320346
await updateDeploymentStatus(deployment.deploymentId, "error");
321347
await updateApplicationStatus(applicationId, "error");
322348
throw error;

packages/server/src/services/compose.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types";
1818
import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error";
1919
import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success";
2020
import {
21+
ExecError,
2122
execAsync,
2223
execAsyncRemote,
2324
} from "@dokploy/server/utils/process/execAsync";
@@ -32,6 +33,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab";
3233
import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw";
3334
import { TRPCError } from "@trpc/server";
3435
import { eq } from "drizzle-orm";
36+
import { encodeBase64 } from "../utils/docker/utils";
3537
import { getDokployUrl } from "./admin";
3638
import {
3739
createDeploymentCompose,
@@ -270,6 +272,21 @@ export const deployCompose = async ({
270272
environmentName: compose.environment.name,
271273
});
272274
} catch (error) {
275+
let command = "";
276+
277+
// Only log details for non-ExecError errors
278+
if (!(error instanceof ExecError)) {
279+
const message = error instanceof Error ? error.message : String(error);
280+
const encodedMessage = encodeBase64(message);
281+
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
282+
}
283+
284+
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
285+
if (compose.serverId) {
286+
await execAsyncRemote(compose.serverId, command);
287+
} else {
288+
await execAsync(command);
289+
}
273290
await updateDeploymentStatus(deployment.deploymentId, "error");
274291
await updateCompose(composeId, {
275292
composeStatus: "error",
@@ -342,6 +359,21 @@ export const rebuildCompose = async ({
342359
composeStatus: "done",
343360
});
344361
} catch (error) {
362+
let command = "";
363+
364+
// Only log details for non-ExecError errors
365+
if (!(error instanceof ExecError)) {
366+
const message = error instanceof Error ? error.message : String(error);
367+
const encodedMessage = encodeBase64(message);
368+
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
369+
}
370+
371+
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
372+
if (compose.serverId) {
373+
await execAsyncRemote(compose.serverId, command);
374+
} else {
375+
await execAsync(command);
376+
}
345377
await updateDeploymentStatus(deployment.deploymentId, "error");
346378
await updateCompose(composeId, {
347379
composeStatus: "error",

packages/server/src/services/settings.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
6060
try {
6161
currentDigest = await getServiceImageDigest();
6262
} catch (error) {
63-
console.error(error);
64-
// Docker service might not exist locally
65-
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
66-
// https://docs.dokploy.com/docs/core/manual-installation
63+
// 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.
6764
return DEFAULT_UPDATE_DATA;
6865
}
6966

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export interface ExecErrorDetails {
2+
command: string;
3+
stdout?: string;
4+
stderr?: string;
5+
exitCode?: number;
6+
originalError?: Error;
7+
serverId?: string | null;
8+
}
9+
10+
export class ExecError extends Error {
11+
public readonly command: string;
12+
public readonly stdout?: string;
13+
public readonly stderr?: string;
14+
public readonly exitCode?: number;
15+
public readonly originalError?: Error;
16+
public readonly serverId?: string | null;
17+
18+
constructor(message: string, details: ExecErrorDetails) {
19+
super(message);
20+
this.name = "ExecError";
21+
this.command = details.command;
22+
this.stdout = details.stdout;
23+
this.stderr = details.stderr;
24+
this.exitCode = details.exitCode;
25+
this.originalError = details.originalError;
26+
this.serverId = details.serverId;
27+
28+
// Maintains proper stack trace for where our error was thrown (only available on V8)
29+
if (Error.captureStackTrace) {
30+
Error.captureStackTrace(this, ExecError);
31+
}
32+
}
33+
34+
/**
35+
* Get a formatted error message with all details
36+
*/
37+
getDetailedMessage(): string {
38+
const parts = [
39+
`Command: ${this.command}`,
40+
this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null,
41+
this.serverId ? `Server ID: ${this.serverId}` : "Location: Local",
42+
this.stderr ? `Stderr: ${this.stderr}` : null,
43+
this.stdout ? `Stdout: ${this.stdout}` : null,
44+
].filter(Boolean);
45+
46+
return `${this.message}\n${parts.join("\n")}`;
47+
}
48+
49+
/**
50+
* Check if this error is from a remote execution
51+
*/
52+
isRemote(): boolean {
53+
return !!this.serverId;
54+
}
55+
}

packages/server/src/utils/process/execAsync.ts

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process";
22
import util from "node:util";
33
import { findServerById } from "@dokploy/server/services/server";
44
import { Client } from "ssh2";
5+
import { ExecError } from "./ExecError";
56

6-
export const execAsync = util.promisify(exec);
7+
// Re-export ExecError for easier imports
8+
export { ExecError } from "./ExecError";
9+
10+
const execAsyncBase = util.promisify(exec);
11+
12+
export const execAsync = async (
13+
command: string,
14+
options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string },
15+
): Promise<{ stdout: string; stderr: string }> => {
16+
try {
17+
const result = await execAsyncBase(command, options);
18+
return {
19+
stdout: result.stdout.toString(),
20+
stderr: result.stderr.toString(),
21+
};
22+
} catch (error) {
23+
if (error instanceof Error) {
24+
// @ts-ignore - exec error has these properties
25+
const exitCode = error.code;
26+
// @ts-ignore
27+
const stdout = error.stdout?.toString() || "";
28+
// @ts-ignore
29+
const stderr = error.stderr?.toString() || "";
30+
31+
throw new ExecError(`Command execution failed: ${error.message}`, {
32+
command,
33+
stdout,
34+
stderr,
35+
exitCode,
36+
originalError: error,
37+
});
38+
}
39+
throw error;
40+
}
41+
};
742

843
interface ExecOptions {
944
cwd?: string;
@@ -21,7 +56,16 @@ export const execAsyncStream = (
2156

2257
const childProcess = exec(command, options, (error) => {
2358
if (error) {
24-
reject(error);
59+
reject(
60+
new ExecError(`Command execution failed: ${error.message}`, {
61+
command,
62+
stdout: stdoutComplete,
63+
stderr: stderrComplete,
64+
// @ts-ignore
65+
exitCode: error.code,
66+
originalError: error,
67+
}),
68+
);
2569
return;
2670
}
2771
resolve({ stdout: stdoutComplete, stderr: stderrComplete });
@@ -45,7 +89,14 @@ export const execAsyncStream = (
4589

4690
childProcess.on("error", (error) => {
4791
console.log(error);
48-
reject(error);
92+
reject(
93+
new ExecError(`Command execution error: ${error.message}`, {
94+
command,
95+
stdout: stdoutComplete,
96+
stderr: stderrComplete,
97+
originalError: error,
98+
}),
99+
);
49100
});
50101
});
51102
};
@@ -108,15 +159,33 @@ export const execAsyncRemote = async (
108159
conn.exec(command, (err, stream) => {
109160
if (err) {
110161
onData?.(err.message);
111-
throw err;
162+
reject(
163+
new ExecError(`Remote command execution failed: ${err.message}`, {
164+
command,
165+
serverId,
166+
originalError: err,
167+
}),
168+
);
169+
return;
112170
}
113171
stream
114172
.on("close", (code: number, _signal: string) => {
115173
conn.end();
116174
if (code === 0) {
117175
resolve({ stdout, stderr });
118176
} else {
119-
reject(new Error(`Error occurred ❌: ${stderr}`));
177+
reject(
178+
new ExecError(
179+
`Remote command failed with exit code ${code}`,
180+
{
181+
command,
182+
stdout,
183+
stderr,
184+
exitCode: code,
185+
serverId,
186+
},
187+
),
188+
);
120189
}
121190
})
122191
.on("data", (data: string) => {
@@ -132,17 +201,25 @@ export const execAsyncRemote = async (
132201
.on("error", (err) => {
133202
conn.end();
134203
if (err.level === "client-authentication") {
135-
onData?.(
136-
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
137-
);
204+
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
205+
onData?.(errorMsg);
138206
reject(
139-
new Error(
140-
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
141-
),
207+
new ExecError(errorMsg, {
208+
command,
209+
serverId,
210+
originalError: err,
211+
}),
142212
);
143213
} else {
144-
onData?.(`SSH connection error: ${err.message}`);
145-
reject(new Error(`SSH connection error: ${err.message}`));
214+
const errorMsg = `SSH connection error: ${err.message}`;
215+
onData?.(errorMsg);
216+
reject(
217+
new ExecError(errorMsg, {
218+
command,
219+
serverId,
220+
originalError: err,
221+
}),
222+
);
146223
}
147224
})
148225
.connect({

packages/server/src/utils/providers/github.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({
159159
const octokit = authGithub(githubProvider);
160160
const token = await getGithubToken(octokit);
161161
const repoclone = `github.com/${owner}/${repository}.git`;
162-
// await recreateDirectory(outputPath);
163162
command += `rm -rf ${outputPath};`;
164163
command += `mkdir -p ${outputPath};`;
165164
const cloneUrl = `https://oauth2:${token}@${repoclone}`;

0 commit comments

Comments
 (0)