From 1617d07fbd62ea0e39b4e54f8369a0310e8034de Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 11 Aug 2026 01:55:42 -0600 Subject: [PATCH 1/4] refactor: move cluster nodes into docker dashboard swarm tab - Add Nodes sub-tab under Docker > Swarm (visible with organization.update permission) - Remove Cluster item from settings sidebar and delete /dashboard/settings/cluster page - Point internal cluster links to /dashboard/docker?tab=swarm&subtab=nodes - Make ShowNodes card full-width to match the other docker tabs --- .../cluster/show-cluster-settings.tsx | 2 +- .../settings/cluster/nodes/show-nodes.tsx | 2 +- .../swarm/containers/empty-states.tsx | 10 ++-- .../containers/show-swarm-containers.tsx | 2 +- .../dashboard/swarm/monitoring-card.tsx | 4 +- apps/dokploy/components/layouts/side.tsx | 8 --- apps/dokploy/pages/dashboard/docker.tsx | 39 +++++++++++- .../pages/dashboard/settings/cluster.tsx | 59 ------------------- 8 files changed, 48 insertions(+), 78 deletions(-) delete mode 100644 apps/dokploy/pages/dashboard/settings/cluster.tsx diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index 95f849480..ae3392473 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -187,7 +187,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => { To use a cluster feature, you need to configure at least a registry first. Please, go to{" "} Settings diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx index c7f580caa..1991d57d2 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx @@ -58,7 +58,7 @@ export const ShowNodes = ({ serverId }: Props) => { const haveAtLeastOneRegistry = !!(registry && registry?.length > 0); return (
- +
diff --git a/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx b/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx index 306b2f808..2e67954fe 100644 --- a/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx +++ b/apps/dokploy/components/dashboard/swarm/containers/empty-states.tsx @@ -35,7 +35,7 @@ export const DocLinks = () => ( Cluster Settings @@ -85,7 +85,7 @@ export const SwarmNotAvailable = ({
  • Check the{" "} Cluster Settings @@ -130,7 +130,7 @@ export const ServicesError = ({
  • Network connectivity issues to a remote server — check{" "} Cluster Settings @@ -183,7 +183,7 @@ export const NoServices = ({ nodeCount, onRefresh }: NoServicesProps) => ( Worker nodes need to pull images from a shared registry. Configure one in{" "} Cluster Settings @@ -255,7 +255,7 @@ export const NoRunningContainers = ({
  • Images can't be pulled on worker nodes — verify your{" "} registry configuration diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx index 4c4c5e2be..303609487 100644 --- a/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx +++ b/apps/dokploy/components/dashboard/swarm/containers/show-swarm-containers.tsx @@ -303,7 +303,7 @@ export const ShowSwarmContainers = ({ serverId }: Props) => {

    Manage nodes in{" "} Cluster Settings diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 01ee03e80..2ad6b68a1 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -51,7 +51,9 @@ export default function SwarmMonitorCard({ serverId }: Props) { {!serverId && (

  • + {canManageCluster && ( + + + + )} diff --git a/apps/dokploy/pages/dashboard/settings/cluster.tsx b/apps/dokploy/pages/dashboard/settings/cluster.tsx deleted file mode 100644 index 8505272b9..000000000 --- a/apps/dokploy/pages/dashboard/settings/cluster.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { validateRequest } from "@dokploy/server"; -import { createServerSideHelpers } from "@trpc/react-query/server"; -import type { GetServerSidePropsContext } from "next"; -import type { ReactElement } from "react"; -import superjson from "superjson"; -import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes"; -import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { ServerFilter } from "@/components/shared/server-filter"; -import { appRouter } from "@/server/api/root"; - -const Page = () => { - return ( - - {(serverId) => ( -
    - -
    - )} -
    - ); -}; - -export default Page; - -Page.getLayout = (page: ReactElement) => { - return {page}; -}; -export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }>, -) { - const { req, res } = ctx; - const { user, session } = await validateRequest(ctx.req); - if (!user || user.role === "member") { - return { - redirect: { - permanent: false, - destination: "/", - }, - }; - } - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session as any, - user: user as any, - }, - transformer: superjson, - }); - await helpers.user.get.prefetch(); - - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; -} From c6c259f926cac18947c84daad70a5695ecb9ca4b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 11 Aug 2026 02:01:21 -0600 Subject: [PATCH 2/4] refactor: add icon support for deployments and services in UI components --- .../deployments/show-deployments-table.tsx | 17 ++++++++++--- .../components/shared/advance-breadcrumb.tsx | 25 +++++++++++++++---- packages/server/src/services/deployment.ts | 4 +-- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx b/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx index 770d4efd0..11ffae7d0 100644 --- a/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx +++ b/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx @@ -14,10 +14,11 @@ import { import type { inferRouterOutputs } from "@trpc/server"; import { ArrowUpDown, - Boxes, ChevronLeft, ChevronRight, + CircuitBoard, ExternalLink, + GlobeIcon, Loader2, Rocket, Server, @@ -71,6 +72,7 @@ function getServiceInfo(d: DeploymentRow) { return { type: "Application" as const, name: app.name, + icon: app.icon, projectId: app.environment.project.projectId, environmentId: app.environment.environmentId, projectName: app.environment.project.name, @@ -83,6 +85,7 @@ function getServiceInfo(d: DeploymentRow) { return { type: "Compose" as const, name: comp.name, + icon: comp.icon, projectId: comp.environment.project.projectId, environmentId: comp.environment.environmentId, projectName: comp.environment.project.name, @@ -175,10 +178,16 @@ export function ShowDeploymentsTable() { if (!info) return —; return (
    - {info.type === "Application" ? ( - + {info.icon ? ( + {info.name} + ) : info.type === "Application" ? ( + ) : ( - + )}
    {info.name} diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx index cd1682e1c..06e4e618c 100644 --- a/apps/dokploy/components/shared/advance-breadcrumb.tsx +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -45,10 +45,12 @@ type ServiceItem = { id: string; name: string; type: ServiceType; + icon: string | null; }; type NamedService = { name: string; + icon?: string | null; }; type EnvironmentServiceCollections = { @@ -116,8 +118,20 @@ const getStringQueryParam = (value: string | string[] | undefined) => const includesSearch = (value: string | null | undefined, search: string) => value?.toLowerCase().includes(search.toLowerCase()) ?? false; -const getServiceIcon = (type: ServiceType, className = "size-4") => { - const Icon = SERVICE_ICONS[type]; +const getServiceIcon = ( + service: Pick, + className = "size-4", +) => { + if (service.icon) { + return ( + + ); + } + const Icon = SERVICE_ICONS[service.type]; return ; }; @@ -127,7 +141,7 @@ const countEnvironmentServices = (environment: ServiceCollections): number => 0, ); -const mapServices = ( +const mapServices = ( items: readonly T[], getId: (item: T) => string, type: ServiceType, @@ -136,6 +150,7 @@ const mapServices = ( id: getId(item), name: item.name, type, + icon: item.icon ?? null, })); const extractServicesFromEnvironment = ( @@ -550,7 +565,7 @@ export const AdvanceBreadcrumb = () => { aria-expanded={serviceOpen} className="h-auto px-2 py-1.5 hover:bg-accent gap-2" > - {getServiceIcon(currentService.type)} + {getServiceIcon(currentService)} {currentService.name} @@ -589,7 +604,7 @@ export const AdvanceBreadcrumb = () => { >
    - {getServiceIcon(service.type)} + {getServiceIcon(service)}
    diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index b78e6c203..18577a861 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -802,7 +802,7 @@ export const findAllDeploymentsByComposeId = async (composeId: string) => { const centralizedDeploymentsWith = { application: { - columns: { applicationId: true, name: true, appName: true }, + columns: { applicationId: true, name: true, appName: true, icon: true }, with: { environment: { columns: { environmentId: true, name: true }, @@ -821,7 +821,7 @@ const centralizedDeploymentsWith = { }, }, compose: { - columns: { composeId: true, name: true, appName: true }, + columns: { composeId: true, name: true, appName: true, icon: true }, with: { environment: { columns: { environmentId: true, name: true }, From c76c5b2f907876c257cecaf2fefb96bc0937bd71 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 11 Aug 2026 02:02:24 -0600 Subject: [PATCH 3/4] refactor: show nodes tab to any user with docker.read --- apps/dokploy/pages/dashboard/docker.tsx | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/apps/dokploy/pages/dashboard/docker.tsx b/apps/dokploy/pages/dashboard/docker.tsx index 54f9e28d8..c84e238a3 100644 --- a/apps/dokploy/pages/dashboard/docker.tsx +++ b/apps/dokploy/pages/dashboard/docker.tsx @@ -23,22 +23,15 @@ const DEFAULT_SWARM_TAB = "overview"; const Dashboard = () => { const router = useRouter(); const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: permissions } = api.user.getPermissions.useQuery(); - - const canManageCluster = !!permissions?.organization.update; const queryTab = typeof router.query.tab === "string" ? router.query.tab : DEFAULT_TAB; const activeTab = isCloud && queryTab === "networks" ? DEFAULT_TAB : queryTab; - const querySwarmTab = + const activeSwarmTab = typeof router.query.subtab === "string" ? router.query.subtab : DEFAULT_SWARM_TAB; - const activeSwarmTab = - !canManageCluster && querySwarmTab === "nodes" - ? DEFAULT_SWARM_TAB - : querySwarmTab; const setTab = (value: string) => { const { tab: _current, subtab: _subtab, ...query } = router.query; @@ -83,9 +76,7 @@ const Dashboard = () => { Overview Containers - {canManageCluster && ( - Nodes - )} + Nodes @@ -97,11 +88,9 @@ const Dashboard = () => {
    - {canManageCluster && ( - - - - )} + + + From 4732b38d5a3b3d6950cddeda800108e83cb8860f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 11 Aug 2026 02:04:09 -0600 Subject: [PATCH 4/4] refactor: align cluster/swarm read queries with docker.read, gate node actions by server permissions --- .../settings/cluster/nodes/show-nodes.tsx | 56 ++++++++++--------- apps/dokploy/server/api/routers/cluster.ts | 2 +- apps/dokploy/server/api/routers/swarm.ts | 10 ++-- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx index 1991d57d2..ef036507a 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes.tsx @@ -52,6 +52,7 @@ export const ShowNodes = ({ serverId }: Props) => { serverId, }); const { data: registry } = api.registry.all.useQuery(); + const { data: permissions } = api.user.getPermissions.useQuery(); const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation(); @@ -68,7 +69,7 @@ export const ShowNodes = ({ serverId }: Props) => { Add nodes to your cluster
    - {haveAtLeastOneRegistry && ( + {haveAtLeastOneRegistry && permissions?.server.create && (
    @@ -144,34 +145,35 @@ export const ShowNodes = ({ serverId }: Props) => { Actions - {!node?.ManagerStatus?.Leader && ( - { - await deleteNode({ - nodeId: node.ID, - serverId, - }) - .then(() => { - refetch(); - toast.success( - "Node deleted successfully", - ); + {!node?.ManagerStatus?.Leader && + permissions?.server.delete && ( + { + await deleteNode({ + nodeId: node.ID, + serverId, }) - .catch(() => { - toast.error("Error deleting node"); - }); - }} - > - e.preventDefault()} + .then(() => { + refetch(); + toast.success( + "Node deleted successfully", + ); + }) + .catch(() => { + toast.error("Error deleting node"); + }); + }} > - Delete - - - )} + e.preventDefault()} + > + Delete + + + )} diff --git a/apps/dokploy/server/api/routers/cluster.ts b/apps/dokploy/server/api/routers/cluster.ts index ff0bc4de6..f121c62ea 100644 --- a/apps/dokploy/server/api/routers/cluster.ts +++ b/apps/dokploy/server/api/routers/cluster.ts @@ -13,7 +13,7 @@ import { getLocalServerIp } from "@/server/wss/terminal"; import { createTRPCRouter, withPermission } from "../trpc"; export const clusterRouter = createTRPCRouter({ - getNodes: withPermission("server", "read") + getNodes: withPermission("docker", "read") .input( z.object({ serverId: z.string().optional(), diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts index 483bccf3e..771c52adf 100644 --- a/apps/dokploy/server/api/routers/swarm.ts +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -12,7 +12,7 @@ import { createTRPCRouter, withPermission } from "../trpc"; import { containerIdRegex } from "./docker"; export const swarmRouter = createTRPCRouter({ - getNodes: withPermission("server", "read") + getNodes: withPermission("docker", "read") .input( z.object({ serverId: z.string().optional(), @@ -30,7 +30,7 @@ export const swarmRouter = createTRPCRouter({ } return await getSwarmNodes(input.serverId); }), - getNodeInfo: withPermission("server", "read") + getNodeInfo: withPermission("docker", "read") .input(z.object({ nodeId: z.string(), serverId: z.string().optional() })) .query(async ({ input, ctx }) => { if (input.serverId) { @@ -44,7 +44,7 @@ export const swarmRouter = createTRPCRouter({ } return await getNodeInfo(input.nodeId, input.serverId); }), - getNodeApps: withPermission("server", "read") + getNodeApps: withPermission("docker", "read") .input( z.object({ serverId: z.string().optional(), @@ -62,7 +62,7 @@ export const swarmRouter = createTRPCRouter({ } return getNodeApplications(input.serverId); }), - getAppInfos: withPermission("server", "read") + getAppInfos: withPermission("docker", "read") .meta({ openapi: { path: "/drop-deployment", @@ -93,7 +93,7 @@ export const swarmRouter = createTRPCRouter({ } return await getApplicationInfo(input.appName, input.serverId); }), - getContainerStats: withPermission("server", "read") + getContainerStats: withPermission("docker", "read") .input( z.object({ serverId: z.string().optional(),