diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx new file mode 100644 index 000000000..5b6e04154 --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx @@ -0,0 +1,241 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { AlertBlock } from "@/components/shared/alert-block"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; + +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; + +interface Props { + composeId: string; +} + +// Schema for Isolated Deployment +const isolatedSchema = z.object({ + isolatedDeployment: z.boolean().optional(), +}); + +type IsolatedSchema = z.infer; + +export const IsolatedDeploymentTab = ({ composeId }: Props) => { + const utils = api.useUtils(); + const [compose, setCompose] = useState(""); + const [isPreviewLoading, setIsPreviewLoading] = useState(false); + const { mutateAsync, error, isError } = + api.compose.isolatedDeployment.useMutation(); + + const [isOpenPreview, setIsOpenPreview] = useState(false); + + const { mutateAsync: updateCompose } = api.compose.update.useMutation(); + + const { data, refetch } = api.compose.one.useQuery( + { composeId }, + { enabled: !!composeId }, + ); + + const form = useForm({ + defaultValues: { + isolatedDeployment: false, + }, + resolver: zodResolver(isolatedSchema), + }); + + useEffect(() => { + if (data) { + form.reset({ + isolatedDeployment: data?.isolatedDeployment || false, + }); + } + }, [form, form.reset, form.formState.isSubmitSuccessful, data]); + + const onSubmit = async (formData: IsolatedSchema) => { + await updateCompose({ + composeId, + isolatedDeployment: formData?.isolatedDeployment || false, + }) + .then(async (_data) => { + await refetch(); + toast.success("Compose updated"); + }) + .catch(() => { + toast.error("Error updating the compose"); + }); + }; + + const generatePreview = async () => { + setIsOpenPreview(true); + setIsPreviewLoading(true); + try { + await mutateAsync({ + composeId, + suffix: data?.appName || "", + }).then(async (data) => { + await utils.project.all.invalidate(); + setCompose(data); + }); + } catch { + toast.error("Error generating preview"); + setIsOpenPreview(false); + } finally { + setIsPreviewLoading(false); + } + }; + + return ( + + + Enable Isolated Deployment + + Configure isolated deployment to the compose file. +
+ + This feature creates an isolated environment for your deployment + by adding unique prefixes to all resources. It establishes a + dedicated network based on your compose file's name, ensuring your + services run in isolation. This prevents conflicts when running + multiple instances of the same template or services with identical + names. + +
+
+

+ Resources that will be isolated: +

+
    +
  • Docker networks
  • +
+
+
+
+
+
+ +
+ {isError && {error?.message}} +
+ + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+
+ ( + +
+ + Enable Isolated Deployment ({data?.appName}) + + + Enable isolated deployment to the compose file. + +
+ + + +
+ )} + /> +
+ +
+ +
+
+ +
+ + + + + Isolated Deployment Preview + + Preview of the compose file with isolated deployment + configuration + + +
+ {isPreviewLoading ? ( +
+ +

+ Generating compose preview... +

+
+ ) : ( +
+													
+												
+ )} +
+
+
+
+
+ +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index 41e40efbe..c2db472d2 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -1,3 +1,8 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { @@ -8,13 +13,7 @@ import { FormMessage, } from "@/components/ui/form"; import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config"; -import { ShowUtilities } from "./show-utilities"; interface Props { composeId: string; @@ -142,9 +141,7 @@ services:
-
- -
+
-
- -
- -
-							
-						
-
- - - - ); -}; diff --git a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx index 253a5fde3..fac6c2a34 100644 --- a/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show-converted-compose.tsx @@ -62,7 +62,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => { {isError && {error?.message}} - + Preview your docker-compose file with added domains. Note: At least one domain must be specified for this conversion to take effect. diff --git a/apps/dokploy/components/dashboard/compose/general/show-utilities.tsx b/apps/dokploy/components/dashboard/compose/general/show-utilities.tsx deleted file mode 100644 index 6df800494..000000000 --- a/apps/dokploy/components/dashboard/compose/general/show-utilities.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useState } from "react"; -import { IsolatedDeployment } from "./isolated-deployment"; -import { RandomizeCompose } from "./randomize-compose"; - -interface Props { - composeId: string; -} - -export const ShowUtilities = ({ composeId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - return ( - - - - - - - Utilities - Modify the application data - - - - Isolated Deployment - Randomize Compose - - - - - - - - - - - ); -}; diff --git a/apps/dokploy/hooks/use-keyboard-nav.tsx b/apps/dokploy/hooks/use-keyboard-nav.tsx index 318f06914..d24064c31 100644 --- a/apps/dokploy/hooks/use-keyboard-nav.tsx +++ b/apps/dokploy/hooks/use-keyboard-nav.tsx @@ -56,7 +56,7 @@ export function UseKeyboardNavForApplications() { setModPressed(false); if (key in SHORTCUTS) { - const tab = SHORTCUTS[key]; + const tab = SHORTCUTS[key as keyof typeof SHORTCUTS]; router.push( `${pathname}?${updateSearchParam("tab", tab.toLowerCase())}`, ); diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index dd5383697..fe229aa5a 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -1,3 +1,17 @@ +import { validateRequest } from "@dokploy/server/lib/auth"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import copy from "copy-to-clipboard"; +import { CircuitBoard, HelpCircle, ServerOff } from "lucide-react"; +import type { + GetServerSidePropsContext, + InferGetServerSidePropsType, +} from "next"; +import Head from "next/head"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { type ReactElement, useEffect, useState } from "react"; +import { toast } from "sonner"; +import superjson from "superjson"; import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import"; import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes"; import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments"; @@ -6,6 +20,7 @@ import { ShowEnvironment } from "@/components/dashboard/application/environment/ import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups"; import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command"; +import { IsolatedDeploymentTab } from "@/components/dashboard/compose/advanced/add-isolation"; import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; @@ -35,21 +50,6 @@ import { } from "@/components/ui/tooltip"; import { appRouter } from "@/server/api/root"; import { api } from "@/utils/api"; -import { validateRequest } from "@dokploy/server/lib/auth"; -import { createServerSideHelpers } from "@trpc/react-query/server"; -import copy from "copy-to-clipboard"; -import { CircuitBoard, ServerOff } from "lucide-react"; -import { HelpCircle } from "lucide-react"; -import type { - GetServerSidePropsContext, - InferGetServerSidePropsType, -} from "next"; -import Head from "next/head"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { type ReactElement, useEffect, useState } from "react"; -import { toast } from "sonner"; -import superjson from "superjson"; type TabState = | "projects" @@ -351,6 +351,7 @@ const Service = ( + diff --git a/packages/server/src/utils/docker/collision.ts b/packages/server/src/utils/docker/collision.ts index 9d399dc0d..de6d9bbb3 100644 --- a/packages/server/src/utils/docker/collision.ts +++ b/packages/server/src/utils/docker/collision.ts @@ -1,8 +1,14 @@ import { findComposeById } from "@dokploy/server/services/compose"; -import { dump, load } from "js-yaml"; +import { dump } from "js-yaml"; import { addAppNameToAllServiceNames } from "./collision/root-network"; import { generateRandomHash } from "./compose"; import { addSuffixToAllVolumes } from "./compose/volume"; +import { + cloneCompose, + cloneComposeRemote, + loadDockerCompose, + loadDockerComposeRemote, +} from "./domain"; import type { ComposeSpecification } from "./types"; export const addAppNameToPreventCollision = ( @@ -24,16 +30,34 @@ export const randomizeIsolatedDeploymentComposeFile = async ( suffix?: string, ) => { const compose = await findComposeById(composeId); - const composeFile = compose.composeFile; - const composeData = load(composeFile) as ComposeSpecification; + + if (compose.serverId) { + await cloneComposeRemote(compose); + } else { + await cloneCompose(compose); + } + + let composeData: ComposeSpecification | null; + + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + if (!composeData) { + throw new Error("Compose data not found"); + } const randomSuffix = suffix || compose.appName || generateRandomHash(); - const newComposeFile = addAppNameToPreventCollision( - composeData, - randomSuffix, - compose.isolatedDeploymentsVolume, - ); + const newComposeFile = compose.isolatedDeployment + ? addAppNameToPreventCollision( + composeData, + randomSuffix, + compose.isolatedDeploymentsVolume, + ) + : composeData; return dump(newComposeFile); };