Merge pull request #5246 from Dokploy/fix/traefik-remote-config-sftp-write

fix: write remote traefik config via SFTP instead of exec+base64
This commit is contained in:
Mauricio Siu
2026-09-01 01:54:21 -06:00
committed by GitHub
2 changed files with 81 additions and 21 deletions
+77 -4
View File
@@ -4,6 +4,20 @@ import { findServerById } from "@dokploy/server/services/server";
import { Client } from "ssh2";
import { ExecError } from "./ExecError";
export class WriteFileRemoteError extends Error {
constructor(
message: string,
public readonly context: {
remotePath: string;
serverId: string;
originalError: Error;
},
) {
super(message);
this.name = "WriteFileRemoteError";
}
}
// Re-export ExecError for easier imports
export { ExecError } from "./ExecError";
@@ -21,11 +35,11 @@ export const execAsync = async (
};
} catch (error) {
if (error instanceof Error) {
// @ts-ignore - exec error has these properties
// @ts-expect-error - exec error has these properties
const exitCode = error.code;
// @ts-ignore
// @ts-expect-error
const stdout = error.stdout?.toString() || "";
// @ts-ignore
// @ts-expect-error
const stderr = error.stderr?.toString() || "";
throw new ExecError(`Command execution failed: ${error.message}`, {
@@ -61,7 +75,7 @@ export const execAsyncStream = (
command,
stdout: stdoutComplete,
stderr: stderrComplete,
// @ts-ignore
// @ts-expect-error
exitCode: error.code,
originalError: error,
}),
@@ -249,6 +263,65 @@ export const execAsyncRemote = async (
});
};
export const writeFileRemote = async (
serverId: string,
remotePath: string,
content: string,
): Promise<void> => {
const server = await findServerById(serverId);
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
return new Promise((resolve, reject) => {
const conn = new Client();
conn
.once("ready", () => {
conn.sftp((err, sftp) => {
if (err) {
conn.end();
reject(
new WriteFileRemoteError(`SFTP session failed: ${err.message}`, {
remotePath,
serverId,
originalError: err,
}),
);
return;
}
sftp.writeFile(remotePath, content, (writeErr) => {
conn.end();
if (writeErr) {
reject(
new WriteFileRemoteError(
`Failed to write remote file ${remotePath}: ${writeErr.message}`,
{ remotePath, serverId, originalError: writeErr },
),
);
return;
}
resolve();
});
});
})
.on("error", (err) => {
conn.end();
reject(
new WriteFileRemoteError(`SSH connection error: ${err.message}`, {
remotePath,
serverId,
originalError: err,
}),
);
})
.connect({
host: server.ipAddress,
port: server.port,
username: server.username,
privateKey: server.sshKey?.privateKey,
timeout: 99999,
});
});
};
export const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
@@ -5,8 +5,7 @@ import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain";
import { quote } from "shell-quote";
import { parse, stringify } from "yaml";
import { encodeBase64 } from "../docker/utils";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { execAsync, execAsyncRemote, writeFileRemote } from "../process/execAsync";
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
export const createTraefikConfig = (appName: string) => {
@@ -228,11 +227,7 @@ export const writeConfigRemote = async (
try {
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const encoded = encodeBase64(traefikConfig);
await execAsyncRemote(
serverId,
`echo "${encoded}" | base64 -d > ${quote([configPath])}`,
);
await writeFileRemote(serverId, configPath, traefikConfig);
} catch (e) {
console.error("Error saving the YAML config file:", e);
}
@@ -246,11 +241,7 @@ export const writeTraefikConfigInPath = async (
try {
const configPath = path.join(pathFile);
if (serverId) {
const encoded = encodeBase64(traefikConfig);
await execAsyncRemote(
serverId,
`echo "${encoded}" | base64 -d > ${quote([configPath])}`,
);
await writeFileRemote(serverId, configPath, traefikConfig);
} else {
fs.writeFileSync(configPath, traefikConfig, "utf8");
}
@@ -282,11 +273,7 @@ export const writeTraefikConfigRemote = async (
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
const yamlStr = stringify(traefikConfig);
const encoded = encodeBase64(yamlStr);
await execAsyncRemote(
serverId,
`echo "${encoded}" | base64 -d > ${quote([configPath])}`,
);
await writeFileRemote(serverId, configPath, yamlStr);
} catch (e) {
console.error("Error saving the YAML config file:", e);
}