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 },
|
{ serverId },
|
||||||
{
|
{
|
||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
refetchInterval: 1000,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync } = api.server.setup.useMutation({
|
const { mutateAsync, isLoading } = api.server.setup.useMutation({
|
||||||
// onMutate: async (variables) => {
|
onMutate: async (variables) => {
|
||||||
// console.log("Running....");
|
console.log("Running....");
|
||||||
// utils.deployment.allByServer.invalidate({ serverId: variables.serverId });
|
refetch();
|
||||||
// // refetch();
|
// refetch();
|
||||||
// },
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,7 +107,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button>Setup Server</Button>
|
<Button isLoading={isLoading}>Setup Server</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ import {
|
|||||||
randomizeComposeFile,
|
randomizeComposeFile,
|
||||||
randomizeSpecificationFile,
|
randomizeSpecificationFile,
|
||||||
} from "@/server/utils/docker/compose";
|
} 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 { removeComposeDirectory } from "@/server/utils/filesystem/directory";
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
@@ -131,7 +135,11 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const compose = await findComposeById(input.composeId);
|
const compose = await findComposeById(input.composeId);
|
||||||
await cloneCompose(compose);
|
if (compose.serverId) {
|
||||||
|
await cloneComposeRemote(compose);
|
||||||
|
} else {
|
||||||
|
await cloneCompose(compose);
|
||||||
|
}
|
||||||
return compose.sourceType;
|
return compose.sourceType;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const createComposeByTemplate = async (
|
|||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
...input,
|
...input,
|
||||||
|
serverId: "y91z1__c4SJbBe1TwQuaN",
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -241,16 +242,33 @@ export const deployCompose = async ({
|
|||||||
command += getCreateComposeFileCommand(compose);
|
command += getCreateComposeFileCommand(compose);
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.resolve()
|
// Promise.resolve()
|
||||||
.then(() => {
|
// .then(() => {
|
||||||
return execAsyncRemote(compose.serverId, command);
|
// return execAsyncRemote(compose.serverId, command);
|
||||||
})
|
// })
|
||||||
.then(() => {
|
// .then(() => {
|
||||||
return getBuildComposeCommand(compose, deployment.logPath);
|
// return getBuildComposeCommand(compose, deployment.logPath);
|
||||||
})
|
// })
|
||||||
.then(() => {
|
// .catch((err) => {
|
||||||
console.log(" ---- done ----");
|
// 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 {
|
} else {
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
await cloneGithubRepository(compose, deployment.logPath, true);
|
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 { 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 () => {
|
export const getContainers = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
writeDomainsToCompose,
|
writeDomainsToCompose,
|
||||||
writeDomainsToComposeRemote,
|
writeDomainsToComposeRemote,
|
||||||
} from "../docker/domain";
|
} from "../docker/domain";
|
||||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils";
|
||||||
import { spawnAsync } from "../process/spawnAsync";
|
import { spawnAsync } from "../process/spawnAsync";
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
|
|
||||||
@@ -106,9 +106,7 @@ docker ${command.split(" ").join(" ")} >> ${logPath} 2>&1;
|
|||||||
echo "Docker Compose Deployed: ✅" >> ${logPath};
|
echo "Docker Compose Deployed: ✅" >> ${logPath};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await execAsyncRemote(compose.serverId, bashCommand);
|
return await execAsyncRemote(compose.serverId, bashCommand);
|
||||||
|
|
||||||
return bashCommand;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeCommand = (command: string) => {
|
const sanitizeCommand = (command: string) => {
|
||||||
@@ -174,6 +172,7 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
|||||||
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
join(COMPOSE_PATH, appName, "code", "docker-compose.yml");
|
||||||
|
|
||||||
const envFilePath = join(dirname(composeFilePath), ".env");
|
const envFilePath = join(dirname(composeFilePath), ".env");
|
||||||
|
|
||||||
let envContent = env || "";
|
let envContent = env || "";
|
||||||
if (!envContent.includes("DOCKER_CONFIG")) {
|
if (!envContent.includes("DOCKER_CONFIG")) {
|
||||||
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
envContent += "\nDOCKER_CONFIG=/root/.docker/config.json";
|
||||||
@@ -184,8 +183,10 @@ export const getCreateEnvFileCommand = (compose: ComposeNested) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
const envFileContent = prepareEnvironmentVariables(envContent).join("\n");
|
||||||
|
|
||||||
|
const encodedContent = encodeBase64(envFileContent);
|
||||||
return `
|
return `
|
||||||
mkdir -p ${envFilePath};
|
touch ${envFilePath};
|
||||||
echo "${envFileContent}" > ${envFilePath};
|
echo "${encodedContent}" | base64 -d > "${envFilePath}";
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import type {
|
|||||||
PropertiesNetworks,
|
PropertiesNetworks,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { encodeBase64 } from "./utils";
|
||||||
|
|
||||||
export const cloneCompose = async (compose: Compose) => {
|
export const cloneCompose = async (compose: Compose) => {
|
||||||
if (compose.sourceType === "github") {
|
if (compose.sourceType === "github") {
|
||||||
@@ -144,13 +145,14 @@ export const writeDomainsToComposeRemote = async (
|
|||||||
try {
|
try {
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
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) {
|
} catch (error) {
|
||||||
throw 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 (
|
export const addDomainToCompose = async (
|
||||||
compose: Compose,
|
compose: Compose,
|
||||||
domains: Domain[],
|
domains: Domain[],
|
||||||
|
|||||||
@@ -410,6 +410,8 @@ export const createFile = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
export const encodeBase64 = (content: string) =>
|
||||||
|
Buffer.from(content, "utf-8").toString("base64");
|
||||||
|
|
||||||
export const getCreateFileCommand = (
|
export const getCreateFileCommand = (
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
@@ -422,10 +424,10 @@ export const getCreateFileCommand = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const directory = path.dirname(fullPath);
|
const directory = path.dirname(fullPath);
|
||||||
|
const encodedContent = encodeBase64(content);
|
||||||
return `
|
return `
|
||||||
mkdir -p ${directory};
|
mkdir -p ${directory};
|
||||||
echo "${content}" > ${fullPath};
|
echo "${encodedContent}" | base64 -d > "${fullPath}";
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,81 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import util from "node:util";
|
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 execAsync = util.promisify(exec);
|
||||||
|
|
||||||
export const execAsyncRemote = async (
|
export const execAsyncRemote = async (
|
||||||
serverId: string,
|
serverId: string | null,
|
||||||
command: string,
|
command: string,
|
||||||
): Promise<{ stdout: string; stderr: 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
client.exec(command, (err, stream) => {
|
conn
|
||||||
if (err) {
|
.once("ready", () => {
|
||||||
client.end();
|
console.log("Client :: ready");
|
||||||
return reject(err);
|
conn.exec(command, (err, stream) => {
|
||||||
}
|
if (err) throw err;
|
||||||
|
stream
|
||||||
let stdout = "";
|
.on("close", (code, signal) => {
|
||||||
let stderr = "";
|
console.log(
|
||||||
|
`Stream :: close :: code: ${code}, signal: ${signal}`,
|
||||||
stream
|
);
|
||||||
.on("data", (data: string) => {
|
conn.end();
|
||||||
stdout += data.toString();
|
if (code === 0) {
|
||||||
})
|
resolve({ stdout, stderr });
|
||||||
.on("close", (code, signal) => {
|
} else {
|
||||||
client.end();
|
reject(new Error(`Command exited with code ${code}`));
|
||||||
if (code === 0) {
|
}
|
||||||
resolve({ stdout, stderr });
|
})
|
||||||
} else {
|
.on("data", (data: string) => {
|
||||||
reject(new Error(`Command exited with code ${code}`));
|
stdout += data.toString();
|
||||||
}
|
})
|
||||||
})
|
.stderr.on("data", (data) => {
|
||||||
.stderr.on("data", (data) => {
|
stderr += data.toString();
|
||||||
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 { COMPOSE_PATH } from "@/server/constants";
|
||||||
import { recreateDirectory } from "../filesystem/directory";
|
import { recreateDirectory } from "../filesystem/directory";
|
||||||
import { execAsyncRemote } from "../process/execAsync";
|
import { execAsyncRemote } from "../process/execAsync";
|
||||||
|
import { encodeBase64 } from "../docker/utils";
|
||||||
|
|
||||||
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
export const createComposeFile = async (compose: Compose, logPath: string) => {
|
||||||
const { appName, composeFile } = compose;
|
const { appName, composeFile } = compose;
|
||||||
@@ -32,13 +33,13 @@ export const getCreateComposeFileCommand = (compose: Compose) => {
|
|||||||
const { appName, composeFile } = compose;
|
const { appName, composeFile } = compose;
|
||||||
const outputPath = join(COMPOSE_PATH, appName, "code");
|
const outputPath = join(COMPOSE_PATH, appName, "code");
|
||||||
const filePath = join(outputPath, "docker-compose.yml");
|
const filePath = join(outputPath, "docker-compose.yml");
|
||||||
const command = [];
|
const encodedContent = encodeBase64(composeFile);
|
||||||
command.push(`rm -rf ${outputPath};`);
|
const bashCommand = `
|
||||||
command.push(`mkdir -p ${outputPath};`);
|
rm -rf ${outputPath};
|
||||||
command.push(
|
mkdir -p ${outputPath};
|
||||||
`printf '%s' '${composeFile.replace(/'/g, "'\\''")}' > ${filePath};`,
|
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||||
);
|
`;
|
||||||
return command.join("\n");
|
return bashCommand;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createComposeFileRaw = async (compose: Compose) => {
|
export const createComposeFileRaw = async (compose: Compose) => {
|
||||||
@@ -59,9 +60,10 @@ export const createComposeFileRawRemote = async (compose: Compose) => {
|
|||||||
const filePath = join(outputPath, "docker-compose.yml");
|
const filePath = join(outputPath, "docker-compose.yml");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const encodedContent = encodeBase64(composeFile);
|
||||||
const command = `
|
const command = `
|
||||||
mkdir -p ${outputPath};
|
mkdir -p ${outputPath};
|
||||||
echo "${composeFile}" > ${filePath};
|
echo "${encodedContent}" | base64 -d > "${filePath}";
|
||||||
`;
|
`;
|
||||||
await execAsyncRemote(serverId, command);
|
await execAsyncRemote(serverId, command);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,24 +1,3 @@
|
|||||||
import { findServerById } from "@/server/api/services/server";
|
import { findServerById } from "@/server/api/services/server";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
import { readSSHKey } from "../filesystem/ssh";
|
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);
|
const keys = await readSSHKey(server.sshKeyId);
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
client
|
client
|
||||||
.on("ready", () => {
|
.once("ready", () => {
|
||||||
console.log("Client :: ready");
|
console.log("Client :: ready");
|
||||||
const bashCommand = `
|
const bashCommand = `
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
const client = new Client();
|
const client = new Client();
|
||||||
new Promise<void>((resolve, reject) => {
|
new Promise<void>((resolve, reject) => {
|
||||||
client
|
client
|
||||||
.on("ready", () => {
|
.once("ready", () => {
|
||||||
const command = `
|
const command = `
|
||||||
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user