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 4385dc6a..546069c5 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 @@ -26,6 +26,7 @@ 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"; +import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; interface Props { serverId?: string; @@ -128,6 +129,14 @@ export const ShowTraefikActions = ({ serverId }: Props) => { Enter the terminal */} + + e.preventDefault()} + className="cursor-pointer" + > + {t("settings.server.webServer.traefik.managePorts")} + + diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx new file mode 100644 index 00000000..aa9741ce --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -0,0 +1,215 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { api } from "@/utils/api"; +import { useTranslation } from "next-i18next"; +import type React from "react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +/** + * Props for the ManageTraefikPorts component + * @interface Props + * @property {React.ReactNode} children - The trigger element that opens the ports management modal + * @property {string} [serverId] - Optional ID of the server whose ports are being managed + */ +interface Props { + children: React.ReactNode; + serverId?: string; +} + +/** + * Represents a port mapping configuration for Traefik + * @interface AdditionalPort + * @property {number} targetPort - The internal port that the service is listening on + * @property {number} publishedPort - The external port that will be exposed + * @property {"ingress" | "host"} publishMode - The Docker Swarm publish mode: + * - "host": Publishes the port directly on the host + * - "ingress": Publishes the port through the Swarm routing mesh + */ +interface AdditionalPort { + targetPort: number; + publishedPort: number; + publishMode: "ingress" | "host"; +} + +/** + * ManageTraefikPorts is a component that provides a modal interface for managing + * additional port mappings for Traefik in a Docker Swarm environment. + * + * Features: + * - Add, remove, and edit port mappings + * - Configure target port, published port, and publish mode for each mapping + * - Persist port configurations through API calls + * + * @component + * @example + * ```tsx + * + * + * + * ``` + */ +export const ManageTraefikPorts = ({ children, serverId }: Props) => { + const { t } = useTranslation("settings"); + const [open, setOpen] = useState(false); + const [additionalPorts, setAdditionalPorts] = useState([]); + + const { data: currentPorts, refetch: refetchPorts } = + api.settings.getTraefikPorts.useQuery({ + serverId, + }); + + const { mutateAsync: updatePorts, isLoading } = + api.settings.updateTraefikPorts.useMutation({ + onSuccess: () => { + refetchPorts(); + }, + }); + + useEffect(() => { + if (currentPorts) { + setAdditionalPorts(currentPorts); + } + }, [currentPorts]); + + const handleAddPort = () => { + setAdditionalPorts([ + ...additionalPorts, + { targetPort: 0, publishedPort: 0, publishMode: "host" }, + ]); + }; + + const handleUpdatePorts = async () => { + try { + await updatePorts({ + serverId, + additionalPorts, + }); + toast.success(t("settings.server.webServer.traefik.portsUpdated")); + setOpen(false); + } catch (error) { + toast.error(t("settings.server.webServer.traefik.portsUpdateError")); + } + }; + + return ( + <> +
setOpen(true)}>{children}
+ + + + + {t("settings.server.webServer.traefik.managePorts")} + + + {t("settings.server.webServer.traefik.managePortsDescription")} + + +
+ {additionalPorts.map((port, index) => ( +
+
+ + { + const newPorts = [...additionalPorts]; + newPorts[index].targetPort = Number.parseInt( + e.target.value, + ); + setAdditionalPorts(newPorts); + }} + className="w-full rounded border p-2" + /> +
+
+ + { + const newPorts = [...additionalPorts]; + newPorts[index].publishedPort = Number.parseInt( + e.target.value, + ); + setAdditionalPorts(newPorts); + }} + className="w-full rounded border p-2" + /> +
+
+ + +
+
+ +
+
+ ))} +
+ + +
+
+
+
+ + ); +}; diff --git a/apps/dokploy/public/locales/en/settings.json b/apps/dokploy/public/locales/en/settings.json index 2103ecc0..1ce54692 100644 --- a/apps/dokploy/public/locales/en/settings.json +++ b/apps/dokploy/public/locales/en/settings.json @@ -18,6 +18,14 @@ "settings.server.webServer.server.label": "Server", "settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.modifyEnv": "Modify Env", + "settings.server.webServer.traefik.managePorts": "Additional Ports", + "settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik", + "settings.server.webServer.traefik.targetPort": "Target Port", + "settings.server.webServer.traefik.publishedPort": "Published Port", + "settings.server.webServer.traefik.addPort": "Add Port", + "settings.server.webServer.traefik.portsUpdated": "Ports updated successfully", + "settings.server.webServer.traefik.portsUpdateError": "Failed to update ports", + "settings.server.webServer.traefik.publishMode": "Publish Mode", "settings.server.webServer.storage.label": "Space", "settings.server.webServer.storage.cleanUnusedImages": "Clean unused images", "settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes", diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index dbc99a41..8bec9b52 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -716,6 +716,83 @@ export const settingsRouter = createTRPCRouter({ throw new Error("Failed to check GPU status"); } }), + updateTraefikPorts: adminProcedure + .input( + z.object({ + serverId: z.string().optional(), + additionalPorts: z.array( + z.object({ + targetPort: z.number(), + publishedPort: z.number(), + publishMode: z.enum(["ingress", "host"]).default("host"), + }), + ), + }), + ) + .mutation(async ({ input }) => { + try { + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Please set a serverId to update Traefik ports", + }); + } + await initializeTraefik({ + serverId: input.serverId, + additionalPorts: input.additionalPorts, + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: + error instanceof Error + ? error.message + : "Error to update Traefik ports", + cause: error, + }); + } + }), + getTraefikPorts: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + + try { + let stdout = ""; + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + stdout = result.stdout; + } else if (!IS_CLOUD) { + const result = await execAsync(command); + stdout = result.stdout; + } + + const ports: { + Protocol: string; + TargetPort: number; + PublishedPort: number; + PublishMode: string; + }[] = JSON.parse(stdout.trim()); + + // Filter out the default ports (80, 443, and optionally 8080) + const additionalPorts = ports + .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) + .map((port) => ({ + targetPort: port.TargetPort, + publishedPort: port.PublishedPort, + publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", + })); + + return additionalPorts; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to get Traefik ports", + cause: error, + }); + } + }), }); // { // "Parallelism": 1, diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index 82832027..5e994dd2 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -16,12 +16,18 @@ interface TraefikOptions { enableDashboard?: boolean; env?: string[]; serverId?: string; + additionalPorts?: { + targetPort: number; + publishedPort: number; + publishMode?: "ingress" | "host"; + }[]; } export const initializeTraefik = async ({ enableDashboard = false, env, serverId, + additionalPorts = [], }: TraefikOptions = {}) => { const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId); const imageName = "traefik:v3.1.2"; @@ -84,6 +90,11 @@ export const initializeTraefik = async ({ }, ] : []), + ...additionalPorts.map((port) => ({ + TargetPort: port.targetPort, + PublishedPort: port.publishedPort, + PublishMode: port.publishMode || ("host" as const), + })), ], }, };