mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #847 from Dokploy/feat/improve-server-setup
Feat/improve server setup
This commit is contained in:
@@ -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) => {
|
||||
) : (
|
||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
||||
<Tabs defaultValue="ssh-keys">
|
||||
<TabsList className="grid grid-cols-3 w-[400px]">
|
||||
<TabsList className="grid grid-cols-4 w-[600px]">
|
||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="validate">Validate</TabsTrigger>
|
||||
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent
|
||||
@@ -203,7 +205,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
|
||||
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle className="text-xl">
|
||||
Deployments
|
||||
@@ -293,6 +295,14 @@ export const SetupServer = ({ serverId }: Props) => {
|
||||
</div>
|
||||
</CardContent>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="validate"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
|
||||
<ValidateServer serverId={serverId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="gpu-setup"
|
||||
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2, PcCase, RefreshCw } from "lucide-react";
|
||||
import { StatusRow } from "./gpu-support";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
}
|
||||
|
||||
export const ValidateServer = ({ 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 (
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<PcCase className="size-5" />
|
||||
<CardTitle className="text-xl">Setup Validation</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Check if your server is ready for deployment
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isRefreshing}
|
||||
onClick={async () => {
|
||||
setIsRefreshing(true);
|
||||
await refetch();
|
||||
setIsRefreshing(false);
|
||||
}}
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="w-full">
|
||||
{error.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center text-muted-foreground py-4">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<span>Checking Server Configuration</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid w-full gap-4">
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-1">Status</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Shows the server configuration status
|
||||
</p>
|
||||
<div className="grid gap-2.5">
|
||||
<StatusRow
|
||||
label="Docker Installed"
|
||||
isEnabled={data?.docker?.enabled}
|
||||
description={
|
||||
data?.docker?.enabled
|
||||
? `Installed: ${data?.docker?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="RClone Installed"
|
||||
isEnabled={data?.rclone?.enabled}
|
||||
description={
|
||||
data?.rclone?.enabled
|
||||
? `Installed: ${data?.rclone?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Nixpacks Installed"
|
||||
isEnabled={data?.nixpacks?.enabled}
|
||||
description={
|
||||
data?.nixpacks?.enabled
|
||||
? `Installed: ${data?.nixpacks?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Buildpacks Installed"
|
||||
isEnabled={data?.buildpacks?.enabled}
|
||||
description={
|
||||
data?.buildpacks?.enabled
|
||||
? `Installed: ${data?.buildpacks?.version}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Dokploy Network Installed"
|
||||
isEnabled={data?.isDokployNetworkInstalled}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Swarm Installed"
|
||||
isEnabled={data?.isSwarmInstalled}
|
||||
/>
|
||||
<StatusRow
|
||||
label="Main Directory Created"
|
||||
isEnabled={data?.isMainDirectoryInstalled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</CardContent>
|
||||
);
|
||||
};
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()}
|
||||
|
||||
144
packages/server/src/setup/server-validate.ts
Normal file
144
packages/server/src/setup/server-validate.ts
Normal file
@@ -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<string>((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,
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user