From c739c67616ae45bd501cc230d60e3c9ccf7f608d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:16:49 +0000 Subject: [PATCH 1/5] Initial plan From f6f09215604fd8d4aa5ed6bb142c8b7d3d3719f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:20:03 +0000 Subject: [PATCH 2/5] Add network configuration form to Swarm Settings Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- .../cluster/modify-swarm-settings.tsx | 9 + .../advanced/cluster/swarm-forms/index.ts | 1 + .../cluster/swarm-forms/network-form.tsx | 207 ++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index ee427feca..4c6fc60c7 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -22,6 +22,7 @@ import { HealthCheckForm, LabelsForm, ModeForm, + NetworkForm, PlacementForm, RestartPolicyForm, RollbackConfigForm, @@ -79,6 +80,13 @@ const menuItems: MenuItem[] = [ docDescription: "Set service mode to either 'Replicated' with a specified number of tasks (Replicas), or 'Global' (one task per node).", }, + { + id: "network", + label: "Network", + description: "Configure network attachments", + docDescription: + "Attach the service to one or more networks. Specify the network name (Target) and optional network aliases for service discovery.", + }, { id: "labels", label: "Labels", @@ -190,6 +198,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => { )} {activeMenu === "mode" && } + {activeMenu === "network" && } {activeMenu === "labels" && } {activeMenu === "stop-grace-period" && ( diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts index 2f07be53d..df972102d 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts @@ -2,6 +2,7 @@ export { EndpointSpecForm } from "./endpoint-spec-form"; export { HealthCheckForm } from "./health-check-form"; export { LabelsForm } from "./labels-form"; export { ModeForm } from "./mode-form"; +export { NetworkForm } from "./network-form"; export { PlacementForm } from "./placement-form"; export { RestartPolicyForm } from "./restart-policy-form"; export { RollbackConfigForm } from "./rollback-config-form"; diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx new file mode 100644 index 000000000..f77966c6e --- /dev/null +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx @@ -0,0 +1,207 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; + +export const networkFormSchema = z.object({ + networks: z + .array( + z.object({ + Target: z.string().optional(), + Aliases: z.string().optional(), + }), + ) + .optional(), +}); + +interface NetworkFormProps { + id: string; + type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; +} + +export const NetworkForm = ({ id, type }: NetworkFormProps) => { + const [isLoading, setIsLoading] = useState(false); + + const queryMap = { + postgres: () => + api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }), + redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }), + mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }), + mariadb: () => + api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }), + application: () => + api.application.one.useQuery({ applicationId: id }, { enabled: !!id }), + mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }), + }; + const { data, refetch } = queryMap[type] + ? queryMap[type]() + : api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }); + + const mutationMap = { + postgres: () => api.postgres.update.useMutation(), + redis: () => api.redis.update.useMutation(), + mysql: () => api.mysql.update.useMutation(), + mariadb: () => api.mariadb.update.useMutation(), + application: () => api.application.update.useMutation(), + mongo: () => api.mongo.update.useMutation(), + }; + + const { mutateAsync } = mutationMap[type] + ? mutationMap[type]() + : api.mongo.update.useMutation(); + + const form = useForm({ + resolver: zodResolver(networkFormSchema), + defaultValues: { + networks: [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "networks", + }); + + useEffect(() => { + if (data?.networkSwarm && Array.isArray(data.networkSwarm)) { + const networkEntries = data.networkSwarm.map((network) => ({ + Target: network.Target || "", + Aliases: network.Aliases?.join(", ") || "", + })); + form.reset({ networks: networkEntries }); + } + }, [data, form]); + + const onSubmit = async (formData: z.infer) => { + setIsLoading(true); + try { + const networksArray = + formData.networks + ?.filter((network) => network.Target) + .map((network) => ({ + Target: network.Target, + Aliases: network.Aliases + ? network.Aliases.split(",").map((alias) => alias.trim()) + : undefined, + })) || []; + + // If no networks, send null to clear the database + const networksToSend = networksArray.length > 0 ? networksArray : null; + + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + networkSwarm: networksToSend, + }); + + toast.success("Network configuration updated successfully"); + refetch(); + } catch { + toast.error("Error updating network configuration"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ +
+ Networks + + Configure network attachments for your service + +
+ {fields.map((field, index) => ( +
+ ( + + Network Name + + + + + The name of the network to attach to + + + + )} + /> + ( + + Aliases (optional) + + + + + Comma-separated list of network aliases + + + + )} + /> + +
+ ))} + +
+
+ +
+ + +
+
+ + ); +}; From 582f493f3f09ea74ba6ced58fd43e9d8ef8de343 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:22:26 +0000 Subject: [PATCH 3/5] Fix type safety and optimize mutation payload in network form Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- .../cluster/swarm-forms/network-form.tsx | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx index f77966c6e..ed7ddf94a 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx @@ -63,7 +63,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => { ? mutationMap[type]() : api.mongo.update.useMutation(); - const form = useForm({ + const form = useForm>({ resolver: zodResolver(networkFormSchema), defaultValues: { networks: [], @@ -101,15 +101,31 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => { // If no networks, send null to clear the database const networksToSend = networksArray.length > 0 ? networksArray : null; - await mutateAsync({ - applicationId: id || "", - postgresId: id || "", - redisId: id || "", - mysqlId: id || "", - mariadbId: id || "", - mongoId: id || "", - networkSwarm: networksToSend, - }); + const mutationPayload: any = { networkSwarm: networksToSend }; + + // Add the appropriate ID based on type + switch (type) { + case "application": + mutationPayload.applicationId = id; + break; + case "postgres": + mutationPayload.postgresId = id; + break; + case "redis": + mutationPayload.redisId = id; + break; + case "mysql": + mutationPayload.mysqlId = id; + break; + case "mariadb": + mutationPayload.mariadbId = id; + break; + case "mongo": + mutationPayload.mongoId = id; + break; + } + + await mutateAsync(mutationPayload); toast.success("Network configuration updated successfully"); refetch(); From c2894260dc759a31b506944041be1a3f505fc84c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:23:29 +0000 Subject: [PATCH 4/5] Revert to consistent pattern with existing swarm forms Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com> --- .../cluster/swarm-forms/network-form.tsx | 34 +++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx index ed7ddf94a..508bb7140 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx @@ -101,31 +101,15 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => { // If no networks, send null to clear the database const networksToSend = networksArray.length > 0 ? networksArray : null; - const mutationPayload: any = { networkSwarm: networksToSend }; - - // Add the appropriate ID based on type - switch (type) { - case "application": - mutationPayload.applicationId = id; - break; - case "postgres": - mutationPayload.postgresId = id; - break; - case "redis": - mutationPayload.redisId = id; - break; - case "mysql": - mutationPayload.mysqlId = id; - break; - case "mariadb": - mutationPayload.mariadbId = id; - break; - case "mongo": - mutationPayload.mongoId = id; - break; - } - - await mutateAsync(mutationPayload); + await mutateAsync({ + applicationId: id || "", + postgresId: id || "", + redisId: id || "", + mysqlId: id || "", + mariadbId: id || "", + mongoId: id || "", + networkSwarm: networksToSend, + }); toast.success("Network configuration updated successfully"); refetch(); From c65026353a3e0496842e5592809f9dba5f7c0006 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 5 Feb 2026 23:18:41 -0600 Subject: [PATCH 5/5] feat(network-form): add DriverOptsEntries to network form schema and UI - Introduced DriverOptsEntries to the network form schema, allowing users to specify driver options for networks. - Updated the form UI to support adding, editing, and removing driver options dynamically. - Adjusted the backend schema to accept driver options as a record of key-value pairs. --- .../cluster/swarm-forms/network-form.tsx | 121 +++++++++++++++++- packages/server/src/db/schema/shared.ts | 2 +- 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx index 508bb7140..43a816dfc 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/network-form.tsx @@ -16,12 +16,18 @@ import { import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; +const driverOptEntrySchema = z.object({ + key: z.string(), + value: z.string(), +}); + export const networkFormSchema = z.object({ networks: z .array( z.object({ Target: z.string().optional(), Aliases: z.string().optional(), + DriverOptsEntries: z.array(driverOptEntrySchema).optional(), }), ) .optional(), @@ -80,6 +86,12 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => { const networkEntries = data.networkSwarm.map((network) => ({ Target: network.Target || "", Aliases: network.Aliases?.join(", ") || "", + DriverOptsEntries: network.DriverOpts + ? Object.entries(network.DriverOpts).map(([key, value]) => ({ + key, + value: value ?? "", + })) + : [], })); form.reset({ networks: networkEntries }); } @@ -91,12 +103,25 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => { const networksArray = formData.networks ?.filter((network) => network.Target) - .map((network) => ({ - Target: network.Target, - Aliases: network.Aliases - ? network.Aliases.split(",").map((alias) => alias.trim()) - : undefined, - })) || []; + .map((network) => { + const entries = + (network.DriverOptsEntries ?? []).filter( + (e) => e.key.trim() !== "", + ); + const driverOpts = + entries.length > 0 + ? Object.fromEntries( + entries.map((e) => [e.key.trim(), e.value]), + ) + : undefined; + return { + Target: network.Target, + Aliases: network.Aliases + ? network.Aliases.split(",").map((alias) => alias.trim()) + : undefined, + DriverOpts: driverOpts, + }; + }) || []; // If no networks, send null to clear the database const networksToSend = networksArray.length > 0 ? networksArray : null; @@ -166,6 +191,82 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => { )} /> +
+ Driver options (optional) + + e.g. com.docker.network.driver.mtu, com.docker.network.driver.host_binding + + {(form.watch(`networks.${index}.DriverOptsEntries`) ?? []).map( + (_, optIndex) => ( +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ ), + )} + +
diff --git a/packages/server/src/db/schema/shared.ts b/packages/server/src/db/schema/shared.ts index 600593d7a..4cdebe1f8 100644 --- a/packages/server/src/db/schema/shared.ts +++ b/packages/server/src/db/schema/shared.ts @@ -167,7 +167,7 @@ export const NetworkSwarmSchema = z.array( .object({ Target: z.string().optional(), Aliases: z.array(z.string()).optional(), - DriverOpts: z.object({}).optional(), + DriverOpts: z.record(z.string()).optional(), }) .strict(), );