diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 119d4d29d..eb0d22553 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -33,6 +33,7 @@ import { useState } from "react"; import { toast } from "sonner"; import { ShowDeployment } from "../../application/deployments/show-deployment"; import { GPUSupport } from "./gpu-support"; +import { ValidateServer } from "./validate-server"; interface Props { serverId: string; @@ -90,9 +91,10 @@ export const SetupServer = ({ serverId }: Props) => { ) : (
- + SSH Keys Deployments + Validate GPU Setup {
-
+
Deployments @@ -293,6 +295,14 @@ export const SetupServer = ({ serverId }: Props) => {
+ +
+ +
+
{ + const [isRefreshing, setIsRefreshing] = useState(false); + const { data, refetch, error, isLoading, isError } = + api.server.validate.useQuery( + { serverId }, + { + enabled: !!serverId, + }, + ); + const utils = api.useUtils(); + return ( + +
+ + +
+
+
+ + Setup Validation +
+ + Check if your server is ready for deployment + +
+ +
+
+ {isError && ( + + {error.message} + + )} +
+
+ + + {isLoading ? ( +
+ + Checking Server Configuration +
+ ) : ( +
+
+

Status

+

+ Shows the server configuration status +

+
+ + + + + + + +
+
+
+ )} +
+
+
+
+ ); +}; diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 0d4ef87f3..f0ea4fedb 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -26,6 +26,7 @@ import { haveActiveServices, removeDeploymentsByServerId, serverSetup, + serverValidate, updateServerById, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; @@ -118,6 +119,47 @@ export const serverRouter = createTRPCRouter({ throw error; } }), + validate: protectedProcedure + .input(apiFindOneServer) + .query(async ({ input, ctx }) => { + try { + const server = await findServerById(input.serverId); + if (server.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to validate this server", + }); + } + const response = await serverValidate(input.serverId); + return response as unknown as { + docker: { + enabled: boolean; + version: string; + }; + rclone: { + enabled: boolean; + version: string; + }; + nixpacks: { + enabled: boolean; + version: string; + }; + buildpacks: { + enabled: boolean; + version: string; + }; + isDokployNetworkInstalled: boolean; + isSwarmInstalled: boolean; + isMainDirectoryInstalled: boolean; + }; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: error instanceof Error ? error?.message : `Error: ${error}`, + cause: error as Error, + }); + } + }), remove: protectedProcedure .input(apiRemoveServer) .mutation(async ({ input, ctx }) => { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index b8ec30e24..f3f1e96f5 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -41,6 +41,7 @@ export * from "./setup/redis-setup"; export * from "./setup/server-setup"; export * from "./setup/setup"; export * from "./setup/traefik-setup"; +export * from "./setup/server-validate"; export * from "./utils/backups/index"; export * from "./utils/backups/mariadb"; diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 73adec43e..81e497795 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -132,14 +132,16 @@ const installRequirements = async (serverId: string, logPath: string) => { echo -e "---------------------------------------------\n" echo -e "1. Installing required packages (curl, wget, git, jq, openssl). " + command_exists() { + command -v "$@" > /dev/null 2>&1 + } + ${installUtilities()} echo -e "2. Validating ports. " ${validatePorts()} - command_exists() { - command -v "$@" > /dev/null 2>&1 - } + echo -e "3. Installing RClone. " ${installRClone()} diff --git a/packages/server/src/setup/server-validate.ts b/packages/server/src/setup/server-validate.ts new file mode 100644 index 000000000..0729feed7 --- /dev/null +++ b/packages/server/src/setup/server-validate.ts @@ -0,0 +1,144 @@ +import { Client } from "ssh2"; +import { findServerById } from "../services/server"; + +export const validateDocker = () => ` + if command_exists docker; then + echo "$(docker --version | awk '{print $3}' | sed 's/,//') true" + else + echo "0.0.0 false" + fi +`; + +export const validateRClone = () => ` + if command_exists rclone; then + echo "$(rclone --version | head -n 1 | awk '{print $2}') true" + else + echo "0.0.0 false" + fi +`; + +export const validateSwarm = () => ` + if docker info --format '{{.Swarm.LocalNodeState}}' | grep -q 'active'; then + echo true + else + echo false + fi +`; + +export const validateNixpacks = () => ` + if command_exists nixpacks; then + echo "$(nixpacks --version | awk '{print $2}') true" + else + echo "0.0.0 false" + fi +`; + +export const validateBuildpacks = () => ` + if command_exists pack; then + echo "$(pack --version | awk '{print $1}') true" + else + echo "0.0.0 false" + fi +`; + +export const validateMainDirectory = () => ` + if [ -d "/etc/dokploy" ]; then + echo true + else + echo false + fi +`; + +export const validateDokployNetwork = () => ` + if docker network ls | grep -q 'dokploy-network'; then + echo true + else + echo false + fi +`; + +export const serverValidate = async (serverId: string) => { + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + throw new Error("No SSH Key found"); + } + + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const bashCommand = ` + command_exists() { + command -v "$@" > /dev/null 2>&1 + } + + dockerVersionEnabled=$(${validateDocker()}) + rcloneVersionEnabled=$(${validateRClone()}) + nixpacksVersionEnabled=$(${validateNixpacks()}) + buildpacksVersionEnabled=$(${validateBuildpacks()}) + + dockerVersion=$(echo $dockerVersionEnabled | awk '{print $1}') + dockerEnabled=$(echo $dockerVersionEnabled | awk '{print $2}') + + rcloneVersion=$(echo $rcloneVersionEnabled | awk '{print $1}') + rcloneEnabled=$(echo $rcloneVersionEnabled | awk '{print $2}') + + nixpacksVersion=$(echo $nixpacksVersionEnabled | awk '{print $1}') + nixpacksEnabled=$(echo $nixpacksVersionEnabled | awk '{print $2}') + + buildpacksVersion=$(echo $buildpacksVersionEnabled | awk '{print $1}') + buildpacksEnabled=$(echo $buildpacksVersionEnabled | awk '{print $2}') + + isDokployNetworkInstalled=$(${validateDokployNetwork()}) + isSwarmInstalled=$(${validateSwarm()}) + isMainDirectoryInstalled=$(${validateMainDirectory()}) + + echo "{\\"docker\\": {\\"version\\": \\"$dockerVersion\\", \\"enabled\\": $dockerEnabled}, \\"rclone\\": {\\"version\\": \\"$rcloneVersion\\", \\"enabled\\": $rcloneEnabled}, \\"nixpacks\\": {\\"version\\": \\"$nixpacksVersion\\", \\"enabled\\": $nixpacksEnabled}, \\"buildpacks\\": {\\"version\\": \\"$buildpacksVersion\\", \\"enabled\\": $buildpacksEnabled}, \\"isDokployNetworkInstalled\\": $isDokployNetworkInstalled, \\"isSwarmInstalled\\": $isSwarmInstalled, \\"isMainDirectoryInstalled\\": $isMainDirectoryInstalled}" + `; + client.exec(bashCommand, (err, stream) => { + if (err) { + reject(err); + return; + } + let output = ""; + stream + .on("close", () => { + client.end(); + try { + const result = JSON.parse(output.trim()); + resolve(result); + } catch (parseError) { + reject( + new Error( + `Failed to parse output: ${parseError instanceof Error ? parseError.message : parseError}`, + ), + ); + } + }) + .on("data", (data: string) => { + output += data; + }) + .stderr.on("data", (data) => {}); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ), + ); + } else { + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + }); +};