diff --git a/.circleci/config.yml b/.circleci/config.yml index 07991599..f47a561a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 05256d57..c9d1049c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 + ``` [optional scope]: @@ -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. diff --git a/apps/dokploy/components/dashboard/settings/appearance-form.tsx b/apps/dokploy/components/dashboard/settings/appearance-form.tsx index 52142fcd..a10b0d05 100644 --- a/apps/dokploy/components/dashboard/settings/appearance-form.tsx +++ b/apps/dokploy/components/dashboard/settings/appearance-form.tsx @@ -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; @@ -35,10 +47,14 @@ type AppearanceFormValues = z.infer; // This can come from your database or API. const defaultValues: Partial = { theme: "system", + language: "en", }; export function AppearanceForm() { const { setTheme, theme } = useTheme(); + const { locale, setLocale } = useLocale(); + const { t } = useTranslation("settings"); + const form = useForm({ 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 ( - Appearance + + {t("settings.appearance.title")} + - Customize the theme of your dashboard. + {t("settings.appearance.description")} @@ -72,9 +92,9 @@ export function AppearanceForm() { render={({ field }) => { return ( - Theme + {t("settings.appearance.theme")} - Select a theme for your dashboard + {t("settings.appearance.themeDescription")} - Light + {t("settings.appearance.themes.light")} @@ -105,7 +125,7 @@ export function AppearanceForm() { dark - Dark + {t("settings.appearance.themes.dark")} @@ -121,7 +141,7 @@ export function AppearanceForm() { system - System + {t("settings.appearance.themes.system")} @@ -131,7 +151,43 @@ export function AppearanceForm() { }} /> - + { + return ( + + {t("settings.appearance.language")} + + {t("settings.appearance.languageDescription")} + + + + + ); + }} + /> + + diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 3c8d51bb..e90eb5e5 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -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({ defaultValues: { @@ -91,10 +93,10 @@ export const ProfileForm = () => {
- Account - - Change the details of your profile here. - + + {t("settings.profile.title")} + + {t("settings.profile.description")}
{!data?.is2FAEnabled ? : }
@@ -107,9 +109,12 @@ export const ProfileForm = () => { name="email" render={({ field }) => ( - Email + {t("settings.profile.email")} - + @@ -120,11 +125,11 @@ export const ProfileForm = () => { name="password" render={({ field }) => ( - Password + {t("settings.profile.password")} @@ -139,7 +144,7 @@ export const ProfileForm = () => { name="image" render={({ field }) => ( - Avatar + {t("settings.profile.avatar")} { @@ -177,7 +182,7 @@ export const ProfileForm = () => {
diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx index 9c30c13f..43392846 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-dokploy-actions.tsx @@ -12,10 +12,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(); @@ -23,11 +26,13 @@ export const ShowDokployActions = () => { - Actions + + {t("settings.server.webServer.actions")} + { }} className="cursor-pointer" > - Reload + {t("settings.server.webServer.reload")} e.preventDefault()} > - Watch Logs + {t("settings.server.webServer.watchLogs")} + { + 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")} - Actions + + {t("settings.server.webServer.actions")} + { }); }} > - Clean Unused Images + + {t("settings.server.webServer.storage.cleanUnusedImages")} + { }); }} > - Clean Unused Volumes + + {t("settings.server.webServer.storage.cleanUnusedVolumes")} + { }); }} > - Clean Stopped Containers + + {t("settings.server.webServer.storage.cleanStoppedContainers")} + { }); }} > - Clean Docker Builder & System + + {t("settings.server.webServer.storage.cleanDockerBuilder")} + {!serverId && ( { }); }} > - Clean Monitoring + + {t("settings.server.webServer.storage.cleanMonitoring")} + )} @@ -168,7 +182,7 @@ export const ShowStorageActions = ({ serverId }: Props) => { }); }} > - Clean All + {t("settings.server.webServer.storage.cleanAll")} diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index d37e3aba..4385dc6a 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -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")} - Actions + + {t("settings.server.webServer.actions")} + { }} className="cursor-pointer" > - Reload + {t("settings.server.webServer.reload")} e.preventDefault()} className="cursor-pointer" > - Watch Logs + {t("settings.server.webServer.watchLogs")} @@ -87,7 +91,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => { onSelect={(e) => e.preventDefault()} className="cursor-pointer" > - Modify Env + {t("settings.server.webServer.traefik.modifyEnv")} diff --git a/apps/dokploy/components/dashboard/settings/servers/gpu-support-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/gpu-support-modal.tsx new file mode 100644 index 00000000..9cf858cd --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/gpu-support-modal.tsx @@ -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 ( + + + e.preventDefault()} + > + GPU Setup + + + + + + Dokploy Server GPU Setup + + + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx b/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx new file mode 100644 index 00000000..b398fe74 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/gpu-support.tsx @@ -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 ( + +
+ + +
+
+
+ + GPU Configuration +
+ + Configure and monitor GPU support + +
+
+ + + + +
+
+
+ + + +
System Requirements:
+
    +
  • NVIDIA GPU hardware must be physically installed
  • +
  • + NVIDIA drivers must be installed and running (check with + nvidia-smi) +
  • +
  • + NVIDIA Container Runtime must be installed + (nvidia-container-runtime) +
  • +
  • User must have sudo/administrative privileges
  • +
  • System must support CUDA for GPU acceleration
  • +
+
+ + {isChecking ? ( +
+ + Checking GPU status... +
+ ) : ( +
+ {/* Prerequisites Section */} +
+

Prerequisites

+

+ Shows all software checks and available hardware +

+
+ + + + + + +
+
+ + {/* Configuration Status */} +
+

+ Docker Swarm GPU Status +

+

+ Shows the configuration state that changes with the Enable + GPU +

+
+ + +
+
+
+ )} +
+
+
+
+ ); +} + +interface StatusRowProps { + label: string; + isEnabled?: boolean; + description?: string; + value?: string | number; + showIcon?: boolean; +} + +export function StatusRow({ + label, + isEnabled, + description, + value, + showIcon = true, +}: StatusRowProps) { + return ( +
+ {label} +
+ {showIcon ? ( + <> + {isEnabled ? ( + + ) : ( + + )} + + {description || (isEnabled ? "Installed" : "Not Installed")} + + + ) : ( + {value} + )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 8bfcf4da..119d4d29 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -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) => { ) : (
- + SSH Keys Deployments + GPU Setup {
+ +
+ +
+
)} diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index 354f1158..00f54904 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -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; 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 = () => {
- Server Domain + + {t("settings.server.domain.title")} + - Add a domain to your server application. + {t("settings.server.domain.description")} @@ -106,7 +110,9 @@ export const WebDomain = () => { render={({ field }) => { return ( - Domain + + {t("settings.server.domain.form.domain")} + { render={({ field }) => { return ( - Letsencrypt Email + + {t("settings.server.domain.form.letsEncryptEmail")} + { render={({ field }) => { return ( - Certificate + + {t("settings.server.domain.form.certificate.label")} + @@ -169,7 +189,7 @@ export const WebDomain = () => { />
diff --git a/apps/dokploy/components/dashboard/settings/web-server.tsx b/apps/dokploy/components/dashboard/settings/web-server.tsx index 188b3db8..2ac88d9a 100644 --- a/apps/dokploy/components/dashboard/settings/web-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server.tsx @@ -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 ( - Web server settings - Reload or clean the web server. + + {t("settings.server.webServer.title")} + + + {t("settings.server.webServer.description")} +
diff --git a/apps/dokploy/next-i18next.config.js b/apps/dokploy/next-i18next.config.js new file mode 100644 index 00000000..5c20bbea --- /dev/null +++ b/apps/dokploy/next-i18next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next-i18next').UserConfig} */ +module.exports = { + i18n: { + defaultLocale: "en", + locales: ["en", "zh-Hans"], + localeDetection: false, + }, + fallbackLng: "en", + keySeparator: false, +}; diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 61db1ff9..8bba5e4a 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -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", diff --git a/apps/dokploy/pages/_app.tsx b/apps/dokploy/pages/_app.tsx index 41b4d50b..b5fcb131 100644 --- a/apps/dokploy/pages/_app.tsx +++ b/apps/dokploy/pages/_app.tsx @@ -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 ( <> + + + + + + + + + diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index e5c34b11..7c777b17 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -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, diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index d37315c3..db4f7adf 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -21,7 +21,6 @@ import { import type { Session, User } from "lucia"; import superjson from "superjson"; import { ZodError } from "zod"; - /** * 1. CONTEXT * diff --git a/apps/dokploy/templates/activepieces/docker-compose.yml b/apps/dokploy/templates/activepieces/docker-compose.yml new file mode 100644 index 00000000..e990379b --- /dev/null +++ b/apps/dokploy/templates/activepieces/docker-compose.yml @@ -0,0 +1,67 @@ +version: "3.8" + +services: + activepieces: + image: activepieces/activepieces:0.35.0 + restart: unless-stopped + networks: + - dokploy-network + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + AP_ENGINE_EXECUTABLE_PATH: dist/packages/engine/main.js + AP_API_KEY: ${AP_API_KEY} + AP_ENCRYPTION_KEY: ${AP_ENCRYPTION_KEY} + AP_JWT_SECRET: ${AP_JWT_SECRET} + AP_ENVIRONMENT: prod + AP_FRONTEND_URL: https://${AP_HOST} + AP_WEBHOOK_TIMEOUT_SECONDS: 30 + AP_TRIGGER_DEFAULT_POLL_INTERVAL: 5 + AP_POSTGRES_DATABASE: activepieces + AP_POSTGRES_HOST: postgres + AP_POSTGRES_PORT: 5432 + AP_POSTGRES_USERNAME: activepieces + AP_POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD} + AP_EXECUTION_MODE: UNSANDBOXED + AP_REDIS_HOST: redis + AP_REDIS_PORT: 6379 + AP_SANDBOX_RUN_TIME_SECONDS: 600 + AP_TELEMETRY_ENABLED: "false" + AP_TEMPLATES_SOURCE_URL: https://cloud.activepieces.com/api/v1/flow-templates + + postgres: + image: postgres:14 + restart: unless-stopped + networks: + - dokploy-network + environment: + POSTGRES_DB: activepieces + POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD} + POSTGRES_USER: activepieces + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U activepieces -d activepieces"] + interval: 30s + timeout: 30s + retries: 3 + + redis: + image: redis:7 + restart: unless-stopped + networks: + - dokploy-network + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 30s + retries: 3 + +volumes: + postgres_data: + redis_data: \ No newline at end of file diff --git a/apps/dokploy/templates/activepieces/index.ts b/apps/dokploy/templates/activepieces/index.ts new file mode 100644 index 00000000..f1d97ccc --- /dev/null +++ b/apps/dokploy/templates/activepieces/index.ts @@ -0,0 +1,44 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const apiKey = Array.from({ length: 32 }, () => + Math.floor(Math.random() * 16).toString(16), + ).join(""); + const encryptionKey = Array.from({ length: 32 }, () => + Math.floor(Math.random() * 16).toString(16), + ).join(""); + const jwtSecret = Array.from({ length: 32 }, () => + Math.floor(Math.random() * 16).toString(16), + ).join(""); + const postgresPassword = Array.from({ length: 32 }, () => + Math.floor(Math.random() * 16).toString(16), + ).join(""); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "activepieces", + }, + ]; + + const envs = [ + `AP_HOST=${mainDomain}`, + `AP_API_KEY=${apiKey}`, + `AP_ENCRYPTION_KEY=${encryptionKey}`, + `AP_JWT_SECRET=${jwtSecret}`, + `AP_POSTGRES_PASSWORD=${postgresPassword}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/blender/docker-compose.yml b/apps/dokploy/templates/blender/docker-compose.yml new file mode 100644 index 00000000..893f3dee --- /dev/null +++ b/apps/dokploy/templates/blender/docker-compose.yml @@ -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 diff --git a/apps/dokploy/templates/blender/index.ts b/apps/dokploy/templates/blender/index.ts new file mode 100644 index 00000000..84e52755 --- /dev/null +++ b/apps/dokploy/templates/blender/index.ts @@ -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, + }; +} diff --git a/apps/dokploy/templates/discord-tickets/docker-compose.yml b/apps/dokploy/templates/discord-tickets/docker-compose.yml new file mode 100644 index 00000000..44324e1e --- /dev/null +++ b/apps/dokploy/templates/discord-tickets/docker-compose.yml @@ -0,0 +1,54 @@ +version: "3.8" + +services: + mysql: + image: mysql:8 + restart: unless-stopped + hostname: mysql + networks: + - dokploy-network + volumes: + - tickets-mysql:/var/lib/mysql + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_USER: ${MYSQL_USER} + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 5 + + bot: + image: eartharoid/discord-tickets:4.0.21 + depends_on: + mysql: + condition: service_healthy + restart: unless-stopped + hostname: bot + networks: + - dokploy-network + volumes: + - tickets-bot:/home/container/user + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + tty: true + stdin_open: true + environment: + DB_CONNECTION_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql/${MYSQL_DATABASE} + DISCORD_SECRET: ${DISCORD_SECRET} + DISCORD_TOKEN: ${DISCORD_TOKEN} + ENCRYPTION_KEY: ${ENCRYPTION_KEY} + DB_PROVIDER: mysql + HTTP_EXTERNAL: https://${TICKETS_HOST} + HTTP_HOST: 0.0.0.0 + HTTP_PORT: 8169 + HTTP_TRUST_PROXY: "true" + PUBLIC_BOT: "false" + PUBLISH_COMMANDS: "true" + SUPER: ${SUPER_USERS} + +volumes: + tickets-mysql: + tickets-bot: \ No newline at end of file diff --git a/apps/dokploy/templates/discord-tickets/index.ts b/apps/dokploy/templates/discord-tickets/index.ts new file mode 100644 index 00000000..48b40ed7 --- /dev/null +++ b/apps/dokploy/templates/discord-tickets/index.ts @@ -0,0 +1,47 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const mysqlPassword = generatePassword(); + const mysqlRootPassword = generatePassword(); + const mysqlUser = "tickets"; + const mysqlDatabase = "tickets"; + + // Generate encryption key in the format they use + const encryptionKey = Array.from({ length: 48 }, () => + Math.floor(Math.random() * 16).toString(16), + ).join(""); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8169, + serviceName: "bot", + }, + ]; + + const envs = [ + `TICKETS_HOST=${mainDomain}`, + `MYSQL_DATABASE=${mysqlDatabase}`, + `MYSQL_PASSWORD=${mysqlPassword}`, + `MYSQL_ROOT_PASSWORD=${mysqlRootPassword}`, + `MYSQL_USER=${mysqlUser}`, + `ENCRYPTION_KEY=${encryptionKey}`, + // These need to be set by the user through the UI + "# Follow the guide at: https://discordtickets.app/self-hosting/installation/docker/#creating-the-discord-application", + "DISCORD_SECRET=", + "DISCORD_TOKEN=", + "SUPER_USERS=YOUR_DISCORD_USER_ID", // Default super user + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/invoiceshelf/docker-compose.yml b/apps/dokploy/templates/invoiceshelf/docker-compose.yml new file mode 100644 index 00000000..ac4c8d26 --- /dev/null +++ b/apps/dokploy/templates/invoiceshelf/docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.8" + +services: + invoiceshelf_db: + image: postgres:15 + networks: + - dokploy-network + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_DB=${DB_DATABASE} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"] + interval: 10s + timeout: 5s + retries: 5 + + invoiceshelf: + image: invoiceshelf/invoiceshelf:latest + networks: + - dokploy-network + volumes: + - app_data:/data + - app_conf:/conf + environment: + - PHP_TZ=UTC + - TIMEZONE=UTC + - APP_NAME=InvoiceShelf + - APP_ENV=production + - APP_DEBUG=false + - APP_URL=http://${INVOICESHELF_HOST} + - DB_CONNECTION=pgsql + - DB_HOST=invoiceshelf_db + - DB_PORT=5432 + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - CACHE_STORE=file + - SESSION_DRIVER=file + - SESSION_LIFETIME=120 + - SESSION_ENCRYPT=true + - SESSION_PATH=/ + - SESSION_DOMAIN=${INVOICESHELF_HOST} + - SANCTUM_STATEFUL_DOMAINS=${INVOICESHELF_HOST} + - STARTUP_DELAY=10 + depends_on: + invoiceshelf_db: + condition: service_healthy + +volumes: + postgres_data: + app_data: + app_conf: \ No newline at end of file diff --git a/apps/dokploy/templates/invoiceshelf/index.ts b/apps/dokploy/templates/invoiceshelf/index.ts new file mode 100644 index 00000000..d8ae8f63 --- /dev/null +++ b/apps/dokploy/templates/invoiceshelf/index.ts @@ -0,0 +1,34 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const dbPassword = generatePassword(); + const dbUsername = "invoiceshelf"; + const dbDatabase = "invoiceshelf"; + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "invoiceshelf", + }, + ]; + + const envs = [ + `INVOICESHELF_HOST=${mainDomain}`, + `DB_PASSWORD=${dbPassword}`, + `DB_USERNAME=${dbUsername}`, + `DB_DATABASE=${dbDatabase}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/nextcloud-aio/docker-compose.yml b/apps/dokploy/templates/nextcloud-aio/docker-compose.yml new file mode 100644 index 00000000..e8381d2b --- /dev/null +++ b/apps/dokploy/templates/nextcloud-aio/docker-compose.yml @@ -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 diff --git a/apps/dokploy/templates/nextcloud-aio/index.ts b/apps/dokploy/templates/nextcloud-aio/index.ts new file mode 100644 index 00000000..f6bfc893 --- /dev/null +++ b/apps/dokploy/templates/nextcloud-aio/index.ts @@ -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 }; +} diff --git a/apps/dokploy/templates/peppermint/docker-compose.yml b/apps/dokploy/templates/peppermint/docker-compose.yml new file mode 100644 index 00000000..a20bedf4 --- /dev/null +++ b/apps/dokploy/templates/peppermint/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.8" + +services: + peppermint_postgres: + image: postgres:latest + restart: always + networks: + - dokploy-network + volumes: + - pgdata:/var/lib/postgresql/data + environment: + POSTGRES_USER: peppermint + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: peppermint + healthcheck: + test: ["CMD-SHELL", "pg_isready -U peppermint"] + interval: 10s + timeout: 5s + retries: 5 + + peppermint: + image: pepperlabs/peppermint:latest + restart: always + networks: + - dokploy-network + depends_on: + peppermint_postgres: + condition: service_healthy + environment: + DB_USERNAME: "peppermint" + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_HOST: "peppermint_postgres" + SECRET: ${SECRET} + +volumes: + pgdata: \ No newline at end of file diff --git a/apps/dokploy/templates/peppermint/index.ts b/apps/dokploy/templates/peppermint/index.ts new file mode 100644 index 00000000..2b71d490 --- /dev/null +++ b/apps/dokploy/templates/peppermint/index.ts @@ -0,0 +1,43 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + // Generate domains and secrets + const mainDomain = generateRandomDomain(schema); + const apiDomain = generateRandomDomain(schema); + const postgresPassword = generatePassword(); + const secret = generateBase64(32); + + // Configure domain routing + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "peppermint", + }, + { + host: apiDomain, + port: 5003, + serviceName: "peppermint", + }, + ]; + + // Set environment variables + const envs = [ + `MAIN_DOMAIN=${mainDomain}`, + `API_DOMAIN=${apiDomain}`, + `POSTGRES_PASSWORD=${postgresPassword}`, + `SECRET=${secret}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/postiz/docker-compose.yml b/apps/dokploy/templates/postiz/docker-compose.yml new file mode 100644 index 00000000..42a4b976 --- /dev/null +++ b/apps/dokploy/templates/postiz/docker-compose.yml @@ -0,0 +1,64 @@ +version: "3.8" + +services: + postiz: + image: ghcr.io/gitroomhq/postiz-app:latest + restart: always + networks: + - dokploy-network + environment: + MAIN_URL: "https://${POSTIZ_HOST}" + FRONTEND_URL: "https://${POSTIZ_HOST}" + NEXT_PUBLIC_BACKEND_URL: "https://${POSTIZ_HOST}/api" + JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: "postgresql://${DB_USER}:${DB_PASSWORD}@postiz-postgres:5432/${DB_NAME}" + REDIS_URL: "redis://postiz-redis:6379" + BACKEND_INTERNAL_URL: "http://localhost:3000" + IS_GENERAL: "true" + STORAGE_PROVIDER: "local" + UPLOAD_DIRECTORY: "/uploads" + NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads" + volumes: + - postiz-config:/config/ + - postiz-uploads:/uploads/ + depends_on: + postiz-postgres: + condition: service_healthy + postiz-redis: + condition: service_healthy + + postiz-postgres: + image: postgres:17-alpine + restart: always + networks: + - dokploy-network + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_USER: ${DB_USER} + POSTGRES_DB: ${DB_NAME} + volumes: + - postgres-volume:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U ${DB_USER} -d ${DB_NAME} + interval: 10s + timeout: 3s + retries: 3 + + postiz-redis: + image: redis:7.2 + restart: always + networks: + - dokploy-network + healthcheck: + test: redis-cli ping + interval: 10s + timeout: 3s + retries: 3 + volumes: + - postiz-redis-data:/data + +volumes: + postgres-volume: + postiz-redis-data: + postiz-config: + postiz-uploads: \ No newline at end of file diff --git a/apps/dokploy/templates/postiz/index.ts b/apps/dokploy/templates/postiz/index.ts new file mode 100644 index 00000000..d9eef415 --- /dev/null +++ b/apps/dokploy/templates/postiz/index.ts @@ -0,0 +1,37 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const dbPassword = generatePassword(); + const dbUser = "postiz"; + const dbName = "postiz"; + const jwtSecret = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 5000, + serviceName: "postiz", + }, + ]; + + const envs = [ + `POSTIZ_HOST=${mainDomain}`, + `DB_PASSWORD=${dbPassword}`, + `DB_USER=${dbUser}`, + `DB_NAME=${dbName}`, + `JWT_SECRET=${jwtSecret}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/slash/docker-compose.yml b/apps/dokploy/templates/slash/docker-compose.yml new file mode 100644 index 00000000..fce3114e --- /dev/null +++ b/apps/dokploy/templates/slash/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3.8" + +services: + slash: + image: yourselfhosted/slash:latest + networks: + - dokploy-network + volumes: + - slash_data:/var/opt/slash + environment: + - SLASH_DRIVER=postgres + - SLASH_DSN=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}?sslmode=disable + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:16-alpine + networks: + - dokploy-network + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=${DB_NAME} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + slash_data: + postgres_data: \ No newline at end of file diff --git a/apps/dokploy/templates/slash/index.ts b/apps/dokploy/templates/slash/index.ts new file mode 100644 index 00000000..4e05f74d --- /dev/null +++ b/apps/dokploy/templates/slash/index.ts @@ -0,0 +1,33 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const dbPassword = generatePassword(); + const dbUser = "slash"; + const dbName = "slash"; + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 5231, + serviceName: "slash", + }, + ]; + + const envs = [ + `DB_USER=${dbUser}`, + `DB_PASSWORD=${dbPassword}`, + `DB_NAME=${dbName}`, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 28af7c56..40f5e736 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -702,4 +702,139 @@ export const templates: TemplateData[] = [ tags: ["IA", "chat"], load: () => import("./lobe-chat/index").then((m) => m.generate), }, + { + id: "peppermint", + name: "Peppermint", + version: "latest", + description: + "Peppermint is a modern, open-source API development platform that helps you build, test and document your APIs.", + logo: "peppermint.svg", + links: { + github: "https://github.com/Peppermint-Lab/peppermint", + website: "https://peppermint.sh/", + docs: "https://docs.peppermint.sh/", + }, + tags: ["api", "development", "documentation"], + load: () => import("./peppermint/index").then((m) => m.generate), + }, + { + id: "windmill", + name: "Windmill", + version: "latest", + description: + "A developer platform to build production-grade workflows and internal apps. Open-source alternative to Airplane, Retool, and GitHub Actions.", + logo: "windmill.svg", + links: { + github: "https://github.com/windmill-labs/windmill", + website: "https://www.windmill.dev/", + docs: "https://docs.windmill.dev/", + }, + tags: ["workflow", "automation", "development"], + load: () => import("./windmill/index").then((m) => m.generate), + }, + { + id: "activepieces", + name: "Activepieces", + version: "0.35.0", + description: + "Open-source no-code business automation tool. An alternative to Zapier, Make.com, and Tray.", + logo: "activepieces.svg", + links: { + github: "https://github.com/activepieces/activepieces", + website: "https://www.activepieces.com/", + docs: "https://www.activepieces.com/docs", + }, + tags: ["automation", "workflow", "no-code"], + load: () => import("./activepieces/index").then((m) => m.generate), + }, + { + id: "invoiceshelf", + name: "InvoiceShelf", + version: "latest", + description: + "InvoiceShelf is a self-hosted open source invoicing system for freelancers and small businesses.", + logo: "invoiceshelf.png", + links: { + github: "https://github.com/InvoiceShelf/invoiceshelf", + website: "https://invoiceshelf.com", + docs: "https://github.com/InvoiceShelf/invoiceshelf#readme", + }, + tags: ["invoice", "business", "finance"], + load: () => import("./invoiceshelf/index").then((m) => m.generate), + }, + { + id: "postiz", + name: "Postiz", + version: "latest", + description: + "Postiz is a modern, open-source platform for managing and publishing content across multiple channels.", + logo: "postiz.png", + links: { + github: "https://github.com/gitroomhq/postiz", + website: "https://postiz.io", + docs: "https://docs.postiz.io", + }, + tags: ["cms", "content-management", "publishing"], + load: () => import("./postiz/index").then((m) => m.generate), + }, + { + id: "slash", + name: "Slash", + version: "latest", + description: + "Slash is a modern, self-hosted bookmarking service and link shortener that helps you organize and share your favorite links.", + logo: "slash.png", + links: { + github: "https://github.com/yourselfhosted/slash", + website: "https://github.com/yourselfhosted/slash#readme", + docs: "https://github.com/yourselfhosted/slash/wiki", + }, + tags: ["bookmarks", "link-shortener", "self-hosted"], + load: () => import("./slash/index").then((m) => m.generate), + }, + { + id: "discord-tickets", + name: "Discord Tickets", + version: "4.0.21", + description: + "An open-source Discord bot for creating and managing support ticket channels.", + logo: "discord-tickets.png", + links: { + github: "https://github.com/discord-tickets/bot", + website: "https://discordtickets.app", + docs: "https://discordtickets.app/self-hosting/installation/docker/", + }, + 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), + }, ]; diff --git a/apps/dokploy/templates/windmill/docker-compose.yml b/apps/dokploy/templates/windmill/docker-compose.yml new file mode 100644 index 00000000..5646c47a --- /dev/null +++ b/apps/dokploy/templates/windmill/docker-compose.yml @@ -0,0 +1,107 @@ +version: "3.8" + +services: + db: + image: postgres:16 + shm_size: 1g + restart: unless-stopped + volumes: + - db_data:/var/lib/postgresql/data + networks: + - dokploy-network + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: windmill + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + + windmill_server: + image: ghcr.io/windmill-labs/windmill:main + networks: + - dokploy-network + restart: unless-stopped + environment: + - DATABASE_URL=${DATABASE_URL} + - MODE=server + - BASE_URL=http://${WINDMILL_HOST} + depends_on: + db: + condition: service_healthy + volumes: + - worker_logs:/tmp/windmill/logs + + windmill_worker: + image: ghcr.io/windmill-labs/windmill:main + deploy: + replicas: 3 + resources: + limits: + cpus: "1" + memory: 2048M + restart: unless-stopped + networks: + - dokploy-network + environment: + - DATABASE_URL=${DATABASE_URL} + - MODE=worker + - WORKER_GROUP=default + depends_on: + db: + condition: service_healthy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - worker_dependency_cache:/tmp/windmill/cache + - worker_logs:/tmp/windmill/logs + + windmill_worker_native: + image: ghcr.io/windmill-labs/windmill:main + deploy: + replicas: 1 + resources: + limits: + cpus: "0.1" + memory: 128M + restart: unless-stopped + networks: + - dokploy-network + environment: + - DATABASE_URL=${DATABASE_URL} + - MODE=worker + - WORKER_GROUP=native + - NUM_WORKERS=8 + - SLEEP_QUEUE=200 + depends_on: + db: + condition: service_healthy + volumes: + - worker_logs:/tmp/windmill/logs + + lsp: + image: ghcr.io/windmill-labs/windmill-lsp:latest + restart: unless-stopped + networks: + - dokploy-network + volumes: + - lsp_cache:/root/.cache + + caddy: + image: ghcr.io/windmill-labs/caddy-l4:latest + restart: unless-stopped + networks: + - dokploy-network + volumes: + - ../files/Caddyfile:/etc/caddy/Caddyfile + environment: + - BASE_URL=":80" + depends_on: + - windmill_server + - lsp + +volumes: + db_data: + worker_dependency_cache: + worker_logs: + lsp_cache: \ No newline at end of file diff --git a/apps/dokploy/templates/windmill/index.ts b/apps/dokploy/templates/windmill/index.ts new file mode 100644 index 00000000..5d6aae3c --- /dev/null +++ b/apps/dokploy/templates/windmill/index.ts @@ -0,0 +1,43 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const postgresPassword = generatePassword(); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "caddy", + }, + ]; + + const envs = [ + `WINDMILL_HOST=${mainDomain}`, + `POSTGRES_PASSWORD=${postgresPassword}`, + `DATABASE_URL=postgres://postgres:${postgresPassword}@db/windmill?sslmode=disable`, + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "Caddyfile", + content: `:80 { + bind 0.0.0.0 + reverse_proxy /ws/* http://lsp:3001 + reverse_proxy /* http://windmill_server:8000 +}`, + }, + ]; + + return { + domains, + envs, + mounts, + }; +} diff --git a/apps/dokploy/utils/hooks/use-locale.ts b/apps/dokploy/utils/hooks/use-locale.ts new file mode 100644 index 00000000..f00e0df8 --- /dev/null +++ b/apps/dokploy/utils/hooks/use-locale.ts @@ -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, + }; +} diff --git a/apps/dokploy/utils/i18n.ts b/apps/dokploy/utils/i18n.ts new file mode 100644 index 00000000..0c165c4e --- /dev/null +++ b/apps/dokploy/utils/i18n.ts @@ -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; +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index abddc405..12f3b64e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -116,3 +116,4 @@ export * from "./monitoring/utilts"; export * from "./db/validations/domain"; export * from "./db/validations/index"; +export * from "./utils/gpu-setup"; diff --git a/packages/server/src/utils/gpu-setup.ts b/packages/server/src/utils/gpu-setup.ts new file mode 100644 index 00000000..ce60adf1 --- /dev/null +++ b/packages/server/src/utils/gpu-setup.ts @@ -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 { + 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 { + 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; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1048c92a..9dd089aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: