mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add dokploy server gpu setup
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
import { GPUSupportModal } from "../gpu-support-modal";
|
||||||
|
|
||||||
export const ShowDokployActions = () => {
|
export const ShowDokployActions = () => {
|
||||||
const { mutateAsync: reloadServer, isLoading } =
|
const { mutateAsync: reloadServer, isLoading } =
|
||||||
@@ -45,6 +46,7 @@ export const ShowDokployActions = () => {
|
|||||||
<ShowModalLogs appName="dokploy">
|
<ShowModalLogs appName="dokploy">
|
||||||
<span>Watch logs</span>
|
<span>Watch logs</span>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
|
<GPUSupportModal />
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { GPUSupport } from "./gpu-support";
|
||||||
|
|
||||||
|
export const GPUSupportModal = () => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<span>GPU Setup</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-4xl overflow-y-auto max-h-screen">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
Dokploy Server GPU Setup
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<GPUSupport serverId="" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,7 +26,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
|
|||||||
api.settings.checkGPUStatus.useQuery(
|
api.settings.checkGPUStatus.useQuery(
|
||||||
{ serverId },
|
{ serverId },
|
||||||
{
|
{
|
||||||
enabled: !!serverId,
|
enabled: serverId !== undefined,
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -38,17 +38,20 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast.success("GPU support enabled successfully");
|
toast.success("GPU support enabled successfully");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
await utils.settings.checkGPUStatus.invalidate({ serverId });
|
||||||
await Promise.all([
|
|
||||||
utils.settings.checkGPUStatus.invalidate({ serverId }),
|
|
||||||
utils.server.invalidate(),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (error instanceof TRPCClientError) {
|
if (error instanceof TRPCClientError) {
|
||||||
const errorMessage = error.message;
|
const errorMessage = error.message;
|
||||||
if (errorMessage.includes("permission denied")) {
|
if (
|
||||||
toast.error("Permission denied. Please ensure proper sudo access.");
|
errorMessage.includes(
|
||||||
|
"Permission denied. Please ensure proper sudo access.",
|
||||||
|
) ||
|
||||||
|
errorMessage.includes("sudo access required")
|
||||||
|
) {
|
||||||
|
toast.error(
|
||||||
|
"Administrator privileges required. Please enter your password when prompted.",
|
||||||
|
);
|
||||||
} else if (errorMessage.includes("Failed to configure GPU")) {
|
} else if (errorMessage.includes("Failed to configure GPU")) {
|
||||||
toast.error(
|
toast.error(
|
||||||
"GPU configuration failed. Please check system requirements.",
|
"GPU configuration failed. Please check system requirements.",
|
||||||
@@ -59,13 +62,12 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
|
|||||||
} else {
|
} else {
|
||||||
toast.error("Failed to enable GPU support. Please check server logs.");
|
toast.error("Failed to enable GPU support. Please check server logs.");
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleEnableGPU = async () => {
|
const handleEnableGPU = async () => {
|
||||||
if (!serverId) {
|
if (serverId === undefined) {
|
||||||
toast.error("No server selected");
|
toast.error("No server selected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -99,7 +101,7 @@ export function GPUSupport({ serverId }: GPUSupportProps) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={isLoading || !serverId || isChecking}
|
disabled={isLoading || serverId === undefined || isChecking}
|
||||||
>
|
>
|
||||||
{isLoading
|
{isLoading
|
||||||
? "Enabling GPU..."
|
? "Enabling GPU..."
|
||||||
@@ -227,7 +229,7 @@ interface StatusRowProps {
|
|||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusRow({
|
export function StatusRow({
|
||||||
label,
|
label,
|
||||||
isEnabled,
|
isEnabled,
|
||||||
description,
|
description,
|
||||||
|
|||||||
@@ -661,33 +661,20 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
setupGPU: adminProcedure
|
setupGPU: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
serverId: z.string(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
throw new Error("GPU setup is not available in cloud mode");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!input.serverId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Server ID is required",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await setupGPUSupport(input.serverId);
|
await setupGPUSupport(input.serverId);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
console.error("GPU Setup Error:", error);
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
throw error;
|
||||||
message:
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Failed to enable GPU support",
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
checkGPUStatus: adminProcedure
|
checkGPUStatus: adminProcedure
|
||||||
@@ -712,7 +699,15 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
gpuResources: 0,
|
gpuResources: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return await checkGPUStatus(input.serverId);
|
|
||||||
|
try {
|
||||||
|
const status = await checkGPUStatus(input.serverId || "");
|
||||||
|
console.log("GPU Status Check Result:", status);
|
||||||
|
return status;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("GPU Status Check Error:", error);
|
||||||
|
throw new Error("Failed to check GPU status");
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as fs from "node:fs/promises";
|
||||||
import { execAsync } from "../utils/process/execAsync";
|
import { execAsync } from "../utils/process/execAsync";
|
||||||
import { execAsyncRemote } from "../utils/process/execAsync";
|
import { execAsyncRemote } from "../utils/process/execAsync";
|
||||||
|
|
||||||
@@ -191,18 +192,36 @@ export async function setupGPUSupport(serverId?: string): Promise<void> {
|
|||||||
"node-generic-resources": [`GPU=${initialStatus.availableGPUs}`],
|
"node-generic-resources": [`GPU=${initialStatus.availableGPUs}`],
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupCommands = [
|
// Different commands for local and remote setup
|
||||||
"sudo -n true",
|
|
||||||
`echo '${JSON.stringify(daemonConfig, null, 2)}' | sudo tee /etc/docker/daemon.json`,
|
|
||||||
"sudo mkdir -p /etc/nvidia-container-runtime",
|
|
||||||
'echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" | sudo tee -a /etc/nvidia-container-runtime/config.toml',
|
|
||||||
"sudo systemctl daemon-reload",
|
|
||||||
"sudo systemctl restart docker",
|
|
||||||
].join(" && ");
|
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
|
// Remote server setup (using sudo)
|
||||||
|
const setupCommands = [
|
||||||
|
"sudo -n true",
|
||||||
|
`echo '${JSON.stringify(daemonConfig, null, 2)}' | sudo tee /etc/docker/daemon.json`,
|
||||||
|
"sudo mkdir -p /etc/nvidia-container-runtime",
|
||||||
|
'echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" | sudo tee -a /etc/nvidia-container-runtime/config.toml',
|
||||||
|
"sudo systemctl daemon-reload",
|
||||||
|
"sudo systemctl restart docker",
|
||||||
|
].join(" && ");
|
||||||
|
|
||||||
await execAsyncRemote(serverId, setupCommands);
|
await execAsyncRemote(serverId, setupCommands);
|
||||||
} else {
|
} else {
|
||||||
|
// Local server setup (using pkexec for GUI password prompt)
|
||||||
|
const configFile = `/tmp/docker-daemon-${Date.now()}.json`;
|
||||||
|
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
|
||||||
|
|
||||||
|
const setupCommands = [
|
||||||
|
// Use pkexec for GUI password prompt
|
||||||
|
`pkexec sh -c '
|
||||||
|
cp ${configFile} /etc/docker/daemon.json &&
|
||||||
|
mkdir -p /etc/nvidia-container-runtime &&
|
||||||
|
echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" >> /etc/nvidia-container-runtime/config.toml &&
|
||||||
|
systemctl daemon-reload &&
|
||||||
|
systemctl restart docker
|
||||||
|
'`,
|
||||||
|
`rm ${configFile}`, // Clean up temp file
|
||||||
|
].join(" && ");
|
||||||
|
|
||||||
await execAsync(setupCommands);
|
await execAsync(setupCommands);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +263,14 @@ export async function setupGPUSupport(serverId?: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("GPU Setup Error:", error);
|
console.error("GPU Setup Error:", error);
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("password is required")
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Sudo access required. Please run with appropriate permissions.",
|
||||||
|
);
|
||||||
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user