mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor(multi-server): fix deploy on docker compose
This commit is contained in:
@@ -46,16 +46,15 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
{ serverId },
|
||||
{
|
||||
enabled: !!serverId,
|
||||
refetchInterval: 1000,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync } = api.server.setup.useMutation({
|
||||
// onMutate: async (variables) => {
|
||||
// console.log("Running....");
|
||||
// utils.deployment.allByServer.invalidate({ serverId: variables.serverId });
|
||||
// // refetch();
|
||||
// },
|
||||
const { mutateAsync, isLoading } = api.server.setup.useMutation({
|
||||
onMutate: async (variables) => {
|
||||
console.log("Running....");
|
||||
refetch();
|
||||
// refetch();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -108,7 +107,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button>Setup Server</Button>
|
||||
<Button isLoading={isLoading}>Setup Server</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
randomizeComposeFile,
|
||||
randomizeSpecificationFile,
|
||||
} from "@/server/utils/docker/compose";
|
||||
import { addDomainToCompose, cloneCompose } from "@/server/utils/docker/domain";
|
||||
import {
|
||||
addDomainToCompose,
|
||||
cloneCompose,
|
||||
cloneComposeRemote,
|
||||
} from "@/server/utils/docker/domain";
|
||||
import { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { templates } from "@/templates/templates";
|
||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||
@@ -131,7 +135,11 @@ export const composeRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
await cloneCompose(compose);
|
||||
if (compose.serverId) {
|
||||
await cloneComposeRemote(compose);
|
||||
} else {
|
||||
await cloneCompose(compose);
|
||||
}
|
||||
return compose.sourceType;
|
||||
} catch (err) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -97,6 +97,7 @@ export const createComposeByTemplate = async (
|
||||
.insert(compose)
|
||||
.values({
|
||||
...input,
|
||||
serverId: "y91z1__c4SJbBe1TwQuaN",
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -241,16 +242,33 @@ export const deployCompose = async ({
|
||||
command += getCreateComposeFileCommand(compose);
|
||||
}
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
return execAsyncRemote(compose.serverId, command);
|
||||
})
|
||||
.then(() => {
|
||||
return getBuildComposeCommand(compose, deployment.logPath);
|
||||
})
|
||||
.then(() => {
|
||||
console.log(" ---- done ----");
|
||||
});
|
||||
// Promise.resolve()
|
||||
// .then(() => {
|
||||
// return execAsyncRemote(compose.serverId, command);
|
||||
// })
|
||||
// .then(() => {
|
||||
// return getBuildComposeCommand(compose, deployment.logPath);
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// throw err;
|
||||
// })
|
||||
// .then(() => {
|
||||
// console.log(" ---- done ----");
|
||||
// });
|
||||
async function* sequentialSteps() {
|
||||
yield execAsyncRemote(compose.serverId, command);
|
||||
yield getBuildComposeCommand(compose, deployment.logPath);
|
||||
}
|
||||
|
||||
const steps = sequentialSteps();
|
||||
for await (const step of steps) {
|
||||
if (step.stderr) {
|
||||
console.log(step.stderr);
|
||||
}
|
||||
step;
|
||||
}
|
||||
|
||||
console.log(" ---- done ----");
|
||||
} else {
|
||||
if (compose.sourceType === "github") {
|
||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { readSSHKey } from "@/server/utils/filesystem/ssh";
|
||||
import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync";
|
||||
import { tail } from "lodash";
|
||||
import { stderr, stdout } from "node:process";
|
||||
import { Client } from "ssh2";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export const getContainers = async () => {
|
||||
try {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
writeDomainsToCompose,
|
||||
writeDomainsToComposeRemote,
|
||||
} from "../docker/domain";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
@@ -106,9 +106,7 @@ docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1;
|
||||
echo "Docker Compose Deployed: ✅" >> ${logPath};
|
||||
`;
|
||||
|
||||
await execAsyncRemote(compose.serverId, bashCommand);
|
||||
|
||||
return bashCommand;
|
||||
return await execAsyncRemote(compose.serverId, bashCommand);
|
||||
};
|
||||
|
||||
const sanitizeCommand = (command: string) => {
|
||||
@@ -174,6 +172,7 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
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";
|
||||
@@ -184,8 +183,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
||||
}
|
||||
|
||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||
|
||||
const encodedContent = encodeBase64(envFileContent);
|
||||
return `
|
||||
mkdir -p ${envFilePath};
|
||||
echo "${envFileContent}" > ${envFilePath};
|
||||
touch ${envFilePath};
|
||||
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
PropertiesNetworks,
|
||||
} from "./types";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { encodeBase64 } from "./utils";
|
||||
|
||||
export const cloneCompose = async (compose: Compose) => {
|
||||
if (compose.sourceType === "github") {
|
||||
@@ -144,13 +145,14 @@ export const writeDomainsToComposeRemote = async (
|
||||
try {
|
||||
if (compose.serverId) {
|
||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||
return `printf '%s' '${composeString.replace(/'/g, "'\\''")}' > ${path}`;
|
||||
const encodedContent = encodeBase64(composeString);
|
||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit
|
||||
export const addDomainToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
|
||||
@@ -410,6 +410,8 @@ export const createFile = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const encodeBase64 = (content: string) =>
|
||||
Buffer.from(content, "utf-8").toString("base64");
|
||||
|
||||
export const getCreateFileCommand = (
|
||||
outputPath: string,
|
||||
@@ -422,10 +424,10 @@ export const getCreateFileCommand = (
|
||||
}
|
||||
|
||||
const directory = path.dirname(fullPath);
|
||||
|
||||
const encodedContent = encodeBase64(content);
|
||||
return `
|
||||
mkdir -p ${directory};
|
||||
echo "${content}" > ${fullPath};
|
||||
echo "${encodedContent}" | base64 -d > "${fullPath}";
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,39 +1,81 @@
|
||||
import { exec } from "node:child_process";
|
||||
import util from "node:util";
|
||||
import { connectSSH } from "../servers/connection";
|
||||
import { findServerById } from "@/server/api/services/server";
|
||||
import { readSSHKey } from "../filesystem/ssh";
|
||||
import { Client } from "ssh2";
|
||||
export const execAsync = util.promisify(exec);
|
||||
|
||||
export const execAsyncRemote = async (
|
||||
serverId: string,
|
||||
serverId: string | null,
|
||||
command: string,
|
||||
): Promise<{ stdout: string; stderr: string }> => {
|
||||
const client = await connectSSH(serverId);
|
||||
if (!serverId) return { stdout: "", stderr: "" };
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
|
||||
|
||||
const keys = await readSSHKey(server.sshKeyId);
|
||||
|
||||
const conn = new Client();
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
return new Promise((resolve, reject) => {
|
||||
client.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
client.end();
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
stream
|
||||
.on("data", (data: string) => {
|
||||
stdout += data.toString();
|
||||
})
|
||||
.on("close", (code, signal) => {
|
||||
client.end();
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`Command exited with code ${code}`));
|
||||
}
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
conn
|
||||
.once("ready", () => {
|
||||
console.log("Client :: ready");
|
||||
conn.exec(command, (err, stream) => {
|
||||
if (err) throw err;
|
||||
stream
|
||||
.on("close", (code, signal) => {
|
||||
console.log(
|
||||
`Stream :: close :: code: ${code}, signal: ${signal}`,
|
||||
);
|
||||
conn.end();
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`Command exited with code ${code}`));
|
||||
}
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
stdout += data.toString();
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: keys.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
|
||||
// client.exec(command, (err, stream) => {
|
||||
// if (err) {
|
||||
// client.end();
|
||||
// return reject(err);
|
||||
// }
|
||||
|
||||
// let stdout = "";
|
||||
// let stderr = "";
|
||||
|
||||
// stream
|
||||
// .on("data", (data: string) => {
|
||||
// stdout += data.toString();
|
||||
// })
|
||||
// .on("close", (code, signal) => {
|
||||
// client.end();
|
||||
// if (code === 0) {
|
||||
// resolve({ stdout, stderr });
|
||||
// } else {
|
||||
// reject(new Error(`Command exited with code ${code}`));
|
||||
// }
|
||||
// })
|
||||
// .stderr.on("data", (data) => {
|
||||
// stderr += data.toString();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Compose } from "@/server/api/services/compose";
|
||||
import { COMPOSE_PATH } from "@/server/constants";
|
||||
import { recreateDirectory } from "../filesystem/directory";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
|
||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||
const { appName, composeFile } = compose;
|
||||
@@ -32,13 +33,13 @@ export const getCreateComposeFileCommand = (compose: Compose) => {
|
||||
const { appName, composeFile } = compose;
|
||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
const command = [];
|
||||
command.push(`rm -rf ${outputPath};`);
|
||||
command.push(`mkdir -p ${outputPath};`);
|
||||
command.push(
|
||||
`printf '%s' '${composeFile.replace(/'/g, "'\\''")}' > ${filePath};`,
|
||||
);
|
||||
return command.join("\n");
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const bashCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
`;
|
||||
return bashCommand;
|
||||
};
|
||||
|
||||
export const createComposeFileRaw = async (compose: Compose) => {
|
||||
@@ -59,9 +60,10 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
|
||||
const filePath = join(outputPath, "docker-compose.yml");
|
||||
|
||||
try {
|
||||
const encodedContent = encodeBase64(composeFile);
|
||||
const command = `
|
||||
mkdir -p ${outputPath};
|
||||
echo "${composeFile}" > ${filePath};
|
||||
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
import { findServerById } from "@/server/api/services/server";
|
||||
import { Client } from "ssh2";
|
||||
import { readSSHKey } from "../filesystem/ssh";
|
||||
|
||||
export const connectSSH = async (serverId: string) => {
|
||||
const server = await findServerById(serverId);
|
||||
if (!server.sshKeyId) throw new Error("No SSH key available for this server");
|
||||
|
||||
const keys = await readSSHKey(server.sshKeyId);
|
||||
const client = new Client();
|
||||
|
||||
return new Promise<Client>((resolve, reject) => {
|
||||
client
|
||||
.once("ready", () => resolve(client))
|
||||
.on("error", reject)
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
username: server.username,
|
||||
privateKey: keys.privateKey,
|
||||
timeout: 99999,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ const connectToServer = async (serverId: string, logPath: string) => {
|
||||
const keys = await readSSHKey(server.sshKeyId);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.on("ready", () => {
|
||||
.once("ready", () => {
|
||||
console.log("Client :: ready");
|
||||
const bashCommand = `
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
const client = new Client();
|
||||
new Promise<void>((resolve, reject) => {
|
||||
client
|
||||
.on("ready", () => {
|
||||
.once("ready", () => {
|
||||
const command = `
|
||||
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user