Merge branch 'canary' into feat/new-templates

This commit is contained in:
Mauricio Siu
2024-11-17 10:51:17 -06:00
committed by GitHub
38 changed files with 1528 additions and 76 deletions

View File

@@ -18,8 +18,10 @@ jobs:
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
else
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
else
TAG="feature"
fi
docker build --platform linux/amd64 -t dokploy/dokploy:${TAG}-amd64 .
docker push dokploy/dokploy:${TAG}-amd64
@@ -41,8 +43,10 @@ jobs:
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
if [ "${CIRCLE_BRANCH}" == "main" ]; then
TAG="latest"
else
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
else
TAG="feature"
fi
docker build --platform linux/arm64 -t dokploy/dokploy:${TAG}-arm64 .
docker push dokploy/dokploy:${TAG}-arm64
@@ -72,12 +76,18 @@ jobs:
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${VERSION}
else
elif [ "${CIRCLE_BRANCH}" == "canary" ]; then
TAG="canary"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
else
TAG="feature"
docker manifest create dokploy/dokploy:${TAG} \
dokploy/dokploy:${TAG}-amd64 \
dokploy/dokploy:${TAG}-arm64
docker manifest push dokploy/dokploy:${TAG}
fi
workflows:
@@ -89,12 +99,14 @@ workflows:
only:
- main
- canary
- pull/665
- build-arm64:
filters:
branches:
only:
- main
- canary
- pull/665
- combine-manifests:
requires:
- build-amd64
@@ -104,3 +116,4 @@ workflows:
only:
- main
- canary
- pull/665

View File

@@ -14,10 +14,12 @@ We have a few guidelines to follow when contributing to this project:
## Commit Convention
Before you create a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
### Commit Message Format
```
<type>[optional scope]: <description>
@@ -235,7 +237,7 @@ export function generate(schema: Schema): Template {
5. Add the logo or image of the template to `public/templates/plausible.svg`
### Recomendations
### Recommendations
- Use the same name of the folder as the id of the template.
- The logo should be in the public folder.

View File

@@ -20,6 +20,15 @@ import {
FormMessage,
} from "@/components/ui/form";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import useLocale from "@/utils/hooks/use-locale";
import { useTranslation } from "next-i18next";
import { useTheme } from "next-themes";
import { useEffect } from "react";
import { toast } from "sonner";
@@ -28,6 +37,9 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], {
required_error: "Please select a theme.",
}),
language: z.enum(["en", "zh-Hans"], {
required_error: "Please select a language.",
}),
});
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
@@ -35,10 +47,14 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: "system",
language: "en",
};
export function AppearanceForm() {
const { setTheme, theme } = useTheme();
const { locale, setLocale } = useLocale();
const { t } = useTranslation("settings");
const form = useForm<AppearanceFormValues>({
resolver: zodResolver(appearanceFormSchema),
defaultValues,
@@ -47,19 +63,23 @@ export function AppearanceForm() {
useEffect(() => {
form.reset({
theme: (theme ?? "system") as AppearanceFormValues["theme"],
language: locale,
});
}, [form, theme]);
}, [form, theme, locale]);
function onSubmit(data: AppearanceFormValues) {
setTheme(data.theme);
setLocale(data.language);
toast.success("Preferences Updated");
}
return (
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Appearance</CardTitle>
<CardTitle className="text-xl">
{t("settings.appearance.title")}
</CardTitle>
<CardDescription>
Customize the theme of your dashboard.
{t("settings.appearance.description")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
@@ -72,9 +92,9 @@ export function AppearanceForm() {
render={({ field }) => {
return (
<FormItem className="space-y-1 ">
<FormLabel>Theme</FormLabel>
<FormLabel>{t("settings.appearance.theme")}</FormLabel>
<FormDescription>
Select a theme for your dashboard
{t("settings.appearance.themeDescription")}
</FormDescription>
<FormMessage />
<RadioGroup
@@ -92,7 +112,7 @@ export function AppearanceForm() {
<img src="/images/theme-light.svg" alt="light" />
</div>
<span className="block w-full p-2 text-center font-normal">
Light
{t("settings.appearance.themes.light")}
</span>
</FormLabel>
</FormItem>
@@ -105,7 +125,7 @@ export function AppearanceForm() {
<img src="/images/theme-dark.svg" alt="dark" />
</div>
<span className="block w-full p-2 text-center font-normal">
Dark
{t("settings.appearance.themes.dark")}
</span>
</FormLabel>
</FormItem>
@@ -121,7 +141,7 @@ export function AppearanceForm() {
<img src="/images/theme-system.svg" alt="system" />
</div>
<span className="block w-full p-2 text-center font-normal">
System
{t("settings.appearance.themes.system")}
</span>
</FormLabel>
</FormItem>
@@ -131,7 +151,43 @@ export function AppearanceForm() {
}}
/>
<Button type="submit">Save</Button>
<FormField
control={form.control}
name="language"
defaultValue={form.control._defaultValues.language}
render={({ field }) => {
return (
<FormItem className="space-y-1">
<FormLabel>{t("settings.appearance.language")}</FormLabel>
<FormDescription>
{t("settings.appearance.languageDescription")}
</FormDescription>
<FormMessage />
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{[
{ label: "English", value: "en" },
{ label: "简体中文", value: "zh-Hans" },
].map((preset) => (
<SelectItem key={preset.label} value={preset.value}>
{preset.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormItem>
);
}}
/>
<Button type="submit">{t("settings.common.save")}</Button>
</form>
</Form>
</CardContent>

View File

@@ -18,6 +18,7 @@ import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -51,6 +52,7 @@ const randomImages = [
export const ProfileForm = () => {
const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { t } = useTranslation("settings");
const form = useForm<Profile>({
defaultValues: {
@@ -91,10 +93,10 @@ export const ProfileForm = () => {
<Card className="bg-transparent">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl">Account</CardTitle>
<CardDescription>
Change the details of your profile here.
</CardDescription>
<CardTitle className="text-xl">
{t("settings.profile.title")}
</CardTitle>
<CardDescription>{t("settings.profile.description")}</CardDescription>
</div>
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
</CardHeader>
@@ -107,9 +109,12 @@ export const ProfileForm = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t("settings.profile.email")}</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
<Input
placeholder={t("settings.profile.email")}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -120,11 +125,11 @@ export const ProfileForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t("settings.profile.password")}</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
placeholder={t("settings.profile.password")}
{...field}
value={field.value || ""}
/>
@@ -139,7 +144,7 @@ export const ProfileForm = () => {
name="image"
render={({ field }) => (
<FormItem>
<FormLabel>Avatar</FormLabel>
<FormLabel>{t("settings.profile.avatar")}</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(e) => {
@@ -177,7 +182,7 @@ export const ProfileForm = () => {
</div>
<div>
<Button type="submit" isLoading={isLoading}>
Save
{t("settings.common.save")}
</Button>
</div>
</form>

View File

@@ -11,10 +11,13 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
import { GPUSupportModal } from "../gpu-support-modal";
export const ShowDokployActions = () => {
const { t } = useTranslation("settings");
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
@@ -22,11 +25,13 @@ export const ShowDokployActions = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
Server
{t("settings.server.webServer.server.label")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel>
{t("settings.server.webServer.actions")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
@@ -40,11 +45,12 @@ export const ShowDokployActions = () => {
});
}}
>
<span>Reload</span>
<span>{t("settings.server.webServer.reload")}</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy">
<span>Watch logs</span>
<span>{t("settings.server.webServer.watchLogs")}</span>
</ShowModalLogs>
<GPUSupportModal />
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -11,12 +11,14 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
interface Props {
serverId?: string;
}
export const ShowStorageActions = ({ serverId }: Props) => {
const { t } = useTranslation("settings");
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
@@ -64,11 +66,13 @@ export const ShowStorageActions = ({ serverId }: Props) => {
}
variant="outline"
>
Space
{t("settings.server.webServer.storage.label")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel>
{t("settings.server.webServer.actions")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
@@ -85,7 +89,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>Clean unused images</span>
<span>
{t("settings.server.webServer.storage.cleanUnusedImages")}
</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
@@ -101,7 +107,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>Clean unused volumes</span>
<span>
{t("settings.server.webServer.storage.cleanUnusedVolumes")}
</span>
</DropdownMenuItem>
<DropdownMenuItem
@@ -118,7 +126,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>Clean stopped containers</span>
<span>
{t("settings.server.webServer.storage.cleanStoppedContainers")}
</span>
</DropdownMenuItem>
<DropdownMenuItem
@@ -135,7 +145,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>Clean Docker Builder & System</span>
<span>
{t("settings.server.webServer.storage.cleanDockerBuilder")}
</span>
</DropdownMenuItem>
{!serverId && (
<DropdownMenuItem
@@ -150,7 +162,9 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>Clean Monitoring </span>
<span>
{t("settings.server.webServer.storage.cleanMonitoring")}
</span>
</DropdownMenuItem>
)}
@@ -168,7 +182,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>Clean all</span>
<span>{t("settings.server.webServer.storage.cleanAll")}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>

View File

@@ -23,6 +23,7 @@ import { api } from "@/utils/api";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { useTranslation } from "next-i18next";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
@@ -30,6 +31,7 @@ interface Props {
serverId?: string;
}
export const ShowTraefikActions = ({ serverId }: Props) => {
const { t } = useTranslation("settings");
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
@@ -51,11 +53,13 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
variant="outline"
>
Traefik
{t("settings.server.webServer.traefik.label")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuLabel>
{t("settings.server.webServer.actions")}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
@@ -71,17 +75,17 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
});
}}
>
<span>Reload</span>
<span>{t("settings.server.webServer.reload")}</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
<span>Watch logs</span>
<span>{t("settings.server.webServer.watchLogs")}</span>
</ShowModalLogs>
<EditTraefikEnv serverId={serverId}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
<span>{t("settings.server.webServer.traefik.modifyEnv")}</span>
</DropdownMenuItem>
</EditTraefikEnv>

View File

@@ -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>
);
};

View File

@@ -0,0 +1,282 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { TRPCClientError } from "@trpc/client";
import { CheckCircle2, Cpu, Loader2, RefreshCw, XCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface GPUSupportProps {
serverId?: string;
}
export function GPUSupport({ serverId }: GPUSupportProps) {
const [isLoading, setIsLoading] = useState(false);
const [isRefreshing, setIsRefreshing] = useState(false);
const utils = api.useContext();
const {
data: gpuStatus,
isLoading: isChecking,
refetch,
} = api.settings.checkGPUStatus.useQuery(
{ serverId },
{
enabled: serverId !== undefined,
},
);
const setupGPU = api.settings.setupGPU.useMutation({
onMutate: () => {
setIsLoading(true);
},
onSuccess: async () => {
toast.success("GPU support enabled successfully");
setIsLoading(false);
await utils.settings.checkGPUStatus.invalidate({ serverId });
},
onError: (error) => {
toast.error(
error.message ||
"Failed to enable GPU support. Please check server logs.",
);
setIsLoading(false);
},
});
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await utils.settings.checkGPUStatus.invalidate({ serverId });
await refetch();
} catch (error) {
toast.error("Failed to refresh GPU status");
} finally {
setIsRefreshing(false);
}
};
useEffect(() => {
handleRefresh();
}, []);
const handleEnableGPU = async () => {
if (serverId === undefined) {
toast.error("No server selected");
return;
}
try {
await setupGPU.mutateAsync({ serverId });
} catch (error) {
// Error handling is done in mutation's onError
}
};
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 items-end max-sm:flex-col">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Cpu className="size-5" />
<CardTitle className="text-xl">GPU Configuration</CardTitle>
</div>
<CardDescription>
Configure and monitor GPU support
</CardDescription>
</div>
<div className="flex items-center gap-2">
<DialogAction
title="Enable GPU Support?"
description="This will enable GPU support for Docker Swarm on this server. Make sure you have the required hardware and drivers installed."
onClick={handleEnableGPU}
>
<Button
isLoading={isLoading}
disabled={isLoading || serverId === undefined || isChecking}
>
{isLoading
? "Enabling GPU..."
: gpuStatus?.swarmEnabled
? "Reconfigure GPU"
: "Enable GPU"}
</Button>
</DialogAction>
<Button
size="icon"
onClick={handleRefresh}
disabled={isChecking || isRefreshing}
>
<RefreshCw
className={`h-5 w-5 ${isChecking || isRefreshing ? "animate-spin" : ""}`}
/>
</Button>
</div>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<AlertBlock type="info">
<div className="font-medium mb-2">System Requirements:</div>
<ul className="list-disc list-inside text-sm space-y-1">
<li>NVIDIA GPU hardware must be physically installed</li>
<li>
NVIDIA drivers must be installed and running (check with
nvidia-smi)
</li>
<li>
NVIDIA Container Runtime must be installed
(nvidia-container-runtime)
</li>
<li>User must have sudo/administrative privileges</li>
<li>System must support CUDA for GPU acceleration</li>
</ul>
</AlertBlock>
{isChecking ? (
<div className="flex items-center justify-center text-muted-foreground py-4">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Checking GPU status...</span>
</div>
) : (
<div className="grid gap-4">
{/* Prerequisites Section */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-semibold mb-1">Prerequisites</h3>
<p className="text-sm text-muted-foreground mb-4">
Shows all software checks and available hardware
</p>
<div className="grid gap-2.5">
<StatusRow
label="NVIDIA Driver"
isEnabled={gpuStatus?.driverInstalled}
description={
gpuStatus?.driverVersion
? `Installed (v${gpuStatus.driverVersion})`
: "Not Installed"
}
/>
<StatusRow
label="GPU Model"
value={gpuStatus?.gpuModel || "Not Detected"}
showIcon={false}
/>
<StatusRow
label="GPU Memory"
value={gpuStatus?.memoryInfo || "Not Available"}
showIcon={false}
/>
<StatusRow
label="Available GPUs"
value={gpuStatus?.availableGPUs || 0}
showIcon={false}
/>
<StatusRow
label="CUDA Support"
isEnabled={gpuStatus?.cudaSupport}
description={
gpuStatus?.cudaVersion
? `Available (v${gpuStatus.cudaVersion})`
: "Not Available"
}
/>
<StatusRow
label="NVIDIA Container Runtime"
isEnabled={gpuStatus?.runtimeInstalled}
description={
gpuStatus?.runtimeInstalled
? "Installed"
: "Not Installed"
}
/>
</div>
</div>
{/* Configuration Status */}
<div className="border rounded-lg p-4">
<h3 className="text-lg font-semibold mb-1">
Docker Swarm GPU Status
</h3>
<p className="text-sm text-muted-foreground mb-4">
Shows the configuration state that changes with the Enable
GPU
</p>
<div className="grid gap-2.5">
<StatusRow
label="Runtime Configuration"
isEnabled={gpuStatus?.runtimeConfigured}
description={
gpuStatus?.runtimeConfigured
? "Default Runtime"
: "Not Default Runtime"
}
/>
<StatusRow
label="Swarm GPU Support"
isEnabled={gpuStatus?.swarmEnabled}
description={
gpuStatus?.swarmEnabled
? `Enabled (${gpuStatus.gpuResources} GPU${gpuStatus.gpuResources !== 1 ? "s" : ""})`
: "Not Enabled"
}
/>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</CardContent>
);
}
interface StatusRowProps {
label: string;
isEnabled?: boolean;
description?: string;
value?: string | number;
showIcon?: boolean;
}
export function StatusRow({
label,
isEnabled,
description,
value,
showIcon = true,
}: StatusRowProps) {
return (
<div className="flex items-center justify-between">
<span className="text-sm">{label}</span>
<div className="flex items-center gap-2">
{showIcon ? (
<>
{isEnabled ? (
<CheckCircle2 className="size-4 text-green-500" />
) : (
<XCircle className="size-4 text-red-500" />
)}
<span
className={`text-sm ${isEnabled ? "text-green-500" : "text-red-500"}`}
>
{description || (isEnabled ? "Installed" : "Not Installed")}
</span>
</>
) : (
<span className="text-sm text-muted-foreground">{value}</span>
)}
</div>
</div>
);
}

View File

@@ -32,6 +32,7 @@ import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { ShowDeployment } from "../../application/deployments/show-deployment";
import { GPUSupport } from "./gpu-support";
interface Props {
serverId: string;
@@ -89,9 +90,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-2 w-[400px]">
<TabsList className="grid grid-cols-3 w-[400px]">
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
</TabsList>
<TabsContent
value="ssh-keys"
@@ -291,6 +293,14 @@ export const SetupServer = ({ serverId }: Props) => {
</div>
</CardContent>
</TabsContent>
<TabsContent
value="gpu-setup"
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">
<GPUSupport serverId={serverId} />
</div>
</TabsContent>
</Tabs>
</div>
)}

View File

@@ -24,6 +24,7 @@ import {
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -49,6 +50,7 @@ const addServerDomain = z
type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data: user, refetch } = api.admin.one.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
@@ -89,9 +91,11 @@ export const WebDomain = () => {
<div className="w-full">
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Server Domain</CardTitle>
<CardTitle className="text-xl">
{t("settings.server.domain.title")}
</CardTitle>
<CardDescription>
Add a domain to your server application.
{t("settings.server.domain.description")}
</CardDescription>
</CardHeader>
<CardContent className="flex w-full flex-col gap-4">
@@ -106,7 +110,9 @@ export const WebDomain = () => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Domain</FormLabel>
<FormLabel>
{t("settings.server.domain.form.domain")}
</FormLabel>
<FormControl>
<Input
className="w-full"
@@ -126,7 +132,9 @@ export const WebDomain = () => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>Letsencrypt Email</FormLabel>
<FormLabel>
{t("settings.server.domain.form.letsEncryptEmail")}
</FormLabel>
<FormControl>
<Input
className="w-full"
@@ -145,20 +153,32 @@ export const WebDomain = () => {
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>Certificate</FormLabel>
<FormLabel>
{t("settings.server.domain.form.certificate.label")}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
<SelectValue
placeholder={t(
"settings.server.domain.form.certificate.placeholder",
)}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>None</SelectItem>
<SelectItem value={"none"}>
{t(
"settings.server.domain.form.certificateOptions.none",
)}
</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
{t(
"settings.server.domain.form.certificateOptions.letsencrypt",
)}
</SelectItem>
</SelectContent>
</Select>
@@ -169,7 +189,7 @@ export const WebDomain = () => {
/>
<div>
<Button isLoading={isLoading} type="submit">
Save
{t("settings.common.save")}
</Button>
</div>
</form>

View File

@@ -7,6 +7,7 @@ import {
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { useTranslation } from "next-i18next";
import React from "react";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
@@ -18,6 +19,7 @@ interface Props {
className?: string;
}
export const WebServer = ({ className }: Props) => {
const { t } = useTranslation("settings");
const { data } = api.admin.one.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -25,8 +27,12 @@ export const WebServer = ({ className }: Props) => {
return (
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
<CardHeader>
<CardTitle className="text-xl">Web server settings</CardTitle>
<CardDescription>Reload or clean the web server.</CardDescription>
<CardTitle className="text-xl">
{t("settings.server.webServer.title")}
</CardTitle>
<CardDescription>
{t("settings.server.webServer.description")}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4 ">
<div className="grid md:grid-cols-2 gap-4">

View File

@@ -0,0 +1,10 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "zh-Hans"],
localeDetection: false,
},
fallbackLng: "en",
keySeparator: false,
};

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.11.1",
"version": "v0.11.2",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -84,13 +84,16 @@
"dotenv": "16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1",
"i18next": "^23.16.4",
"input-otp": "^1.2.4",
"js-cookie": "^3.0.5",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"lucia": "^3.0.1",
"lucide-react": "^0.312.0",
"nanoid": "3",
"next": "^15.0.1",
"next-i18next": "^15.3.1",
"next-themes": "^0.2.1",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
@@ -100,6 +103,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.3",
"react-i18next": "^15.1.0",
"recharts": "^2.12.7",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
@@ -119,6 +123,7 @@
"devDependencies": {
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/node": "^18.17.0",

View File

@@ -3,6 +3,7 @@ import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { api } from "@/utils/api";
import type { NextPage } from "next";
import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
@@ -27,6 +28,7 @@ const MyApp = ({
pageProps: { ...pageProps },
}: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<>
<style jsx global>{`
@@ -59,4 +61,21 @@ const MyApp = ({
);
};
export default api.withTRPC(MyApp);
export default api.withTRPC(
appWithTranslation(
MyApp,
// keep this in sync with next-i18next.config.js
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
// if one day every page is translated, we can safely remove this config.
{
i18n: {
defaultLocale: "en",
locales: ["en", "zh-Hans"],
localeDetection: false,
},
fallbackLng: "en",
keySeparator: false,
},
),
);

View File

@@ -88,7 +88,6 @@ export default async function handler(
.update(admins)
.set({
stripeSubscriptionId: newSubscription.id,
serversQuantity: 0,
stripeCustomerId: newSubscription.customer as string,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
@@ -121,12 +120,6 @@ export default async function handler(
}
case "customer.subscription.updated": {
const newSubscription = event.data.object as Stripe.Subscription;
await db
.update(admins)
.set({
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
const admin = await findAdminByStripeCustomerId(
newSubscription.customer as string,
@@ -136,8 +129,27 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found");
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
if (newSubscription.status === "active") {
await db
.update(admins)
.set({
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(
eq(admins.stripeCustomerId, newSubscription.customer as string),
);
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
} else {
await disableServers(admin.adminId);
await db
.update(admins)
.set({ serversQuantity: 0 })
.where(
eq(admins.stripeCustomerId, newSubscription.customer as string),
);
}
break;
}
@@ -148,6 +160,13 @@ export default async function handler(
newInvoice.subscription as string,
);
if (suscription.status !== "active") {
console.log(
`Skipping invoice.payment_succeeded for subscription ${suscription.id} with status ${suscription.status}`,
);
break;
}
await db
.update(admins)
.set({
@@ -168,22 +187,29 @@ export default async function handler(
}
case "invoice.payment_failed": {
const newInvoice = event.data.object as Stripe.Invoice;
await db
.update(admins)
.set({
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
const admin = await findAdminByStripeCustomerId(
newInvoice.customer as string,
const subscription = await stripe.subscriptions.retrieve(
newInvoice.subscription as string,
);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
if (subscription.status !== "active") {
const admin = await findAdminByStripeCustomerId(
newInvoice.customer as string,
);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
await db
.update(admins)
.set({
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
await disableServers(admin.adminId);
}
await disableServers(admin.adminId);
break;
}

View File

@@ -2,9 +2,11 @@ import { AppearanceForm } from "@/components/dashboard/settings/appearance-form"
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { getLocale } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import React, { type ReactElement } from "react";
import superjson from "superjson";
@@ -30,6 +32,7 @@ export async function getServerSideProps(
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
const locale = getLocale(req.cookies);
const helpers = createServerSideHelpers({
router: appRouter,
@@ -63,6 +66,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["settings"])),
},
};
}

View File

@@ -4,9 +4,11 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import React, { type ReactElement } from "react";
import superjson from "superjson";
@@ -41,6 +43,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = getLocale(req.cookies);
const { user, session } = await validateRequest(req, res);
const helpers = createServerSideHelpers({
@@ -75,6 +78,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["settings"])),
},
};
}

View File

@@ -3,9 +3,11 @@ import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { getLocale } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import React, { type ReactElement } from "react";
import superjson from "superjson";
@@ -31,6 +33,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const locale = await getLocale(req.cookies);
if (IS_CLOUD) {
return {
redirect: {
@@ -73,6 +76,7 @@ export async function getServerSideProps(
return {
props: {
trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["settings"])),
},
};
}

View File

@@ -199,7 +199,7 @@ export default function Home({ IS_CLOUD }: Props) {
) : (
<Link
className="hover:underline text-muted-foreground"
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
href="https://docs.dokploy.com/docs/core/reset-password"
target="_blank"
>
Lost your password?

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,43 @@
{
"settings.common.save": "Save",
"settings.server.domain.title": "Server Domain",
"settings.server.domain.description": "Add a domain to your server application.",
"settings.server.domain.form.domain": "Domain",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
"settings.server.domain.form.certificate.label": "Certificate",
"settings.server.domain.form.certificate.placeholder": "Select a certificate",
"settings.server.domain.form.certificateOptions.none": "None",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Default)",
"settings.server.webServer.title": "Web Server",
"settings.server.webServer.description": "Reload or clean the web server.",
"settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Reload",
"settings.server.webServer.watchLogs": "Watch logs",
"settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modify Env",
"settings.server.webServer.storage.label": "Space",
"settings.server.webServer.storage.cleanUnusedImages": "Clean unused images",
"settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes",
"settings.server.webServer.storage.cleanStoppedContainers": "Clean stopped containers",
"settings.server.webServer.storage.cleanDockerBuilder": "Clean Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring",
"settings.server.webServer.storage.cleanAll": "Clean all",
"settings.profile.title": "Account",
"settings.profile.description": "Change the details of your profile here.",
"settings.profile.email": "Email",
"settings.profile.password": "Password",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Appearance",
"settings.appearance.description": "Customize the theme of your dashboard.",
"settings.appearance.theme": "Theme",
"settings.appearance.themeDescription": "Select a theme for your dashboard",
"settings.appearance.themes.light": "Light",
"settings.appearance.themes.dark": "Dark",
"settings.appearance.themes.system": "System",
"settings.appearance.language": "Language",
"settings.appearance.languageDescription": "Select a language for your dashboard"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,40 @@
{
"settings.common.save": "保存",
"settings.server.domain.title": "服务器域名",
"settings.server.domain.description": "添加一个域名到您的服务器。",
"settings.server.domain.form.domain": "域名",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱",
"settings.server.domain.form.certificate.label": "证书",
"settings.server.domain.form.certificate.placeholder": "选择一个证书",
"settings.server.domain.form.certificateOptions.none": "无",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (默认)",
"settings.server.webServer.title": "Web 服务器",
"settings.server.webServer.description": "管理 Web 服务器。",
"settings.server.webServer.actions": "操作",
"settings.server.webServer.reload": "重新加载",
"settings.server.webServer.watchLogs": "查看日志",
"settings.server.webServer.server.label": "服务器",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "修改环境变量",
"settings.server.webServer.storage.label": "磁盘空间",
"settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像",
"settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷",
"settings.server.webServer.storage.cleanStoppedContainers": "清理停止的容器",
"settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统缓存",
"settings.server.webServer.storage.cleanMonitoring": "Clean Monitoring",
"settings.server.webServer.storage.cleanAll": "清理所有",
"settings.profile.title": "账户偏好",
"settings.profile.description": "更改您的个人资料详情。",
"settings.profile.email": "电子邮件",
"settings.profile.password": "密码",
"settings.profile.avatar": "头像",
"settings.appearance.title": "外观",
"settings.appearance.description": "自定义仪表板主题。",
"settings.appearance.theme": "主题",
"settings.appearance.themeDescription": "选择仪表板主题",
"settings.appearance.themes.light": "亮",
"settings.appearance.themes.dark": "暗",
"settings.appearance.themes.system": "系统",
"settings.appearance.language": "语言",
"settings.appearance.languageDescription": "选择仪表板语言"
}

View File

@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="11.567343mm"
height="15.032981mm"
viewBox="0 0 11.567343 15.03298"
version="1.1"
id="svg8"
sodipodi:docname="community_logo_black.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#c8c8c8"
bordercolor="#666666"
borderopacity="1.0"
showgrid="false"
showguides="true"
borderlayer="true"
fit-margin-top="1"
fit-margin-left="1"
fit-margin-right="1"
fit-margin-bottom="1"/>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
transform="translate(-115.93625,-150.07138)">
<g
transform="translate(-3.8788837,214.53487)"
id="g1369">
<path
style="opacity:1;fill:#000000;fill-opacity:0.07058824;stroke:none;stroke-width:0.31555739;stroke-miterlimit:1.41420996;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
d="m 121.59341,-62.933898 c -0.43151,0 -0.77882,0.347312 -0.77882,0.778817 v 7.918777 c 0,0.04214 0.004,0.08316 0.0106,0.12345 7.5e-4,0.0053 10e-4,0.01041 0.002,0.01567 0.001,0.0073 0.002,0.01466 0.004,0.02186 0.10284,0.693169 0.73757,1.119278 2.19888,2.190555 2.64127,1.936306 2.45943,1.935512 5.11716,0.02186 1.68877,-1.215962 2.28048,-1.590346 2.23197,-2.501308 v -7.790874 c 0,-0.431505 -0.34751,-0.778817 -0.77902,-0.778817 z"
id="path1373"/>
<path
id="path1323"
d="m 121.59341,-63.463065 c -0.43151,0 -0.77882,0.347312 -0.77882,0.778817 v 7.918777 c 0,0.04214 0.004,0.08316 0.0106,0.12345 7.5e-4,0.0053 10e-4,0.01041 0.002,0.01567 0.001,0.0073 0.002,0.01466 0.004,0.02186 0.10284,0.693169 0.73757,1.119278 2.19888,2.190555 2.64127,1.936306 2.45943,1.935512 5.11716,0.02186 1.68877,-1.215962 2.28048,-1.590346 2.23197,-2.501308 v -7.790874 c 0,-0.431505 -0.34751,-0.778817 -0.77902,-0.778817 z"
style="opacity:1;fill:#363636;fill-opacity:1;stroke:none;stroke-width:0.31555739;stroke-miterlimit:1.41420996;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" />
<g
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996"
id="g1353"
transform="matrix(0.02054188,0,0,0.02054188,97.15326,-61.563495)">
<g
id="g1327"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<path
id="path1325"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
d="m 364.467,-333.746 c 0.171,-1.908 1.646,-3.118 3.899,-3.118 2.256,0 3.73,1.21 3.901,3.118 z m 7.569,4.711 c -0.577,1.414 -1.937,2.251 -3.784,2.251 -2.313,0 -3.87,-1.444 -3.933,-3.725 h 13.297 c 0,-0.237 0,-0.435 0,-0.671 0,-5.714 -3.354,-8.925 -9.364,-8.925 -5.836,0 -9.365,3.241 -9.365,8.324 0,5.114 3.584,8.35 9.365,8.35 3.469,0 6.159,-1.189 7.817,-3.279 z"/>
</g>
<g
id="g1331"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<path
id="path1329"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
d="m 305.468,-333.737 c 0.176,-1.908 1.651,-3.118 3.906,-3.118 2.252,0 3.726,1.21 3.899,3.118 z m 7.574,4.711 c -0.578,1.418 -1.937,2.255 -3.788,2.255 -2.309,0 -3.87,-1.448 -3.931,-3.73 h 13.294 c 0,-0.234 0,-0.431 0,-0.667 0,-5.717 -3.353,-8.929 -9.363,-8.929 -5.839,0 -9.361,3.242 -9.361,8.325 0,5.114 3.582,8.35 9.361,8.35 3.468,0 6.16,-1.185 7.821,-3.278 z"/>
</g>
<g
id="g1335"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<rect
id="rect1333"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
height="19.617001"
width="4.7950001"
y="-343.56"
x="293.90701" />
</g>
<g
id="g1339"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<path
id="path1337"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
d="m 319.81,-338.348 h 4.822 v 1.168 c 1.707,-1.822 3.757,-2.743 6.069,-2.743 2.663,0 4.679,0.921 5.72,2.489 0.869,1.295 0.926,2.858 0.926,4.912 v 8.579 h -4.829 v -7.538 c 0,-3.128 -0.629,-4.572 -3.375,-4.572 -2.775,0 -4.511,1.653 -4.511,4.428 v 7.682 h -4.822 z"/>
</g>
<g
id="g1343"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<path
id="path1341"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
d="m 352.876,-331.538 c 0,2.685 -1.794,4.446 -4.57,4.446 -2.778,0 -4.572,-1.701 -4.572,-4.415 0,-2.754 1.77,-4.454 4.572,-4.454 2.776,0 4.57,1.73 4.57,4.423 z m 0,-6.157 c -1.219,-1.307 -2.983,-2.024 -5.435,-2.024 -5.29,0 -8.902,3.262 -8.902,8.151 0,4.793 3.587,8.146 8.815,8.146 2.397,0 4.157,-0.606 5.522,-1.965 v 1.444 h 4.825 v -20.861 l -4.825,1.244 z"/>
</g>
<g
id="g1347"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<path
id="path1345"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
d="m 282.947,-335.961 c 2.804,0 4.567,1.7 4.567,4.454 0,2.714 -1.791,4.415 -4.567,4.415 -2.774,0 -4.566,-1.761 -4.566,-4.446 0,-2.693 1.792,-4.423 4.566,-4.423 z m -4.566,-7.599 -4.827,-1.244 v 20.861 h 4.827 v -1.444 c 1.358,1.359 3.121,1.965 5.52,1.965 5.231,0 8.813,-3.353 8.813,-8.146 0,-4.889 -3.613,-8.151 -8.9,-8.151 -2.457,0 -4.22,0.717 -5.433,2.024 z"/>
</g>
<g
id="g1351"
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
style="clip-rule:evenodd;fill:#d8d8d8;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996">
<path
id="path1349"
style="fill:#d8d8d8;fill-opacity:1;fill-rule:nonzero"
d="m 378.806,-323.943 v -14.405 h 4.825 v 0.89 c 1.445,-1.74 2.974,-2.606 4.713,-2.606 0.345,0 0.779,0.056 1.356,0.113 v 4.107 c -0.465,-0.061 -0.983,-0.061 -1.533,-0.061 -2.805,0 -4.536,1.85 -4.536,4.996 v 6.966 z"/>
</g>
</g>
<g
transform="matrix(0.04039667,0,0,0.04039667,81.604348,-55.892386)"
style="clip-rule:evenodd;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41420996"
id="g1367">
<g
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
id="g1361"
style="fill:#ffffff;fill-opacity:1">
<path
d="m 243.13,-333.715 c 0.106,-1.891 1.032,-3.557 2.429,-4.738 1.37,-1.16 3.214,-1.869 5.226,-1.869 2.01,0 3.854,0.709 5.225,1.869 1.396,1.181 2.322,2.847 2.429,4.736 0.106,1.943 -0.675,3.748 -2.045,5.086 -1.397,1.361 -3.384,2.215 -5.609,2.215 -2.225,0 -4.216,-0.854 -5.612,-2.215 -1.371,-1.338 -2.15,-3.143 -2.043,-5.084 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero"
id="path1359" />
</g>
<g
transform="matrix(3.3451117,0,0,3.3451075,277.7359,1100.2048)"
id="g1365"
style="fill:#ffffff;fill-opacity:1">
<path
d="m 230.94,-329.894 c 0.013,0.74 0.249,2.178 0.603,3.301 0.744,2.377 2.006,4.576 3.762,6.514 1.802,1.992 4.021,3.592 6.584,4.728 2.694,1.193 5.613,1.801 8.645,1.796 3.027,-0.004 5.946,-0.624 8.64,-1.826 2.563,-1.147 4.78,-2.754 6.579,-4.747 1.755,-1.946 3.015,-4.149 3.761,-6.526 0.375,-1.201 0.612,-2.42 0.707,-3.643 0.093,-1.205 0.054,-2.412 -0.117,-3.618 -0.334,-2.35 -1.147,-4.555 -2.399,-6.565 -1.145,-1.847 -2.621,-3.464 -4.376,-4.825 l 0.004,-0.003 -17.711,-13.599 c -0.016,-0.012 -0.029,-0.025 -0.046,-0.036 -1.162,-0.892 -3.116,-0.889 -4.394,0.005 -1.292,0.904 -1.44,2.399 -0.29,3.342 l -0.005,0.005 7.387,6.007 -22.515,0.024 c -0.011,0 -0.022,0 -0.03,0 -1.861,0.002 -3.65,1.223 -4.004,2.766 -0.364,1.572 0.9,2.876 2.835,2.883 l -0.003,0.007 11.412,-0.022 -20.364,15.631 c -0.026,0.019 -0.054,0.039 -0.078,0.058 -1.921,1.471 -2.542,3.917 -1.332,5.465 1.228,1.574 3.839,1.577 5.78,0.009 l 11.114,-9.096 c 0,0 -0.162,1.228 -0.149,1.965 z m 28.559,4.112 c -2.29,2.333 -5.496,3.656 -8.965,3.663 -3.474,0.006 -6.68,-1.305 -8.97,-3.634 -1.119,-1.135 -1.941,-2.441 -2.448,-3.832 -0.497,-1.367 -0.69,-2.818 -0.562,-4.282 0.121,-1.431 0.547,-2.796 1.227,-4.031 0.668,-1.214 1.588,-2.311 2.724,-3.239 2.226,-1.814 5.06,-2.796 8.024,-2.8 2.967,-0.004 5.799,0.969 8.027,2.777 1.134,0.924 2.053,2.017 2.721,3.229 0.683,1.234 1.106,2.594 1.232,4.029 0.126,1.462 -0.067,2.911 -0.564,4.279 -0.508,1.395 -1.327,2.701 -2.446,3.841 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero"
id="path1363" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1 @@
<svg width="256" height="128" version="1.1" viewBox="0 0 256 128" xmlns="http://www.w3.org/2000/svg"><path d="m128 7c-25.871 0-47.817 17.485-54.713 41.209-5.9795-12.461-18.642-21.209-33.287-21.209-20.304 0-37 16.696-37 37s16.696 37 37 37c14.645 0 27.308-8.7481 33.287-21.209 6.8957 23.724 28.842 41.209 54.713 41.209s47.817-17.485 54.713-41.209c5.9795 12.461 18.642 21.209 33.287 21.209 20.304 0 37-16.696 37-37s-16.696-37-37-37c-14.645 0-27.308 8.7481-33.287 21.209-6.8957-23.724-28.842-41.209-54.713-41.209zm0 22c19.46 0 35 15.54 35 35s-15.54 35-35 35-35-15.54-35-35 15.54-35 35-35zm-88 20c8.4146 0 15 6.5854 15 15s-6.5854 15-15 15-15-6.5854-15-15 6.5854-15 15-15zm176 0c8.4146 0 15 6.5854 15 15s-6.5854 15-15 15-15-6.5854-15-15 6.5854-15 15-15z" color="#000000" fill="#fff" style="-inkscape-stroke:none"/></svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@@ -52,6 +52,7 @@ import {
writeMainConfig,
writeTraefikConfigInPath,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
@@ -657,6 +658,54 @@ export const settingsRouter = createTRPCRouter({
}
return { status: "not_cloud" };
}),
setupGPU: adminProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD && !input.serverId) {
throw new Error("Select a server to enable the GPU Setup");
}
try {
await setupGPUSupport(input.serverId);
return { success: true };
} catch (error) {
console.error("GPU Setup Error:", error);
throw error;
}
}),
checkGPUStatus: adminProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
if (IS_CLOUD && !input.serverId) {
return {
driverInstalled: false,
driverVersion: undefined,
gpuModel: undefined,
runtimeInstalled: false,
runtimeConfigured: false,
cudaSupport: undefined,
cudaVersion: undefined,
memoryInfo: undefined,
availableGPUs: 0,
swarmEnabled: false,
gpuResources: 0,
};
}
try {
return await checkGPUStatus(input.serverId || "");
} catch (error) {
throw new Error("Failed to check GPU status");
}
}),
});
// {
// "Parallelism": 1,

View File

@@ -21,7 +21,6 @@ import {
import type { Session, User } from "lucia";
import superjson from "superjson";
import { ZodError } from "zod";
/**
* 1. CONTEXT
*

View File

@@ -0,0 +1,26 @@
version: "3.8"
services:
blender:
image: lscr.io/linuxserver/blender:latest
runtime: nvidia
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities:
- gpu
environment:
- NVIDIA_VISIBLE_DEVICES=all
- NVIDIA_DRIVER_CAPABILITIES=all
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- SUBFOLDER=/ #optional
ports:
- 3000
- 3001
restart: unless-stopped
shm_size: 1gb

View File

@@ -0,0 +1,34 @@
import {
type DomainSchema,
type Schema,
type Template,
generateHash,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainServiceHash = generateHash(schema.projectName);
const mainDomain = generateRandomDomain(schema);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 3000,
serviceName: "blender",
},
];
const envs = [
"PUID=1000",
"PGID=1000",
"TZ=Etc/UTC",
"SUBFOLDER=/",
"NVIDIA_VISIBLE_DEVICES=all",
"NVIDIA_DRIVER_CAPABILITIES=all",
];
return {
envs,
domains,
};
}

View File

@@ -0,0 +1,38 @@
services:
nextcloud:
image: nextcloud:30.0.2
restart: always
networks:
- dokploy-network
ports:
- 80
volumes:
- nextcloud_data:/var/www/html
environment:
- NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN}
- MYSQL_HOST=nextcloud_db
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=${MYSQL_SECRET_PASSWORD}
- OVERWRITEPROTOCOL=https
nextcloud_db:
image: mariadb
restart: always
networks:
- dokploy-network
volumes:
- nextcloud_db_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_SECRET_PASSWORD_ROOT}
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_PASSWORD=${MYSQL_SECRET_PASSWORD}
volumes:
nextcloud_data:
nextcloud_db_data:
networks:
dokploy-network:
external: true

View File

@@ -0,0 +1,28 @@
import {
type DomainSchema,
type Schema,
type Template,
generatePassword,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const randomDomain = generateRandomDomain(schema);
const databasePassword = generatePassword();
const databaseRootPassword = generatePassword();
const envs = [
`NEXTCLOUD_DOMAIN=${randomDomain}`,
`MYSQL_SECRET_PASSWORD=${databasePassword}`,
`MYSQL_SECRET_PASSWORD_ROOT=${databaseRootPassword}`,
];
const domains: DomainSchema[] = [
{
host: randomDomain,
port: 80,
serviceName: "nextcloud",
},
];
return { envs, domains };
}

View File

@@ -799,5 +799,35 @@ export const templates: TemplateData[] = [
},
tags: ["discord", "tickets", "support"],
load: () => import("./discord-tickets/index").then((m) => m.generate),
},
{
id: "nextcloud-aio",
name: "Nextcloud All in One",
version: "30.0.2",
description:
"Nextcloud (AIO) is a self-hosted file storage and sync platform with powerful collaboration capabilities. It integrates Files, Talk, Groupware, Office, Assistant and more into a single platform for remote work and data protection.",
logo: "nextcloud-aio.svg",
links: {
github: "https://github.com/nextcloud/docker",
website: "https://nextcloud.com/",
docs: "https://docs.nextcloud.com/",
},
tags: ["file", "sync"],
load: () => import("./nextcloud-aio/index").then((m) => m.generate),
},
{
id: "blender",
name: "Blender",
version: "latest",
description:
"Blender is a free and open-source 3D creation suite. It supports the entire 3D pipeline—modeling, rigging, animation, simulation, rendering, compositing and motion tracking, video editing and 2D animation pipeline.",
logo: "blender.svg",
links: {
github: "https://github.com/linuxserver/docker-blender",
website: "https://www.blender.org/",
docs: "https://docs.blender.org/",
},
tags: ["3d", "rendering", "animation"],
load: () => import("./blender/index").then((m) => m.generate),
},
];

View File

@@ -0,0 +1,19 @@
import Cookies from "js-cookie";
const SUPPORTED_LOCALES = ["en", "zh-Hans"] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number];
export default function useLocale() {
const currentLocale = (Cookies.get("DOKPLOY_LOCALE") ?? "en") as Locale;
const setLocale = (locale: Locale) => {
Cookies.set("DOKPLOY_LOCALE", locale);
window.location.reload();
};
return {
locale: currentLocale,
setLocale,
};
}

View File

@@ -0,0 +1,6 @@
import type { NextApiRequestCookies } from "next/dist/server/api-utils";
export function getLocale(cookies: NextApiRequestCookies) {
const locale = cookies.DOKPLOY_LOCALE ?? "en";
return locale;
}

View File

@@ -116,3 +116,4 @@ export * from "./monitoring/utilts";
export * from "./db/validations/domain";
export * from "./db/validations/index";
export * from "./utils/gpu-setup";

View File

@@ -0,0 +1,349 @@
import * as fs from "node:fs/promises";
import { execAsync, sleep } from "../utils/process/execAsync";
import { execAsyncRemote } from "../utils/process/execAsync";
interface GPUInfo {
driverInstalled: boolean;
driverVersion?: string;
gpuModel?: string;
runtimeInstalled: boolean;
runtimeConfigured: boolean;
cudaSupport: boolean;
cudaVersion?: string;
memoryInfo?: string;
availableGPUs: number;
swarmEnabled: boolean;
gpuResources: number;
}
export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
try {
const [driverInfo, runtimeInfo, swarmInfo, gpuInfo, cudaInfo] =
await Promise.all([
checkGpuDriver(serverId),
checkRuntime(serverId),
checkSwarmResources(serverId),
checkGpuInfo(serverId),
checkCudaSupport(serverId),
]);
return {
...driverInfo,
...runtimeInfo,
...swarmInfo,
...gpuInfo,
...cudaInfo,
};
} catch (error) {
console.error("Error in checkGPUStatus:", error);
return {
driverInstalled: false,
driverVersion: undefined,
runtimeInstalled: false,
runtimeConfigured: false,
cudaSupport: false,
cudaVersion: undefined,
gpuModel: undefined,
memoryInfo: undefined,
availableGPUs: 0,
swarmEnabled: false,
gpuResources: 0,
};
}
}
const checkGpuDriver = async (serverId?: string) => {
let driverVersion: string | undefined;
let driverInstalled = false;
let availableGPUs = 0;
try {
const driverCommand =
"nvidia-smi --query-gpu=driver_version --format=csv,noheader";
const { stdout: nvidiaSmi } = serverId
? await execAsyncRemote(serverId, driverCommand)
: await execAsync(driverCommand);
driverVersion = nvidiaSmi.trim();
if (driverVersion) {
driverInstalled = true;
const countCommand =
"nvidia-smi --query-gpu=gpu_name --format=csv,noheader | wc -l";
const { stdout: gpuCount } = serverId
? await execAsyncRemote(serverId, countCommand)
: await execAsync(countCommand);
availableGPUs = Number.parseInt(gpuCount.trim(), 10);
}
} catch (error) {
console.debug("GPU driver check:", error);
}
return { driverVersion, driverInstalled, availableGPUs };
};
const checkRuntime = async (serverId?: string) => {
let runtimeInstalled = false;
let runtimeConfigured = false;
try {
// First check: Is nvidia-container-runtime installed?
const checkBinaryCommand = "command -v nvidia-container-runtime";
try {
const { stdout } = serverId
? await execAsyncRemote(serverId, checkBinaryCommand)
: await execAsync(checkBinaryCommand);
runtimeInstalled = !!stdout.trim();
} catch (error) {
console.debug("Runtime binary check:", error);
}
// Second check: Is it configured in Docker?
try {
const runtimeCommand = 'docker info --format "{{json .Runtimes}}"';
const { stdout: runtimeInfo } = serverId
? await execAsyncRemote(serverId, runtimeCommand)
: await execAsync(runtimeCommand);
const defaultCommand = 'docker info --format "{{.DefaultRuntime}}"';
const { stdout: defaultRuntime } = serverId
? await execAsyncRemote(serverId, defaultCommand)
: await execAsync(defaultCommand);
const runtimes = JSON.parse(runtimeInfo);
const hasNvidiaRuntime = "nvidia" in runtimes;
const isDefaultRuntime = defaultRuntime.trim() === "nvidia";
// Only set runtimeConfigured if both conditions are met
runtimeConfigured = hasNvidiaRuntime && isDefaultRuntime;
} catch (error) {
console.debug("Runtime configuration check:", error);
}
} catch (error) {
console.debug("Runtime check:", error);
}
return { runtimeInstalled, runtimeConfigured };
};
const checkSwarmResources = async (serverId?: string) => {
let swarmEnabled = false;
let gpuResources = 0;
try {
const nodeCommand =
"docker node inspect self --format '{{json .Description.Resources.GenericResources}}'";
const { stdout: resources } = serverId
? await execAsyncRemote(serverId, nodeCommand)
: await execAsync(nodeCommand);
if (resources && resources !== "null") {
const genericResources = JSON.parse(resources);
for (const resource of genericResources) {
if (
resource.DiscreteResourceSpec &&
(resource.DiscreteResourceSpec.Kind === "GPU" ||
resource.DiscreteResourceSpec.Kind === "gpu")
) {
gpuResources = resource.DiscreteResourceSpec.Value;
swarmEnabled = true;
break;
}
}
}
} catch (error) {
console.debug("Swarm resource check:", error);
}
return { swarmEnabled, gpuResources };
};
const checkGpuInfo = async (serverId?: string) => {
let gpuModel: string | undefined;
let memoryInfo: string | undefined;
try {
const gpuInfoCommand =
"nvidia-smi --query-gpu=gpu_name,memory.total --format=csv,noheader";
const { stdout: gpuInfo } = serverId
? await execAsyncRemote(serverId, gpuInfoCommand)
: await execAsync(gpuInfoCommand);
[gpuModel, memoryInfo] = gpuInfo.split(",").map((s) => s.trim());
} catch (error) {
console.debug("GPU info check:", error);
}
return { gpuModel, memoryInfo };
};
const checkCudaSupport = async (serverId?: string) => {
let cudaVersion: string | undefined;
let cudaSupport = false;
try {
const cudaCommand = 'nvidia-smi -q | grep "CUDA Version"';
const { stdout: cudaInfo } = serverId
? await execAsyncRemote(serverId, cudaCommand)
: await execAsync(cudaCommand);
const cudaMatch = cudaInfo.match(/CUDA Version\s*:\s*([\d\.]+)/);
cudaVersion = cudaMatch ? cudaMatch[1] : undefined;
cudaSupport = !!cudaVersion;
} catch (error) {
console.debug("CUDA support check:", error);
}
return { cudaVersion, cudaSupport };
};
export async function setupGPUSupport(serverId?: string): Promise<void> {
try {
// 1. Initial status check and validation
const initialStatus = await checkGPUStatus(serverId);
const shouldContinue = await validatePrerequisites(initialStatus);
if (!shouldContinue) return;
// 2. Get node ID
const nodeId = await getNodeId(serverId);
// 3. Create daemon configuration
const daemonConfig = createDaemonConfig(initialStatus.availableGPUs);
// 4. Setup server based on environment
if (serverId) {
await setupRemoteServer(serverId, daemonConfig);
} else {
await setupLocalServer(daemonConfig);
}
// 5. Wait for Docker restart
await sleep(10000);
// 6. Add GPU label
await addGpuLabel(nodeId, serverId);
// 7. Final verification
await sleep(5000);
await verifySetup(nodeId, serverId);
} catch (error) {
if (
error instanceof Error &&
error.message.includes("password is required")
) {
throw new Error(
"Sudo access required. Please run with appropriate permissions.",
);
}
throw error;
}
}
const validatePrerequisites = async (initialStatus: GPUInfo) => {
if (!initialStatus.driverInstalled) {
throw new Error(
"NVIDIA drivers not installed. Please install appropriate NVIDIA drivers first.",
);
}
if (!initialStatus.runtimeInstalled) {
throw new Error(
"NVIDIA Container Runtime not installed. Please install nvidia-container-runtime first.",
);
}
if (initialStatus.swarmEnabled && initialStatus.runtimeConfigured) {
return false;
}
return true;
};
const getNodeId = async (serverId?: string) => {
const nodeIdCommand = 'docker info --format "{{.Swarm.NodeID}}"';
const { stdout: nodeId } = serverId
? await execAsyncRemote(serverId, nodeIdCommand)
: await execAsync(nodeIdCommand);
const trimmedNodeId = nodeId.trim();
if (!trimmedNodeId) {
throw new Error("Setup Server before enabling GPU support");
}
return trimmedNodeId;
};
const createDaemonConfig = (availableGPUs: number) => ({
runtimes: {
nvidia: {
path: "nvidia-container-runtime",
runtimeArgs: [],
},
},
"default-runtime": "nvidia",
"node-generic-resources": [`GPU=${availableGPUs}`],
});
const setupRemoteServer = async (serverId: string, daemonConfig: any) => {
const setupCommands = [
"sudo -n true",
`echo '${JSON.stringify(daemonConfig, null, 2)}' | sudo tee /etc/docker/daemon.json`,
"sudo mkdir -p /etc/nvidia-container-runtime",
'sudo sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml',
'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);
};
const setupLocalServer = async (daemonConfig: any) => {
const configFile = `/tmp/docker-daemon-${Date.now()}.json`;
await fs.writeFile(configFile, JSON.stringify(daemonConfig, null, 2));
const setupCommands = [
`pkexec sh -c '
cp ${configFile} /etc/docker/daemon.json &&
mkdir -p /etc/nvidia-container-runtime &&
sed -i "/swarm-resource/d" /etc/nvidia-container-runtime/config.toml &&
echo "swarm-resource = \\"DOCKER_RESOURCE_GPU\\"" >> /etc/nvidia-container-runtime/config.toml &&
systemctl daemon-reload &&
systemctl restart docker
'`,
`rm ${configFile}`,
].join(" && ");
await execAsync(setupCommands);
};
const addGpuLabel = async (nodeId: string, serverId?: string) => {
const labelCommand = `docker node update --label-add gpu=true ${nodeId}`;
if (serverId) {
await execAsyncRemote(serverId, labelCommand);
} else {
await execAsync(labelCommand);
}
};
const verifySetup = async (nodeId: string, serverId?: string) => {
const finalStatus = await checkGPUStatus(serverId);
if (!finalStatus.swarmEnabled) {
const diagnosticCommands = [
`docker node inspect ${nodeId}`,
'nvidia-smi -a | grep "GPU UUID"',
"cat /etc/docker/daemon.json",
"cat /etc/nvidia-container-runtime/config.toml",
].join(" && ");
const { stdout: diagnostics } = serverId
? await execAsyncRemote(serverId, diagnosticCommands)
: await execAsync(diagnosticCommands);
console.error("Diagnostic Information:", diagnostics);
throw new Error("GPU support not detected in swarm after setup");
}
return finalStatus;
};

108
pnpm-lock.yaml generated
View File

@@ -247,9 +247,15 @@ importers:
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8)
i18next:
specifier: ^23.16.4
version: 23.16.4
input-otp:
specifier: ^1.2.4
version: 1.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
js-cookie:
specifier: ^3.0.5
version: 3.0.5
js-yaml:
specifier: 4.1.0
version: 4.1.0
@@ -268,6 +274,9 @@ importers:
next:
specifier: ^15.0.1
version: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-i18next:
specifier: ^15.3.1
version: 15.3.1(i18next@23.16.4)(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
next-themes:
specifier: ^0.2.1
version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -295,6 +304,9 @@ importers:
react-hook-form:
specifier: ^7.49.3
version: 7.52.1(react@18.2.0)
react-i18next:
specifier: ^15.1.0
version: 15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
recharts:
specifier: ^2.12.7
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -347,6 +359,9 @@ importers:
'@types/bcrypt':
specifier: 5.0.2
version: 5.0.2
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
'@types/js-yaml':
specifier: 4.0.9
version: 4.0.9
@@ -3206,12 +3221,18 @@ packages:
'@types/hast@2.3.10':
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
'@types/hoist-non-react-statics@3.3.5':
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
'@types/http-cache-semantics@4.0.4':
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
'@types/http-errors@2.0.4':
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
'@types/js-cookie@3.0.6':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
@@ -3893,6 +3914,9 @@ packages:
core-js-pure@3.38.1:
resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==}
core-js@3.39.0:
resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
cosmiconfig-typescript-loader@5.0.0:
resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==}
engines: {node: '>=v16'}
@@ -4662,10 +4686,16 @@ packages:
highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
hono@4.5.8:
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
engines: {node: '>=16.0.0'}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
@@ -4701,6 +4731,12 @@ packages:
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
engines: {node: '>=10.18'}
i18next-fs-backend@2.3.2:
resolution: {integrity: sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==}
i18next@23.16.4:
resolution: {integrity: sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -5247,6 +5283,15 @@ packages:
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
engines: {node: '>= 10'}
next-i18next@15.3.1:
resolution: {integrity: sha512-+pa2pZJb7B6k5PKW3TLVMmAodqkNaOBWVYlpWX56mgcEJz0UMW+MKSdKM9Z72CHp6Bp48g7OWwDnLqxXNp/84w==}
engines: {node: '>=14'}
peerDependencies:
i18next: '>= 23.7.13'
next: '>= 12.0.0'
react: '>= 17.0.2'
react-i18next: '>= 13.5.0'
next-themes@0.2.1:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies:
@@ -5726,6 +5771,19 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@15.1.0:
resolution: {integrity: sha512-zj3nJynMnZsy2gPZiOTC7XctCY5eQGqT3tcKMmfJWC9FMvgd+960w/adq61j8iPzpwmsXejqID9qC3Mqu1Xu2Q==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
react-immutable-proptypes@2.2.0:
resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==}
peerDependencies:
@@ -6570,6 +6628,10 @@ packages:
jsdom:
optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -9282,10 +9344,17 @@ snapshots:
dependencies:
'@types/unist': 2.0.10
'@types/hoist-non-react-statics@3.3.5':
dependencies:
'@types/react': 18.3.5
hoist-non-react-statics: 3.3.2
'@types/http-cache-semantics@4.0.4': {}
'@types/http-errors@2.0.4': {}
'@types/js-cookie@3.0.6': {}
'@types/js-yaml@4.0.9': {}
'@types/json-schema@7.0.15': {}
@@ -10056,6 +10125,8 @@ snapshots:
core-js-pure@3.38.1: {}
core-js@3.39.0: {}
cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3):
dependencies:
'@types/node': 18.19.42
@@ -10820,8 +10891,16 @@ snapshots:
highlight.js@10.7.3: {}
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
hono@4.5.8: {}
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
html-to-text@9.0.5:
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
@@ -10865,6 +10944,12 @@ snapshots:
hyperdyperid@1.2.0: {}
i18next-fs-backend@2.3.2: {}
i18next@23.16.4:
dependencies:
'@babel/runtime': 7.25.0
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -11365,6 +11450,18 @@ snapshots:
neotraverse@0.6.18: {}
next-i18next@15.3.1(i18next@23.16.4)(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.25.0
'@types/hoist-non-react-statics': 3.3.5
core-js: 3.39.0
hoist-non-react-statics: 3.3.2
i18next: 23.16.4
i18next-fs-backend: 2.3.2
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-i18next: 15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes@0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -11854,6 +11951,15 @@ snapshots:
dependencies:
react: 18.2.0
react-i18next@15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.25.0
html-parse-stringify: 3.0.1
i18next: 23.16.4
react: 18.2.0
optionalDependencies:
react-dom: 18.2.0(react@18.2.0)
react-immutable-proptypes@2.2.0(immutable@3.8.2):
dependencies:
immutable: 3.8.2
@@ -12791,6 +12897,8 @@ snapshots:
- supports-color
- terser
void-elements@3.1.0: {}
w3c-keyname@2.2.8: {}
watchpack@2.4.1: