From 4b1146ab6be1412a3570c1fa260d1205af5a05c0 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:58:15 -0600 Subject: [PATCH 1/5] remove the "isWildcard" column from the "domain" table in the database schema --- .../compose/advanced/add-isolation.tsx | 195 ++++++++++++++++++ .../compose/general/compose-file-editor.tsx | 15 +- .../compose/general/show-utilities.tsx | 46 ----- .../services/compose/[composeId].tsx | 31 +-- 4 files changed, 217 insertions(+), 70 deletions(-) create mode 100644 apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx delete mode 100644 apps/dokploy/components/dashboard/compose/general/show-utilities.tsx 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..55cb0d906 --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx @@ -0,0 +1,195 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle } 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 { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/form"; +import { Label } from "@/components/ui/label"; +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 { mutateAsync, error, isError } = + api.compose.isolatedDeployment.useMutation(); + + 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(() => { + randomizeCompose(); + 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 randomizeCompose(); + await refetch(); + toast.success("Compose updated"); + }) + .catch(() => { + toast.error("Error updating the compose"); + }); + }; + + const randomizeCompose = async () => { + await mutateAsync({ + composeId, + suffix: data?.appName || "", + }).then(async (data) => { + await utils.project.all.invalidate(); + setCompose(data); + }); + }; + + 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 volumes
  • +
  • Docker networks
  • +
+
+
+
+ {isError && {error?.message}} +
+ + {isError && ( +
+ + + {error?.message} + +
+ )} + +
+
+ ( + +
+ + Enable Isolated Deployment ({data?.appName}) + + + Enable isolated deployment to the compose file. + +
+ + + +
+ )} + /> +
+ +
+ +
+
+
+ +
+									
+								
+
+
+ +
+
+
+ ); +}; 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:
-
- -
+
- - - - Utilities - Modify the application data - - - - Isolated Deployment - Randomize Compose - - - - - - - - - - - ); -}; 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 = ( +
From 1fe12ba93e2eff1e93c99a18ec0e3005407a5511 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:38:10 -0600 Subject: [PATCH 2/5] feat(isolation): add preview functionality for isolated deployment with loading state and dialog --- .../compose/advanced/add-isolation.tsx | 101 +++++++++++++----- .../general/show-converted-compose.tsx | 2 +- packages/server/src/utils/docker/collision.ts | 40 +++++-- 3 files changed, 107 insertions(+), 36 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx index 55cb0d906..cea716858 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx @@ -1,5 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; +import { AlertTriangle, Loader2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -14,6 +14,13 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Form, FormControl, @@ -22,7 +29,7 @@ import { FormItem, FormLabel, } from "@/components/ui/form"; -import { Label } from "@/components/ui/label"; + import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; @@ -40,9 +47,12 @@ 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( @@ -58,7 +68,6 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => { }); useEffect(() => { - randomizeCompose(); if (data) { form.reset({ isolatedDeployment: data?.isolatedDeployment || false, @@ -72,7 +81,6 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => { isolatedDeployment: formData?.isolatedDeployment || false, }) .then(async (_data) => { - await randomizeCompose(); await refetch(); toast.success("Compose updated"); }) @@ -81,26 +89,31 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => { }); }; - const randomizeCompose = async () => { - await mutateAsync({ - composeId, - suffix: data?.appName || "", - }).then(async (data) => { - await utils.project.all.invalidate(); - setCompose(data); - }); + 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 + Enable Isolated Deployment Configure isolated deployment to the compose file. - - - -
This feature creates an isolated environment for your deployment @@ -122,6 +135,10 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
+ + + +
{isError && {error?.message}}
{
-
- -
-									
-								
+ +
+ + + + + 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/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/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); }; From 85bce827eb617b8b59a97004836db557fecc24eb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:41:18 -0600 Subject: [PATCH 3/5] fix(keyboard-nav): ensure correct type for shortcut keys in navigation --- apps/dokploy/hooks/use-keyboard-nav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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())}`, ); From cfa01359329ee2ad18b4e756ac38189f441b4dbb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:42:50 -0600 Subject: [PATCH 4/5] remove: delete IsolatedDeployment component from dashboard --- .../compose/general/isolated-deployment.tsx | 188 ------------------ 1 file changed, 188 deletions(-) delete mode 100644 apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx diff --git a/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx b/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx deleted file mode 100644 index d76f79021..000000000 --- a/apps/dokploy/components/dashboard/compose/general/isolated-deployment.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { AlertBlock } from "@/components/shared/alert-block"; -import { CodeEditor } from "@/components/shared/code-editor"; -import { Button } from "@/components/ui/button"; -import { - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, -} from "@/components/ui/form"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { AlertTriangle } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -interface Props { - composeId: string; -} - -const schema = z.object({ - isolatedDeployment: z.boolean().optional(), -}); - -type Schema = z.infer; - -export const IsolatedDeployment = ({ composeId }: Props) => { - const utils = api.useUtils(); - const [compose, setCompose] = useState(""); - const { mutateAsync, error, isError } = - api.compose.isolatedDeployment.useMutation(); - - const { mutateAsync: updateCompose } = api.compose.update.useMutation(); - - const { data, refetch } = api.compose.one.useQuery( - { composeId }, - { enabled: !!composeId }, - ); - - console.log(data); - - const form = useForm({ - defaultValues: { - isolatedDeployment: false, - }, - resolver: zodResolver(schema), - }); - - useEffect(() => { - randomizeCompose(); - if (data) { - form.reset({ - isolatedDeployment: data?.isolatedDeployment || false, - }); - } - }, [form, form.reset, form.formState.isSubmitSuccessful, data]); - - const onSubmit = async (formData: Schema) => { - await updateCompose({ - composeId, - isolatedDeployment: formData?.isolatedDeployment || false, - }) - .then(async (_data) => { - await randomizeCompose(); - await refetch(); - toast.success("Compose updated"); - }) - .catch(() => { - toast.error("Error updating the compose"); - }); - }; - - const randomizeCompose = async () => { - await mutateAsync({ - composeId, - suffix: data?.appName || "", - }).then(async (data) => { - await utils.project.all.invalidate(); - setCompose(data); - }); - }; - - return ( - <> - - Isolate Deployment - - Use this option to isolate the deployment of this 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 volumes
  • -
  • Docker networks
  • -
-
-
-
- {isError && {error?.message}} -
- - {isError && ( -
- - - {error?.message} - -
- )} - -
-
- ( - -
- - Enable Isolated Deployment ({data?.appName}) - - - Enable isolated deployment to the compose file. - -
- - - -
- )} - /> -
- -
- -
-
-
- -
-							
-						
-
-
- - - ); -}; From 231b8ed19d9c1c82654bc9b0d0d58e0157e86832 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:43:03 -0600 Subject: [PATCH 5/5] remove: eliminate Docker volumes from isolated deployment resources list --- .../components/dashboard/compose/advanced/add-isolation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx index cea716858..5b6e04154 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-isolation.tsx @@ -129,7 +129,6 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => { Resources that will be isolated:
    -
  • Docker volumes
  • Docker networks