diff --git a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx index 28c34963..4ed9df16 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/save-drag-n-drop.tsx @@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => { type="submit" className="w-fit" isLoading={isLoading} - disabled={!zip} + disabled={!zip || isLoading} > Deploy{" "} diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index d72f7021..abf6549f 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -397,7 +397,7 @@ export const applicationRouter = createTRPCRouter({ }); const app = await findApplicationById(input.applicationId as string); - await unzipDrop(zipFile, app.appName); + await unzipDrop(zipFile, app); const jobData: DeploymentJob = { applicationId: app.applicationId, @@ -405,6 +405,7 @@ export const applicationRouter = createTRPCRouter({ descriptionLog: "", type: "deploy", applicationType: "application", + server: !!app.serverId, }; await myQueue.add( "deployments", diff --git a/apps/dokploy/server/api/services/compose.ts b/apps/dokploy/server/api/services/compose.ts index d7a5aae4..0ab88f7d 100644 --- a/apps/dokploy/server/api/services/compose.ts +++ b/apps/dokploy/server/api/services/compose.ts @@ -449,6 +449,9 @@ export const stopCompose = async (composeId: string) => { const compose = await findComposeById(composeId); try { if (compose.composeType === "docker-compose") { + console.log( + `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`, + ); if (compose.serverId) { await execAsyncRemote( compose.serverId, diff --git a/apps/dokploy/server/utils/builders/compose.ts b/apps/dokploy/server/utils/builders/compose.ts index 5a5f72da..06241d86 100644 --- a/apps/dokploy/server/utils/builders/compose.ts +++ b/apps/dokploy/server/utils/builders/compose.ts @@ -9,7 +9,6 @@ import { COMPOSE_PATH } from "@/server/constants"; import type { InferResultType } from "@/server/types/with"; import boxen from "boxen"; import { - getComposePath, writeDomainsToCompose, writeDomainsToComposeRemote, } from "../docker/domain"; @@ -114,11 +113,6 @@ Compose Type: ${composeType} ✅`; cd "${projectPath}"; - if [ ! -f "${composePath}" ]; then - echo "❌ Error: Compose file not found" >> "${logPath}"; - exit 1; - fi - docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; } echo "Docker Compose Deployed: ✅" >> "${logPath}" diff --git a/apps/dokploy/server/utils/builders/drop.ts b/apps/dokploy/server/utils/builders/drop.ts index a61981c5..99b80987 100644 --- a/apps/dokploy/server/utils/builders/drop.ts +++ b/apps/dokploy/server/utils/builders/drop.ts @@ -2,12 +2,27 @@ import fs from "node:fs/promises"; import path, { join } from "node:path"; import { APPLICATIONS_PATH } from "@/server/constants"; import AdmZip from "adm-zip"; -import { recreateDirectory } from "../filesystem/directory"; +import { + recreateDirectory, + recreateDirectoryRemote, +} from "../filesystem/directory"; +import type { Application } from "@/server/api/services/application"; +import { execAsyncRemote } from "../process/execAsync"; +import { Client, type SFTPWrapper } from "ssh2"; +import { findServerById } from "@/server/api/services/server"; +import { readSSHKey } from "../filesystem/ssh"; + +export const unzipDrop = async (zipFile: File, application: Application) => { + let sftp: SFTPWrapper | null = null; -export const unzipDrop = async (zipFile: File, appName: string) => { try { + const { appName } = application; const outputPath = join(APPLICATIONS_PATH, appName, "code"); - await recreateDirectory(outputPath); + if (application.serverId) { + await recreateDirectoryRemote(outputPath, application.serverId); + } else { + await recreateDirectory(outputPath); + } const arrayBuffer = await zipFile.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); @@ -28,6 +43,9 @@ export const unzipDrop = async (zipFile: File, appName: string) => { ? rootEntries[0]?.entryName.split("/")[0] : ""; + if (application.serverId) { + sftp = await getSFTPConnection(application.serverId); + } for (const entry of zipEntries) { let filePath = entry.entryName; @@ -42,15 +60,64 @@ export const unzipDrop = async (zipFile: File, appName: string) => { if (!filePath) continue; const fullPath = path.join(outputPath, filePath); - if (entry.isDirectory) { - await fs.mkdir(fullPath, { recursive: true }); + + if (application.serverId) { + if (entry.isDirectory) { + await execAsyncRemote(application.serverId, `mkdir -p ${fullPath}`); + } else { + if (sftp === null) throw new Error("No SFTP connection available"); + await uploadFileToServer(sftp, entry.getData(), fullPath); + } } else { - await fs.mkdir(path.dirname(fullPath), { recursive: true }); - await fs.writeFile(fullPath, entry.getData()); + if (entry.isDirectory) { + await fs.mkdir(fullPath, { recursive: true }); + } else { + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + await fs.writeFile(fullPath, entry.getData()); + } } } } catch (error) { console.error("Error processing ZIP file:", error); throw error; + } finally { + sftp?.end(); } }; + +const getSFTPConnection = async (serverId: string): Promise => { + const server = await findServerById(serverId); + if (!server.sshKeyId) throw new Error("No SSH key available for this server"); + + const keys = await readSSHKey(server.sshKeyId); + return new Promise((resolve, reject) => { + const conn = new Client(); + conn + .on("ready", () => { + conn.sftp((err, sftp) => { + if (err) return reject(err); + resolve(sftp); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: keys.privateKey, + timeout: 99999, + }); + }); +}; + +const uploadFileToServer = ( + sftp: SFTPWrapper, + data: Buffer, + remotePath: string, +): Promise => { + return new Promise((resolve, reject) => { + sftp.writeFile(remotePath, data, (err) => { + if (err) return reject(err); + resolve(); + }); + }); +}; diff --git a/apps/dokploy/server/utils/docker/domain.ts b/apps/dokploy/server/utils/docker/domain.ts index f51effb2..d49a59fb 100644 --- a/apps/dokploy/server/utils/docker/domain.ts +++ b/apps/dokploy/server/utils/docker/domain.ts @@ -151,14 +151,21 @@ export const writeDomainsToComposeRemote = async ( try { const composeConverted = await addDomainToCompose(compose, domains); const path = getComposePath(compose); + + if (!composeConverted) { + return ` +echo "❌ Error: Compose file not found" >> ${logPath}; +exit 1; + `; + } if (compose.serverId) { const composeString = dump(composeConverted, { lineWidth: 1000 }); const encodedContent = encodeBase64(composeString); return `echo "${encodedContent}" | base64 -d > "${path}";`; } } catch (error) { - return ` -echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath}; + // @ts-ignore + return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath}; exit 1; `; } diff --git a/apps/dokploy/server/utils/filesystem/directory.ts b/apps/dokploy/server/utils/filesystem/directory.ts index 4f6915f4..24daca17 100644 --- a/apps/dokploy/server/utils/filesystem/directory.ts +++ b/apps/dokploy/server/utils/filesystem/directory.ts @@ -17,6 +17,20 @@ export const recreateDirectory = async (pathFolder: string): Promise => { } }; +export const recreateDirectoryRemote = async ( + pathFolder: string, + serverId: string | null, +): Promise => { + try { + await execAsyncRemote( + serverId, + `rm -rf ${pathFolder}; mkdir -p ${pathFolder}`, + ); + } catch (error) { + console.error(`Error recreating directory '${pathFolder}':`, error); + } +}; + export const removeDirectoryIfExistsContent = async ( path: string, ): Promise => {