From 6da122eab7d5f86fc6ef4c7b7cdcf970e811fc67 Mon Sep 17 00:00:00 2001 From: Vlad Vladov Date: Wed, 3 Sep 2025 17:57:44 +0300 Subject: [PATCH 01/79] feat(tags): Add support for tags from Github Packages --- .../pages/api/deploy/[refreshToken].ts | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts index 3e515b182..22fabb39d 100644 --- a/apps/dokploy/pages/api/deploy/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts @@ -43,17 +43,19 @@ export default async function handler( if (sourceType === "docker") { const applicationDockerTag = extractImageTag(application.dockerImage); - const webhookDockerTag = extractImageTagFromRequest( + const webhookDockerTags = extractImageTagFromRequest( req.headers, req.body, ); - if ( + const isMismatch = applicationDockerTag && - webhookDockerTag && - webhookDockerTag !== applicationDockerTag - ) { + webhookDockerTags && + webhookDockerTags.length > 0 && + !webhookDockerTags.includes(applicationDockerTag); + + if (isMismatch) { res.status(301).json({ - message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`, + message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag(s) (${webhookDockerTags.join(", ")}).`, }); return; } @@ -236,10 +238,38 @@ function extractImageTag(dockerImage: string | null) { export const extractImageTagFromRequest = ( headers: any, body: any, -): string | null => { +): string[] | null => { if (headers["user-agent"]?.includes("Go-http-client")) { if (body.push_data && body.repository) { - return body.push_data.tag; + return [body.push_data.tag] as string[]; + } + } + // GitHub Packages: package or registry_package events (container tags) + // See: https://docs.github.com/en/webhooks/webhook-events-and-payloads#package + const githubEvent = headers["x-github-event"]; + + if (githubEvent === "package" || githubEvent === "registry_package") { + const pkg = body?.package ?? body?.registry_package?.package ?? null; + const packageVersion = + body?.package_version ?? body?.registry_package?.package_version ?? null; + const packageType = pkg?.package_type; + + if (packageType === "container" && packageVersion) { + const tags = + packageVersion?.metadata?.container?.tags ?? + packageVersion?.container?.tags ?? + null; + if (Array.isArray(tags) && tags.length > 0) { + return tags as string[]; + } + const singleTag = + packageVersion?.metadata?.container?.tag ?? + packageVersion?.metadata?.tag ?? + packageVersion?.tag ?? + null; + if (typeof singleTag === "string") { + return [singleTag] as string[]; + } } } return null; From 4b44bc86b43d63f0837b4957b50111f07923229b Mon Sep 17 00:00:00 2001 From: HarikrishnanD Date: Fri, 26 Sep 2025 18:30:38 +0530 Subject: [PATCH 02/79] fix: allow all organization members to access requests functionality - Change requests-related procedures from adminProcedure to protectedProcedure - Fixes issue where members with all permissions couldn't see/activate requests - Affects readStatsLogs, haveActivateRequests, toggleRequests, updateLogCleanup, and getLogCleanupStatus - Resolves #2687 --- apps/dokploy/server/api/routers/settings.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index b4968c260..e0a74f5cf 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -587,7 +587,7 @@ export const settingsRouter = createTRPCRouter({ return ports.some((port) => port.targetPort === 8080); }), - readStatsLogs: adminProcedure + readStatsLogs: protectedProcedure .meta({ openapi: { path: "/read-stats-logs", @@ -650,7 +650,7 @@ export const settingsRouter = createTRPCRouter({ const processedLogs = processLogs(rawConfig as string, input?.dateRange); return processedLogs || []; }), - haveActivateRequests: adminProcedure.query(async () => { + haveActivateRequests: protectedProcedure.query(async () => { if (IS_CLOUD) { return true; } @@ -665,7 +665,7 @@ export const settingsRouter = createTRPCRouter({ return !!parsedConfig?.accessLog?.filePath; }), - toggleRequests: adminProcedure + toggleRequests: protectedProcedure .input( z.object({ enable: z.boolean(), @@ -835,7 +835,7 @@ export const settingsRouter = createTRPCRouter({ const ports = await readPorts("dokploy-traefik", input?.serverId); return ports; }), - updateLogCleanup: adminProcedure + updateLogCleanup: protectedProcedure .input( z.object({ cronExpression: z.string().nullable(), @@ -851,7 +851,7 @@ export const settingsRouter = createTRPCRouter({ return stopLogCleanup(); }), - getLogCleanupStatus: adminProcedure.query(async () => { + getLogCleanupStatus: protectedProcedure.query(async () => { return getLogCleanupStatus(); }), From 0f100c7bc8749b930fd5ee287f922b058a73b8c9 Mon Sep 17 00:00:00 2001 From: Aathil Felix Date: Sat, 1 Nov 2025 18:03:40 +0530 Subject: [PATCH 03/79] feat: add server time clock --- .../components/dashboard/projects/show.tsx | 952 ++++----- apps/dokploy/components/layouts/side.tsx | 1868 ++++++++--------- apps/dokploy/components/ui/time-badge.tsx | 60 + apps/dokploy/server/api/routers/server.ts | 6 + 4 files changed, 1463 insertions(+), 1423 deletions(-) create mode 100644 apps/dokploy/components/ui/time-badge.tsx diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 783c5bb32..8531e6b97 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -1,13 +1,13 @@ import { - AlertTriangle, - ArrowUpDown, - BookIcon, - ExternalLinkIcon, - FolderInput, - Loader2, - MoreHorizontalIcon, - Search, - TrashIcon, + AlertTriangle, + ArrowUpDown, + BookIcon, + ExternalLinkIcon, + FolderInput, + Loader2, + MoreHorizontalIcon, + Search, + TrashIcon, } from "lucide-react"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; @@ -16,497 +16,497 @@ import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; import { HandleProject } from "./handle-project"; import { ProjectEnvironment } from "./project-environment"; +import { TimeBadge } from "@/components/ui/time-badge"; export const ShowProjects = () => { - const utils = api.useUtils(); - const { data, isLoading } = api.project.all.useQuery(); - const { data: auth } = api.user.get.useQuery(); - const { mutateAsync } = api.project.remove.useMutation(); - const [searchQuery, setSearchQuery] = useState(""); - const [sortBy, setSortBy] = useState(() => { - if (typeof window !== "undefined") { - return localStorage.getItem("projectsSort") || "createdAt-desc"; - } - return "createdAt-desc"; - }); + const utils = api.useUtils(); + const { data, isLoading } = api.project.all.useQuery(); + const { data: auth } = api.user.get.useQuery(); + const { mutateAsync } = api.project.remove.useMutation(); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("projectsSort") || "createdAt-desc"; + } + return "createdAt-desc"; + }); - useEffect(() => { - localStorage.setItem("projectsSort", sortBy); - }, [sortBy]); + useEffect(() => { + localStorage.setItem("projectsSort", sortBy); + }, [sortBy]); - const filteredProjects = useMemo(() => { - if (!data) return []; + const filteredProjects = useMemo(() => { + if (!data) return []; - // First filter by search query - const filtered = data.filter( - (project) => - project.name.toLowerCase().includes(searchQuery.toLowerCase()) || - project.description?.toLowerCase().includes(searchQuery.toLowerCase()), - ); + // First filter by search query + const filtered = data.filter( + (project) => + project.name.toLowerCase().includes(searchQuery.toLowerCase()) || + project.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ); - // Then sort the filtered results - const [field, direction] = sortBy.split("-"); - return [...filtered].sort((a, b) => { - let comparison = 0; - switch (field) { - case "name": - comparison = a.name.localeCompare(b.name); - break; - case "createdAt": - comparison = - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); - break; - case "services": { - const aTotalServices = a.environments.reduce((total, env) => { - return ( - total + - (env.applications?.length || 0) + - (env.mariadb?.length || 0) + - (env.mongo?.length || 0) + - (env.mysql?.length || 0) + - (env.postgres?.length || 0) + - (env.redis?.length || 0) + - (env.compose?.length || 0) - ); - }, 0); - const bTotalServices = b.environments.reduce((total, env) => { - return ( - total + - (env.applications?.length || 0) + - (env.mariadb?.length || 0) + - (env.mongo?.length || 0) + - (env.mysql?.length || 0) + - (env.postgres?.length || 0) + - (env.redis?.length || 0) + - (env.compose?.length || 0) - ); - }, 0); - comparison = aTotalServices - bTotalServices; - break; - } - default: - comparison = 0; - } - return direction === "asc" ? comparison : -comparison; - }); - }, [data, searchQuery, sortBy]); + // Then sort the filtered results + const [field, direction] = sortBy.split("-"); + return [...filtered].sort((a, b) => { + let comparison = 0; + switch (field) { + case "name": + comparison = a.name.localeCompare(b.name); + break; + case "createdAt": + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + case "services": { + const aTotalServices = a.environments.reduce((total, env) => { + return ( + total + + (env.applications?.length || 0) + + (env.mariadb?.length || 0) + + (env.mongo?.length || 0) + + (env.mysql?.length || 0) + + (env.postgres?.length || 0) + + (env.redis?.length || 0) + + (env.compose?.length || 0) + ); + }, 0); + const bTotalServices = b.environments.reduce((total, env) => { + return ( + total + + (env.applications?.length || 0) + + (env.mariadb?.length || 0) + + (env.mongo?.length || 0) + + (env.mysql?.length || 0) + + (env.postgres?.length || 0) + + (env.redis?.length || 0) + + (env.compose?.length || 0) + ); + }, 0); + comparison = aTotalServices - bTotalServices; + break; + } + default: + comparison = 0; + } + return direction === "asc" ? comparison : -comparison; + }); + }, [data, searchQuery, sortBy]); - return ( - <> - -
- -
-
- - - - Projects - - - Create and manage your projects - - + return ( + <> + +
+ +
+
+ +
+
+ + + + Projects + + + Create and manage your projects + + + {(auth?.role === "owner" || auth?.canCreateProjects) && ( +
+ +
+ )} +
- {(auth?.role === "owner" || auth?.canCreateProjects) && ( -
- -
- )} -
+ + {isLoading ? ( +
+ Loading... + +
+ ) : ( + <> +
+
+ setSearchQuery(e.target.value)} + className="pr-10" + /> - - {isLoading ? ( -
- Loading... - -
- ) : ( - <> -
-
- setSearchQuery(e.target.value)} - className="pr-10" - /> + +
+
+ + +
+
+ {filteredProjects?.length === 0 && ( +
+ + + No projects found + +
+ )} +
+ {filteredProjects?.map((project) => { + const emptyServices = project?.environments + .map( + (env) => + env.applications.length === 0 && + env.mariadb.length === 0 && + env.mongo.length === 0 && + env.mysql.length === 0 && + env.postgres.length === 0 && + env.redis.length === 0 && + env.applications.length === 0 && + env.compose.length === 0 + ) + .every(Boolean); - -
-
- - -
-
- {filteredProjects?.length === 0 && ( -
- - - No projects found - -
- )} -
- {filteredProjects?.map((project) => { - const emptyServices = project?.environments - .map( - (env) => - env.applications.length === 0 && - env.mariadb.length === 0 && - env.mongo.length === 0 && - env.mysql.length === 0 && - env.postgres.length === 0 && - env.redis.length === 0 && - env.applications.length === 0 && - env.compose.length === 0, - ) - .every(Boolean); + const totalServices = project?.environments + .map( + (env) => + env.mariadb.length + + env.mongo.length + + env.mysql.length + + env.postgres.length + + env.redis.length + + env.applications.length + + env.compose.length + ) + .reduce((acc, curr) => acc + curr, 0); - const totalServices = project?.environments - .map( - (env) => - env.mariadb.length + - env.mongo.length + - env.mysql.length + - env.postgres.length + - env.redis.length + - env.applications.length + - env.compose.length, - ) - .reduce((acc, curr) => acc + curr, 0); + const haveServicesWithDomains = project?.environments + .map( + (env) => + env.applications.length > 0 || + env.compose.length > 0 + ) + .some(Boolean); - const haveServicesWithDomains = project?.environments - .map( - (env) => - env.applications.length > 0 || - env.compose.length > 0, - ) - .some(Boolean); + return ( +
+ + + {haveServicesWithDomains ? ( + + + + + e.stopPropagation()}> + {project.environments.some( + (env) => env.applications.length > 0 + ) && ( + + + Applications + + {project.environments.map((env) => + env.applications.map((app) => ( +
+ + + + {app.name} + + + + {app.domains.map((domain) => ( + + + + {domain.host} + + + + + ))} + +
+ )) + )} +
+ )} + {project.environments.some( + (env) => env.compose.length > 0 + ) && ( + + + Compose + + {project.environments.map((env) => + env.compose.map((comp) => ( +
+ + + + {comp.name} + + + + {comp.domains.map((domain) => ( + + + + {domain.host} + + + + + ))} + +
+ )) + )} +
+ )} +
+
+ ) : null} + + + +
+ + + {project.name} + +
- return ( -
- - - {haveServicesWithDomains ? ( - - - - - e.stopPropagation()} - > - {project.environments.some( - (env) => env.applications.length > 0, - ) && ( - - - Applications - - {project.environments.map((env) => - env.applications.map((app) => ( -
- - - - {app.name} - - - - {app.domains.map((domain) => ( - - - - {domain.host} - - - - - ))} - -
- )), - )} -
- )} - {project.environments.some( - (env) => env.compose.length > 0, - ) && ( - - - Compose - - {project.environments.map((env) => - env.compose.map((comp) => ( -
- - - - {comp.name} - - - - {comp.domains.map((domain) => ( - - - - {domain.host} - - - - - ))} - -
- )), - )} -
- )} -
-
- ) : null} - - - -
- - - {project.name} - -
+ + {project.description} + +
+
+ + + + + e.stopPropagation()}> + + Actions + +
e.stopPropagation()}> + +
+
e.stopPropagation()}> + +
- - {project.description} - - -
- - - - - e.stopPropagation()} - > - - Actions - -
e.stopPropagation()} - > - -
-
e.stopPropagation()} - > - -
- -
e.stopPropagation()} - > - {(auth?.role === "owner" || - auth?.canDeleteProjects) && ( - - - - e.preventDefault() - } - > - - Delete - - - - - - Are you sure to delete this - project? - - {!emptyServices ? ( -
- - - You have active - services, please delete - them first - -
- ) : ( - - This action cannot be - undone - - )} -
- - - Cancel - - { - await mutateAsync({ - projectId: - project.projectId, - }) - .then(() => { - toast.success( - "Project deleted successfully", - ); - }) - .catch(() => { - toast.error( - "Error deleting this project", - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }} - > - Delete - - -
-
- )} -
-
-
-
- - - -
- - Created - - - {totalServices}{" "} - {totalServices === 1 - ? "service" - : "services"} - -
-
- - -
- ); - })} -
- - )} - -
- -
- - ); +
e.stopPropagation()}> + {(auth?.role === "owner" || + auth?.canDeleteProjects) && ( + + + + e.preventDefault() + }> + + Delete + + + + + + Are you sure to delete this + project? + + {!emptyServices ? ( +
+ + + You have active + services, please delete + them first + +
+ ) : ( + + This action cannot be + undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: + project.projectId, + }) + .then(() => { + toast.success( + "Project deleted successfully" + ); + }) + .catch(() => { + toast.error( + "Error deleting this project" + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }}> + Delete + + +
+
+ )} +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 + ? "service" + : "services"} + +
+
+
+ +
+ ); + })} +
+ + )} + +
+
+
+ + ); }; diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index d1d4ae273..9833766f0 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -1,78 +1,78 @@ "use client"; import type { inferRouterOutputs } from "@trpc/server"; import { - Activity, - BarChartHorizontalBigIcon, - Bell, - BlocksIcon, - BookIcon, - BotIcon, - Boxes, - ChevronRight, - ChevronsUpDown, - CircleHelp, - Clock, - CreditCard, - Database, - Folder, - Forward, - GalleryVerticalEnd, - GitBranch, - HeartIcon, - KeyRound, - Loader2, - type LucideIcon, - Package, - PieChart, - Server, - ShieldCheck, - Trash2, - User, - Users, + Activity, + BarChartHorizontalBigIcon, + Bell, + BlocksIcon, + BookIcon, + BotIcon, + Boxes, + ChevronRight, + ChevronsUpDown, + CircleHelp, + Clock, + CreditCard, + Database, + Folder, + Forward, + GalleryVerticalEnd, + GitBranch, + HeartIcon, + KeyRound, + Loader2, + type LucideIcon, + Package, + PieChart, + Server, + ShieldCheck, + Trash2, + User, + Users, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, } from "@/components/ui/breadcrumb"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, } from "@/components/ui/collapsible"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Separator } from "@/components/ui/separator"; import { - SIDEBAR_COOKIE_NAME, - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupLabel, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarTrigger, - useSidebar, + SIDEBAR_COOKIE_NAME, + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarTrigger, + useSidebar, } from "@/components/ui/sidebar"; import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; @@ -82,6 +82,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization"; import { DialogAction } from "../shared/dialog-action"; import { Logo } from "../shared/logo"; import { Button } from "../ui/button"; +import { TimeBadge } from "../ui/time-badge"; import { UpdateServerButton } from "./update-server"; import { UserNav } from "./user-nav"; @@ -89,11 +90,11 @@ import { UserNav } from "./user-nav"; type AuthQueryOutput = inferRouterOutputs["user"]["get"]; type SingleNavItem = { - isSingle?: true; - title: string; - url: string; - icon?: LucideIcon; - isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; + isSingle?: true; + title: string; + url: string; + icon?: LucideIcon; + isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; }; // NavItem type @@ -101,33 +102,33 @@ type SingleNavItem = { // If `isSingle` is true or undefined, the item is a single item // If `isSingle` is false, the item is a group of items type NavItem = - | SingleNavItem - | { - isSingle: false; - title: string; - icon: LucideIcon; - items: SingleNavItem[]; - isEnabled?: (opts: { - auth?: AuthQueryOutput; - isCloud: boolean; - }) => boolean; - }; + | SingleNavItem + | { + isSingle: false; + title: string; + icon: LucideIcon; + items: SingleNavItem[]; + isEnabled?: (opts: { + auth?: AuthQueryOutput; + isCloud: boolean; + }) => boolean; + }; // ExternalLink type // Represents an external link item (used for the help section) type ExternalLink = { - name: string; - url: string; - icon: React.ComponentType<{ className?: string }>; - isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; + name: string; + url: string; + icon: React.ComponentType<{ className?: string }>; + isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; }; // Menu type // Consists of home, settings, and help items type Menu = { - home: NavItem[]; - settings: NavItem[]; - help: ExternalLink[]; + home: NavItem[]; + settings: NavItem[]; + help: ExternalLink[]; }; // Menu items @@ -135,257 +136,257 @@ type Menu = { // The items are filtered based on the user's role and permissions // The `isEnabled` function is called to determine if the item should be displayed const MENU: Menu = { - home: [ - { - isSingle: true, - title: "Projects", - url: "/dashboard/projects", - icon: Folder, - }, - { - isSingle: true, - title: "Monitoring", - url: "/dashboard/monitoring", - icon: BarChartHorizontalBigIcon, - // Only enabled in non-cloud environments - isEnabled: ({ isCloud }) => !isCloud, - }, - { - isSingle: true, - title: "Schedules", - url: "/dashboard/schedules", - icon: Clock, - // Only enabled in non-cloud environments - isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner", - }, - { - isSingle: true, - title: "Traefik File System", - url: "/dashboard/traefik", - icon: GalleryVerticalEnd, - // Only enabled for admins and users with access to Traefik files in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!( - (auth?.role === "owner" || auth?.canAccessToTraefikFiles) && - !isCloud - ), - }, - { - isSingle: true, - title: "Docker", - url: "/dashboard/docker", - icon: BlocksIcon, - // Only enabled for admins and users with access to Docker in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), - }, - { - isSingle: true, - title: "Swarm", - url: "/dashboard/swarm", - icon: PieChart, - // Only enabled for admins and users with access to Docker in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), - }, - { - isSingle: true, - title: "Requests", - url: "/dashboard/requests", - icon: Forward, - // Only enabled for admins and users with access to Docker in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), - }, + home: [ + { + isSingle: true, + title: "Projects", + url: "/dashboard/projects", + icon: Folder, + }, + { + isSingle: true, + title: "Monitoring", + url: "/dashboard/monitoring", + icon: BarChartHorizontalBigIcon, + // Only enabled in non-cloud environments + isEnabled: ({ isCloud }) => !isCloud, + }, + { + isSingle: true, + title: "Schedules", + url: "/dashboard/schedules", + icon: Clock, + // Only enabled in non-cloud environments + isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner", + }, + { + isSingle: true, + title: "Traefik File System", + url: "/dashboard/traefik", + icon: GalleryVerticalEnd, + // Only enabled for admins and users with access to Traefik files in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!( + (auth?.role === "owner" || auth?.canAccessToTraefikFiles) && + !isCloud + ), + }, + { + isSingle: true, + title: "Docker", + url: "/dashboard/docker", + icon: BlocksIcon, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), + }, + { + isSingle: true, + title: "Swarm", + url: "/dashboard/swarm", + icon: PieChart, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), + }, + { + isSingle: true, + title: "Requests", + url: "/dashboard/requests", + icon: Forward, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), + }, - // Legacy unused menu, adjusted to the new structure - // { - // isSingle: true, - // title: "Projects", - // url: "/dashboard/projects", - // icon: Folder, - // }, - // { - // isSingle: true, - // title: "Monitoring", - // icon: BarChartHorizontalBigIcon, - // url: "/dashboard/settings/monitoring", - // }, - // { - // isSingle: false, - // title: "Settings", - // icon: Settings2, - // items: [ - // { - // title: "Profile", - // url: "/dashboard/settings/profile", - // }, - // { - // title: "Users", - // url: "/dashboard/settings/users", - // }, - // { - // title: "SSH Key", - // url: "/dashboard/settings/ssh-keys", - // }, - // { - // title: "Git", - // url: "/dashboard/settings/git-providers", - // }, - // ], - // }, - // { - // isSingle: false, - // title: "Integrations", - // icon: BlocksIcon, - // items: [ - // { - // title: "S3 Destinations", - // url: "/dashboard/settings/destinations", - // }, - // { - // title: "Registry", - // url: "/dashboard/settings/registry", - // }, - // { - // title: "Notifications", - // url: "/dashboard/settings/notifications", - // }, - // ], - // }, - ], + // Legacy unused menu, adjusted to the new structure + // { + // isSingle: true, + // title: "Projects", + // url: "/dashboard/projects", + // icon: Folder, + // }, + // { + // isSingle: true, + // title: "Monitoring", + // icon: BarChartHorizontalBigIcon, + // url: "/dashboard/settings/monitoring", + // }, + // { + // isSingle: false, + // title: "Settings", + // icon: Settings2, + // items: [ + // { + // title: "Profile", + // url: "/dashboard/settings/profile", + // }, + // { + // title: "Users", + // url: "/dashboard/settings/users", + // }, + // { + // title: "SSH Key", + // url: "/dashboard/settings/ssh-keys", + // }, + // { + // title: "Git", + // url: "/dashboard/settings/git-providers", + // }, + // ], + // }, + // { + // isSingle: false, + // title: "Integrations", + // icon: BlocksIcon, + // items: [ + // { + // title: "S3 Destinations", + // url: "/dashboard/settings/destinations", + // }, + // { + // title: "Registry", + // url: "/dashboard/settings/registry", + // }, + // { + // title: "Notifications", + // url: "/dashboard/settings/notifications", + // }, + // ], + // }, + ], - settings: [ - { - isSingle: true, - title: "Web Server", - url: "/dashboard/settings/server", - icon: Activity, - // Only enabled for admins in non-cloud environments - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), - }, - { - isSingle: true, - title: "Profile", - url: "/dashboard/settings/profile", - icon: User, - }, - { - isSingle: true, - title: "Remote Servers", - url: "/dashboard/settings/servers", - icon: Server, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Users", - icon: Users, - url: "/dashboard/settings/users", - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "SSH Keys", - icon: KeyRound, - url: "/dashboard/settings/ssh-keys", - // Only enabled for admins and users with access to SSH keys - isEnabled: ({ auth }) => - !!(auth?.role === "owner" || auth?.canAccessToSSHKeys), - }, - { - title: "AI", - icon: BotIcon, - url: "/dashboard/settings/ai", - isSingle: true, - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Git", - url: "/dashboard/settings/git-providers", - icon: GitBranch, - // Only enabled for admins and users with access to Git providers - isEnabled: ({ auth }) => - !!(auth?.role === "owner" || auth?.canAccessToGitProviders), - }, - { - isSingle: true, - title: "Registry", - url: "/dashboard/settings/registry", - icon: Package, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "S3 Destinations", - url: "/dashboard/settings/destinations", - icon: Database, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, + settings: [ + { + isSingle: true, + title: "Web Server", + url: "/dashboard/settings/server", + icon: Activity, + // Only enabled for admins in non-cloud environments + isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), + }, + { + isSingle: true, + title: "Profile", + url: "/dashboard/settings/profile", + icon: User, + }, + { + isSingle: true, + title: "Remote Servers", + url: "/dashboard/settings/servers", + icon: Server, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Users", + icon: Users, + url: "/dashboard/settings/users", + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "SSH Keys", + icon: KeyRound, + url: "/dashboard/settings/ssh-keys", + // Only enabled for admins and users with access to SSH keys + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.canAccessToSSHKeys), + }, + { + title: "AI", + icon: BotIcon, + url: "/dashboard/settings/ai", + isSingle: true, + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Git", + url: "/dashboard/settings/git-providers", + icon: GitBranch, + // Only enabled for admins and users with access to Git providers + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.canAccessToGitProviders), + }, + { + isSingle: true, + title: "Registry", + url: "/dashboard/settings/registry", + icon: Package, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "S3 Destinations", + url: "/dashboard/settings/destinations", + icon: Database, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, - { - isSingle: true, - title: "Certificates", - url: "/dashboard/settings/certificates", - icon: ShieldCheck, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Cluster", - url: "/dashboard/settings/cluster", - icon: Boxes, - // Only enabled for admins in non-cloud environments - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), - }, - { - isSingle: true, - title: "Notifications", - url: "/dashboard/settings/notifications", - icon: Bell, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Billing", - url: "/dashboard/settings/billing", - icon: CreditCard, - // Only enabled for admins in cloud environments - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud), - }, - ], + { + isSingle: true, + title: "Certificates", + url: "/dashboard/settings/certificates", + icon: ShieldCheck, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Cluster", + url: "/dashboard/settings/cluster", + icon: Boxes, + // Only enabled for admins in non-cloud environments + isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), + }, + { + isSingle: true, + title: "Notifications", + url: "/dashboard/settings/notifications", + icon: Bell, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Billing", + url: "/dashboard/settings/billing", + icon: CreditCard, + // Only enabled for admins in cloud environments + isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud), + }, + ], - help: [ - { - name: "Documentation", - url: "https://docs.dokploy.com/docs/core", - icon: BookIcon, - }, - { - name: "Support", - url: "https://discord.gg/2tBnJ3jDJc", - icon: CircleHelp, - }, - { - name: "Sponsor", - url: "https://opencollective.com/dokploy", - icon: ({ className }) => ( - - ), - }, - ], + help: [ + { + name: "Documentation", + url: "https://docs.dokploy.com/docs/core", + icon: BookIcon, + }, + { + name: "Support", + url: "https://discord.gg/2tBnJ3jDJc", + icon: CircleHelp, + }, + { + name: "Sponsor", + url: "https://opencollective.com/dokploy", + icon: ({ className }) => ( + + ), + }, + ], } as const; /** @@ -393,41 +394,41 @@ const MENU: Menu = { * @returns a menu object with the home, settings, and help items */ function createMenuForAuthUser(opts: { - auth?: AuthQueryOutput; - isCloud: boolean; + auth?: AuthQueryOutput; + isCloud: boolean; }): Menu { - return { - // Filter the home items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - home: MENU.home.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }), - ), - // Filter the settings items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - settings: MENU.settings.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }), - ), - // Filter the help items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - help: MENU.help.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }), - ), - }; + return { + // Filter the home items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + home: MENU.home.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + isCloud: opts.isCloud, + }) + ), + // Filter the settings items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + settings: MENU.settings.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + isCloud: opts.isCloud, + }) + ), + // Filter the help items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + help: MENU.help.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + isCloud: opts.isCloud, + }) + ), + }; } /** @@ -435,24 +436,24 @@ function createMenuForAuthUser(opts: { * @returns true if the item url is active, false otherwise */ function isActiveRoute(opts: { - /** The url of the item. Usually obtained from `item.url` */ - itemUrl: string; - /** The current pathname. Usually obtained from `usePathname()` */ - pathname: string; + /** The url of the item. Usually obtained from `item.url` */ + itemUrl: string; + /** The current pathname. Usually obtained from `usePathname()` */ + pathname: string; }): boolean { - const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project"); - const normalizedPathname = opts.pathname?.replace("/projects", "/project"); + const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project"); + const normalizedPathname = opts.pathname?.replace("/projects", "/project"); - if (!normalizedPathname) return false; + if (!normalizedPathname) return false; - if (normalizedPathname === normalizedItemUrl) return true; + if (normalizedPathname === normalizedItemUrl) return true; - if (normalizedPathname.startsWith(normalizedItemUrl)) { - const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); - return nextChar === "/"; - } + if (normalizedPathname.startsWith(normalizedItemUrl)) { + const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); + return nextChar === "/"; + } - return false; + return false; } /** @@ -460,614 +461,587 @@ function isActiveRoute(opts: { * @returns the active nav item with `SingleNavItem` type or undefined if none is active */ function findActiveNavItem( - navItems: NavItem[], - pathname: string, + navItems: NavItem[], + pathname: string ): SingleNavItem | undefined { - const found = navItems.find((item) => - item.isSingle !== false - ? // The current item is single, so check if the item url is active - isActiveRoute({ itemUrl: item.url, pathname }) - : // The current item is not single, so check if any of the sub items are active - item.items.some((item) => - isActiveRoute({ itemUrl: item.url, pathname }), - ), - ); + const found = navItems.find((item) => + item.isSingle !== false + ? // The current item is single, so check if the item url is active + isActiveRoute({ itemUrl: item.url, pathname }) + : // The current item is not single, so check if any of the sub items are active + item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }) + ) + ); - if (found?.isSingle !== false) { - // The found item is single, so return it - return found; - } + if (found?.isSingle !== false) { + // The found item is single, so return it + return found; + } - // The found item is not single, so find the active sub item - return found?.items.find((item) => - isActiveRoute({ itemUrl: item.url, pathname }), - ); + // The found item is not single, so find the active sub item + return found?.items.find((item) => + isActiveRoute({ itemUrl: item.url, pathname }) + ); } interface Props { - children: React.ReactNode; + children: React.ReactNode; } function LogoWrapper() { - return ; + return ; } function SidebarLogo() { - const { state } = useSidebar(); - const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: user } = api.user.get.useQuery(); - const { data: session } = authClient.useSession(); + const { state } = useSidebar(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: user } = api.user.get.useQuery(); + const { data: session } = authClient.useSession(); - const { - data: organizations, - refetch, - isLoading, - } = api.organization.all.useQuery(); - const { mutateAsync: deleteOrganization, isLoading: isRemoving } = - api.organization.delete.useMutation(); - const { isMobile } = useSidebar(); - const { data: activeOrganization } = authClient.useActiveOrganization(); - const _utils = api.useUtils(); + const { + data: organizations, + refetch, + isLoading, + } = api.organization.all.useQuery(); + const { mutateAsync: deleteOrganization, isLoading: isRemoving } = + api.organization.delete.useMutation(); + const { isMobile } = useSidebar(); + const { data: activeOrganization } = authClient.useActiveOrganization(); + const _utils = api.useUtils(); - const { data: invitations, refetch: refetchInvitations } = - api.user.getInvitations.useQuery(); + const { data: invitations, refetch: refetchInvitations } = + api.user.getInvitations.useQuery(); - const [_activeTeam, setActiveTeam] = useState< - typeof activeOrganization | null - >(null); + const [_activeTeam, setActiveTeam] = useState< + typeof activeOrganization | null + >(null); - useEffect(() => { - if (activeOrganization) { - setActiveTeam(activeOrganization); - } - }, [activeOrganization]); + useEffect(() => { + if (activeOrganization) { + setActiveTeam(activeOrganization); + } + }, [activeOrganization]); - return ( - <> - {isLoading ? ( -
- -
- ) : ( - - {/* Organization Logo and Selector */} - - - - -
-
- -
-
-

- {activeOrganization?.name ?? "Select Organization"} -

-
-
- -
-
- - - Organizations - - {organizations?.map((org) => ( -
- { - await authClient.organization.setActive({ - organizationId: org.id, - }); - window.location.reload(); - }} - className="w-full gap-2 p-2" - > -
{org.name}
-
- -
-
- {org.ownerId === session?.user?.id && ( -
- - { - await deleteOrganization({ - organizationId: org.id, - }) - .then(() => { - refetch(); - toast.success( - "Organization deleted successfully", - ); - }) - .catch((error) => { - toast.error( - error?.message || - "Error deleting organization", - ); - }); - }} - > - - -
- )} -
- ))} - {(user?.role === "owner" || isCloud) && ( - <> - - - - )} -
-
-
+ return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + + {/* Organization Logo and Selector */} + + + + +
+
+ +
+
+

+ {activeOrganization?.name ?? "Select Organization"} +

+
+
+ +
+
+ + + Organizations + + {organizations?.map((org) => ( +
+ { + await authClient.organization.setActive({ + organizationId: org.id, + }); + window.location.reload(); + }} + className="w-full gap-2 p-2"> +
{org.name}
+
+ +
+
+ {org.ownerId === session?.user?.id && ( +
+ + { + await deleteOrganization({ + organizationId: org.id, + }) + .then(() => { + refetch(); + toast.success( + "Organization deleted successfully" + ); + }) + .catch((error) => { + toast.error( + error?.message || + "Error deleting organization" + ); + }); + }}> + + +
+ )} +
+ ))} + {(user?.role === "owner" || isCloud) && ( + <> + + + + )} +
+
+
- {/* Notification Bell */} - - - - - - - Pending Invitations -
- {invitations && invitations.length > 0 ? ( - invitations.map((invitation) => ( -
- e.preventDefault()} - > -
- {invitation?.organization?.name} -
-
- Expires:{" "} - {new Date(invitation.expiresAt).toLocaleString()} -
-
- Role: {invitation.role} -
-
- { - const { error } = - await authClient.organization.acceptInvitation({ - invitationId: invitation.id, - }); + {/* Notification Bell */} + + + + + + + Pending Invitations +
+ {invitations && invitations.length > 0 ? ( + invitations.map((invitation) => ( +
+ e.preventDefault()}> +
+ {invitation?.organization?.name} +
+
+ Expires:{" "} + {new Date(invitation.expiresAt).toLocaleString()} +
+
+ Role: {invitation.role} +
+
+ { + const { error } = + await authClient.organization.acceptInvitation({ + invitationId: invitation.id, + }); - if (error) { - toast.error( - error.message || "Error accepting invitation", - ); - } else { - toast.success("Invitation accepted successfully"); - await refetchInvitations(); - await refetch(); - } - }} - > - - -
- )) - ) : ( - - No pending invitations - - )} -
-
-
-
- - )} - - ); + if (error) { + toast.error( + error.message || "Error accepting invitation" + ); + } else { + toast.success("Invitation accepted successfully"); + await refetchInvitations(); + await refetch(); + } + }}> + +
+
+ )) + ) : ( + + No pending invitations + + )} +
+
+
+
+
+ )} + + ); } export default function Page({ children }: Props) { - const [defaultOpen, setDefaultOpen] = useState( - undefined, - ); - const [isLoaded, setIsLoaded] = useState(false); + const [defaultOpen, setDefaultOpen] = useState( + undefined + ); + const [isLoaded, setIsLoaded] = useState(false); - useEffect(() => { - const cookieValue = document.cookie - .split("; ") - .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`)) - ?.split("=")[1]; + useEffect(() => { + const cookieValue = document.cookie + .split("; ") + .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`)) + ?.split("=")[1]; - setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true"); - setIsLoaded(true); - }, []); + setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true"); + setIsLoaded(true); + }, []); - const pathname = usePathname(); - const { data: auth } = api.user.get.useQuery(); - const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); + const pathname = usePathname(); + const { data: auth } = api.user.get.useQuery(); + const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); - const includesProjects = pathname?.includes("/dashboard/project"); - const { data: isCloud } = api.settings.isCloud.useQuery(); + const includesProjects = pathname?.includes("/dashboard/project"); + const { data: isCloud } = api.settings.isCloud.useQuery(); - const { - home: filteredHome, - settings: filteredSettings, - help, - } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); + const { + home: filteredHome, + settings: filteredSettings, + help, + } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); - const activeItem = findActiveNavItem( - [...filteredHome, ...filteredSettings], - pathname, - ); + const activeItem = findActiveNavItem( + [...filteredHome, ...filteredSettings], + pathname + ); - if (!isLoaded) { - return
; // Placeholder mientras se carga - } + if (!isLoaded) { + return
; // Placeholder mientras se carga + } - return ( - { - setDefaultOpen(open); + return ( + { + setDefaultOpen(open); - document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`; - }} - style={ - { - "--sidebar-width": "19.5rem", - "--sidebar-width-mobile": "19.5rem", - } as React.CSSProperties - } - > - - - {/* + + + {/* */} - - {/* */} - - - - Home - - {filteredHome.map((item) => { - const isSingle = item.isSingle !== false; - const isActive = isSingle - ? isActiveRoute({ itemUrl: item.url, pathname }) - : item.items.some((item) => - isActiveRoute({ itemUrl: item.url, pathname }), - ); + + {/* */} + + + + Home + + {filteredHome.map((item) => { + const isSingle = item.isSingle !== false; + const isActive = isSingle + ? isActiveRoute({ itemUrl: item.url, pathname }) + : item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }) + ); - return ( - - - {isSingle ? ( - - - {item.icon && ( - - )} - {item.title} - - - ) : ( - <> - - - {item.icon && } + return ( + + + {isSingle ? ( + + + {item.icon && ( + + )} + {item.title} + + + ) : ( + <> + + + {item.icon && } - {item.title} - {item.items?.length && ( - - )} - - - - - {item.items?.map((subItem) => ( - - - - {subItem.icon && ( - - - - )} - {subItem.title} - - - - ))} - - - - )} - - - ); - })} - - - - Settings - - {filteredSettings.map((item) => { - const isSingle = item.isSingle !== false; - const isActive = isSingle - ? isActiveRoute({ itemUrl: item.url, pathname }) - : item.items.some((item) => - isActiveRoute({ itemUrl: item.url, pathname }), - ); + {item.title} + {item.items?.length && ( + + )} + + + + + {item.items?.map((subItem) => ( + + + + {subItem.icon && ( + + + + )} + {subItem.title} + + + + ))} + + + + )} + + + ); + })} + + + + Settings + + {filteredSettings.map((item) => { + const isSingle = item.isSingle !== false; + const isActive = isSingle + ? isActiveRoute({ itemUrl: item.url, pathname }) + : item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }) + ); - return ( - - - {isSingle ? ( - - - {item.icon && ( - - )} - {item.title} - - - ) : ( - <> - - - {item.icon && } + return ( + + + {isSingle ? ( + + + {item.icon && ( + + )} + {item.title} + + + ) : ( + <> + + + {item.icon && } - {item.title} - {item.items?.length && ( - - )} - - - - - {item.items?.map((subItem) => ( - - - - {subItem.icon && ( - - - - )} - {subItem.title} - - - - ))} - - - - )} - - - ); - })} - - - - Extra - - {help.map((item: ExternalLink) => ( - - - - - - - {item.name} - - - - ))} - - - - - - {!isCloud && auth?.role === "owner" && ( - - - - )} - - - - {dokployVersion && ( - <> -
- Version {dokployVersion} -
-
- {dokployVersion} -
- - )} -
-
- -
- - {!includesProjects && ( -
-
-
- - - - - - - - {activeItem?.title} - - - - - -
-
-
- )} + {item.title} + {item.items?.length && ( + + )} + + + + + {item.items?.map((subItem) => ( + + + + {subItem.icon && ( + + + + )} + {subItem.title} + + + + ))} + + + + )} + + + ); + })} + + + + Extra + + {help.map((item: ExternalLink) => ( + + + + + + + {item.name} + + + + ))} + + + + + + {!isCloud && auth?.role === "owner" && ( + + + + )} + + + + {dokployVersion && ( + <> +
+ Version {dokployVersion} +
+
+ {dokployVersion} +
+ + )} +
+
+ + + + {!includesProjects && ( +
+
+
+ + + + + + + + {activeItem?.title} + + + + + +
+ +
+
+ )} -
{children}
-
-
- ); +
{children}
+ +
+ ); } diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx new file mode 100644 index 000000000..8d47297ea --- /dev/null +++ b/apps/dokploy/components/ui/time-badge.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { api } from "@/utils/api"; + +export function TimeBadge() { + const { data: serverTime } = api.server.getServerTime.useQuery(undefined, { + refetchInterval: 60000, // Refetch every 60 seconds + }); + const [time, setTime] = useState(null); + + useEffect(() => { + if (serverTime?.time) { + setTime(new Date(serverTime.time)); + } + }, [serverTime]); + + useEffect(() => { + const timer = setInterval(() => { + setTime((prevTime) => { + if (!prevTime) return null; + const newTime = new Date(prevTime.getTime() + 1000); + return newTime; + }); + }, 1000); + + return () => { + clearInterval(timer); + }; + }, []); + + if (!time || !serverTime?.timezone) { + return null; + } + + const getUtcOffset = (timeZone: string) => { + const date = new Date(); + const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); + const tzDate = new Date(date.toLocaleString("en-US", { timeZone })); + const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60); + const sign = offset >= 0 ? "+" : "-"; + const hours = Math.floor(Math.abs(offset)); + const minutes = (Math.abs(offset) * 60) % 60; + return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}`; + }; + + return ( +
+ Server Time: + + {time.toLocaleTimeString()} + + + ({serverTime.timezone} | {getUtcOffset(serverTime.timezone)}) + +
+ ); +} diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index d6904a7ec..4eb75bdf0 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -383,6 +383,12 @@ export const serverRouter = createTRPCRouter({ const ip = await getPublicIpWithFallback(); return ip; }), + getServerTime: protectedProcedure.query(() => { + return { + time: new Date(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }; + }), getServerMetrics: protectedProcedure .input( z.object({ From 53b66e41e2d2b37f676dfe345f92dec05fcd453f Mon Sep 17 00:00:00 2001 From: Aathil Felix Date: Sat, 1 Nov 2025 19:09:58 +0530 Subject: [PATCH 04/79] chore(ui): apply Biome format to time badge and headers --- .../components/dashboard/projects/show.tsx | 965 ++++----- apps/dokploy/components/layouts/side.tsx | 1868 +++++++++-------- apps/dokploy/components/ui/time-badge.tsx | 94 +- 3 files changed, 1485 insertions(+), 1442 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 8531e6b97..92f7ed5cb 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -1,13 +1,13 @@ import { - AlertTriangle, - ArrowUpDown, - BookIcon, - ExternalLinkIcon, - FolderInput, - Loader2, - MoreHorizontalIcon, - Search, - TrashIcon, + AlertTriangle, + ArrowUpDown, + BookIcon, + ExternalLinkIcon, + FolderInput, + Loader2, + MoreHorizontalIcon, + Search, + TrashIcon, } from "lucide-react"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; @@ -16,41 +16,41 @@ import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; import { HandleProject } from "./handle-project"; @@ -58,455 +58,470 @@ import { ProjectEnvironment } from "./project-environment"; import { TimeBadge } from "@/components/ui/time-badge"; export const ShowProjects = () => { - const utils = api.useUtils(); - const { data, isLoading } = api.project.all.useQuery(); - const { data: auth } = api.user.get.useQuery(); - const { mutateAsync } = api.project.remove.useMutation(); - const [searchQuery, setSearchQuery] = useState(""); - const [sortBy, setSortBy] = useState(() => { - if (typeof window !== "undefined") { - return localStorage.getItem("projectsSort") || "createdAt-desc"; - } - return "createdAt-desc"; - }); + const utils = api.useUtils(); + const { data, isLoading } = api.project.all.useQuery(); + const { data: auth } = api.user.get.useQuery(); + const { mutateAsync } = api.project.remove.useMutation(); + const [searchQuery, setSearchQuery] = useState(""); + const [sortBy, setSortBy] = useState(() => { + if (typeof window !== "undefined") { + return localStorage.getItem("projectsSort") || "createdAt-desc"; + } + return "createdAt-desc"; + }); - useEffect(() => { - localStorage.setItem("projectsSort", sortBy); - }, [sortBy]); + useEffect(() => { + localStorage.setItem("projectsSort", sortBy); + }, [sortBy]); - const filteredProjects = useMemo(() => { - if (!data) return []; + const filteredProjects = useMemo(() => { + if (!data) return []; - // First filter by search query - const filtered = data.filter( - (project) => - project.name.toLowerCase().includes(searchQuery.toLowerCase()) || - project.description?.toLowerCase().includes(searchQuery.toLowerCase()) - ); + // First filter by search query + const filtered = data.filter( + (project) => + project.name.toLowerCase().includes(searchQuery.toLowerCase()) || + project.description?.toLowerCase().includes(searchQuery.toLowerCase()), + ); - // Then sort the filtered results - const [field, direction] = sortBy.split("-"); - return [...filtered].sort((a, b) => { - let comparison = 0; - switch (field) { - case "name": - comparison = a.name.localeCompare(b.name); - break; - case "createdAt": - comparison = - new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); - break; - case "services": { - const aTotalServices = a.environments.reduce((total, env) => { - return ( - total + - (env.applications?.length || 0) + - (env.mariadb?.length || 0) + - (env.mongo?.length || 0) + - (env.mysql?.length || 0) + - (env.postgres?.length || 0) + - (env.redis?.length || 0) + - (env.compose?.length || 0) - ); - }, 0); - const bTotalServices = b.environments.reduce((total, env) => { - return ( - total + - (env.applications?.length || 0) + - (env.mariadb?.length || 0) + - (env.mongo?.length || 0) + - (env.mysql?.length || 0) + - (env.postgres?.length || 0) + - (env.redis?.length || 0) + - (env.compose?.length || 0) - ); - }, 0); - comparison = aTotalServices - bTotalServices; - break; - } - default: - comparison = 0; - } - return direction === "asc" ? comparison : -comparison; - }); - }, [data, searchQuery, sortBy]); + // Then sort the filtered results + const [field, direction] = sortBy.split("-"); + return [...filtered].sort((a, b) => { + let comparison = 0; + switch (field) { + case "name": + comparison = a.name.localeCompare(b.name); + break; + case "createdAt": + comparison = + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + case "services": { + const aTotalServices = a.environments.reduce((total, env) => { + return ( + total + + (env.applications?.length || 0) + + (env.mariadb?.length || 0) + + (env.mongo?.length || 0) + + (env.mysql?.length || 0) + + (env.postgres?.length || 0) + + (env.redis?.length || 0) + + (env.compose?.length || 0) + ); + }, 0); + const bTotalServices = b.environments.reduce((total, env) => { + return ( + total + + (env.applications?.length || 0) + + (env.mariadb?.length || 0) + + (env.mongo?.length || 0) + + (env.mysql?.length || 0) + + (env.postgres?.length || 0) + + (env.redis?.length || 0) + + (env.compose?.length || 0) + ); + }, 0); + comparison = aTotalServices - bTotalServices; + break; + } + default: + comparison = 0; + } + return direction === "asc" ? comparison : -comparison; + }); + }, [data, searchQuery, sortBy]); - return ( - <> - -
- -
-
- -
-
- - - - Projects - - - Create and manage your projects - - - {(auth?.role === "owner" || auth?.canCreateProjects) && ( -
- -
- )} -
+ return ( + <> + +
+ +
+
+ +
+
+ + + + Projects + + + Create and manage your projects + + + {(auth?.role === "owner" || auth?.canCreateProjects) && ( +
+ +
+ )} +
- - {isLoading ? ( -
- Loading... - -
- ) : ( - <> -
-
- setSearchQuery(e.target.value)} - className="pr-10" - /> + + {isLoading ? ( +
+ Loading... + +
+ ) : ( + <> +
+
+ setSearchQuery(e.target.value)} + className="pr-10" + /> - -
-
- - -
-
- {filteredProjects?.length === 0 && ( -
- - - No projects found - -
- )} -
- {filteredProjects?.map((project) => { - const emptyServices = project?.environments - .map( - (env) => - env.applications.length === 0 && - env.mariadb.length === 0 && - env.mongo.length === 0 && - env.mysql.length === 0 && - env.postgres.length === 0 && - env.redis.length === 0 && - env.applications.length === 0 && - env.compose.length === 0 - ) - .every(Boolean); + +
+
+ + +
+
+ {filteredProjects?.length === 0 && ( +
+ + + No projects found + +
+ )} +
+ {filteredProjects?.map((project) => { + const emptyServices = project?.environments + .map( + (env) => + env.applications.length === 0 && + env.mariadb.length === 0 && + env.mongo.length === 0 && + env.mysql.length === 0 && + env.postgres.length === 0 && + env.redis.length === 0 && + env.applications.length === 0 && + env.compose.length === 0, + ) + .every(Boolean); - const totalServices = project?.environments - .map( - (env) => - env.mariadb.length + - env.mongo.length + - env.mysql.length + - env.postgres.length + - env.redis.length + - env.applications.length + - env.compose.length - ) - .reduce((acc, curr) => acc + curr, 0); + const totalServices = project?.environments + .map( + (env) => + env.mariadb.length + + env.mongo.length + + env.mysql.length + + env.postgres.length + + env.redis.length + + env.applications.length + + env.compose.length, + ) + .reduce((acc, curr) => acc + curr, 0); - const haveServicesWithDomains = project?.environments - .map( - (env) => - env.applications.length > 0 || - env.compose.length > 0 - ) - .some(Boolean); + const haveServicesWithDomains = project?.environments + .map( + (env) => + env.applications.length > 0 || + env.compose.length > 0, + ) + .some(Boolean); - return ( -
- - - {haveServicesWithDomains ? ( - - - - - e.stopPropagation()}> - {project.environments.some( - (env) => env.applications.length > 0 - ) && ( - - - Applications - - {project.environments.map((env) => - env.applications.map((app) => ( -
- - - - {app.name} - - - - {app.domains.map((domain) => ( - - - - {domain.host} - - - - - ))} - -
- )) - )} -
- )} - {project.environments.some( - (env) => env.compose.length > 0 - ) && ( - - - Compose - - {project.environments.map((env) => - env.compose.map((comp) => ( -
- - - - {comp.name} - - - - {comp.domains.map((domain) => ( - - - - {domain.host} - - - - - ))} - -
- )) - )} -
- )} -
-
- ) : null} - - - -
- - - {project.name} - -
+ return ( +
+ + + {haveServicesWithDomains ? ( + + + + + e.stopPropagation()} + > + {project.environments.some( + (env) => env.applications.length > 0, + ) && ( + + + Applications + + {project.environments.map((env) => + env.applications.map((app) => ( +
+ + + + {app.name} + + + + {app.domains.map((domain) => ( + + + + {domain.host} + + + + + ))} + +
+ )), + )} +
+ )} + {project.environments.some( + (env) => env.compose.length > 0, + ) && ( + + + Compose + + {project.environments.map((env) => + env.compose.map((comp) => ( +
+ + + + {comp.name} + + + + {comp.domains.map((domain) => ( + + + + {domain.host} + + + + + ))} + +
+ )), + )} +
+ )} +
+
+ ) : null} + + + +
+ + + {project.name} + +
- - {project.description} - -
-
- - - - - e.stopPropagation()}> - - Actions - -
e.stopPropagation()}> - -
-
e.stopPropagation()}> - -
+ + {project.description} + + +
+ + + + + e.stopPropagation()} + > + + Actions + +
e.stopPropagation()} + > + +
+
e.stopPropagation()} + > + +
-
e.stopPropagation()}> - {(auth?.role === "owner" || - auth?.canDeleteProjects) && ( - - - - e.preventDefault() - }> - - Delete - - - - - - Are you sure to delete this - project? - - {!emptyServices ? ( -
- - - You have active - services, please delete - them first - -
- ) : ( - - This action cannot be - undone - - )} -
- - - Cancel - - { - await mutateAsync({ - projectId: - project.projectId, - }) - .then(() => { - toast.success( - "Project deleted successfully" - ); - }) - .catch(() => { - toast.error( - "Error deleting this project" - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }}> - Delete - - -
-
- )} -
-
-
-
- - - -
- - Created - - - {totalServices}{" "} - {totalServices === 1 - ? "service" - : "services"} - -
-
- - -
- ); - })} -
- - )} - -
- -
- - ); +
e.stopPropagation()} + > + {(auth?.role === "owner" || + auth?.canDeleteProjects) && ( + + + + e.preventDefault() + } + > + + Delete + + + + + + Are you sure to delete this + project? + + {!emptyServices ? ( +
+ + + You have active + services, please delete + them first + +
+ ) : ( + + This action cannot be + undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: + project.projectId, + }) + .then(() => { + toast.success( + "Project deleted successfully", + ); + }) + .catch(() => { + toast.error( + "Error deleting this project", + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }} + > + Delete + + +
+
+ )} +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 + ? "service" + : "services"} + +
+
+ + +
+ ); + })} +
+ + )} + +
+
+
+ + ); }; diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 9833766f0..3dae77de6 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -1,78 +1,78 @@ "use client"; import type { inferRouterOutputs } from "@trpc/server"; import { - Activity, - BarChartHorizontalBigIcon, - Bell, - BlocksIcon, - BookIcon, - BotIcon, - Boxes, - ChevronRight, - ChevronsUpDown, - CircleHelp, - Clock, - CreditCard, - Database, - Folder, - Forward, - GalleryVerticalEnd, - GitBranch, - HeartIcon, - KeyRound, - Loader2, - type LucideIcon, - Package, - PieChart, - Server, - ShieldCheck, - Trash2, - User, - Users, + Activity, + BarChartHorizontalBigIcon, + Bell, + BlocksIcon, + BookIcon, + BotIcon, + Boxes, + ChevronRight, + ChevronsUpDown, + CircleHelp, + Clock, + CreditCard, + Database, + Folder, + Forward, + GalleryVerticalEnd, + GitBranch, + HeartIcon, + KeyRound, + Loader2, + type LucideIcon, + Package, + PieChart, + Server, + ShieldCheck, + Trash2, + User, + Users, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, } from "@/components/ui/breadcrumb"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, } from "@/components/ui/collapsible"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Separator } from "@/components/ui/separator"; import { - SIDEBAR_COOKIE_NAME, - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupLabel, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarProvider, - SidebarRail, - SidebarTrigger, - useSidebar, + SIDEBAR_COOKIE_NAME, + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarInset, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + SidebarProvider, + SidebarRail, + SidebarTrigger, + useSidebar, } from "@/components/ui/sidebar"; import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; @@ -90,11 +90,11 @@ import { UserNav } from "./user-nav"; type AuthQueryOutput = inferRouterOutputs["user"]["get"]; type SingleNavItem = { - isSingle?: true; - title: string; - url: string; - icon?: LucideIcon; - isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; + isSingle?: true; + title: string; + url: string; + icon?: LucideIcon; + isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; }; // NavItem type @@ -102,33 +102,33 @@ type SingleNavItem = { // If `isSingle` is true or undefined, the item is a single item // If `isSingle` is false, the item is a group of items type NavItem = - | SingleNavItem - | { - isSingle: false; - title: string; - icon: LucideIcon; - items: SingleNavItem[]; - isEnabled?: (opts: { - auth?: AuthQueryOutput; - isCloud: boolean; - }) => boolean; - }; + | SingleNavItem + | { + isSingle: false; + title: string; + icon: LucideIcon; + items: SingleNavItem[]; + isEnabled?: (opts: { + auth?: AuthQueryOutput; + isCloud: boolean; + }) => boolean; + }; // ExternalLink type // Represents an external link item (used for the help section) type ExternalLink = { - name: string; - url: string; - icon: React.ComponentType<{ className?: string }>; - isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; + name: string; + url: string; + icon: React.ComponentType<{ className?: string }>; + isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; }; // Menu type // Consists of home, settings, and help items type Menu = { - home: NavItem[]; - settings: NavItem[]; - help: ExternalLink[]; + home: NavItem[]; + settings: NavItem[]; + help: ExternalLink[]; }; // Menu items @@ -136,257 +136,257 @@ type Menu = { // The items are filtered based on the user's role and permissions // The `isEnabled` function is called to determine if the item should be displayed const MENU: Menu = { - home: [ - { - isSingle: true, - title: "Projects", - url: "/dashboard/projects", - icon: Folder, - }, - { - isSingle: true, - title: "Monitoring", - url: "/dashboard/monitoring", - icon: BarChartHorizontalBigIcon, - // Only enabled in non-cloud environments - isEnabled: ({ isCloud }) => !isCloud, - }, - { - isSingle: true, - title: "Schedules", - url: "/dashboard/schedules", - icon: Clock, - // Only enabled in non-cloud environments - isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner", - }, - { - isSingle: true, - title: "Traefik File System", - url: "/dashboard/traefik", - icon: GalleryVerticalEnd, - // Only enabled for admins and users with access to Traefik files in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!( - (auth?.role === "owner" || auth?.canAccessToTraefikFiles) && - !isCloud - ), - }, - { - isSingle: true, - title: "Docker", - url: "/dashboard/docker", - icon: BlocksIcon, - // Only enabled for admins and users with access to Docker in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), - }, - { - isSingle: true, - title: "Swarm", - url: "/dashboard/swarm", - icon: PieChart, - // Only enabled for admins and users with access to Docker in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), - }, - { - isSingle: true, - title: "Requests", - url: "/dashboard/requests", - icon: Forward, - // Only enabled for admins and users with access to Docker in non-cloud environments - isEnabled: ({ auth, isCloud }) => - !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), - }, + home: [ + { + isSingle: true, + title: "Projects", + url: "/dashboard/projects", + icon: Folder, + }, + { + isSingle: true, + title: "Monitoring", + url: "/dashboard/monitoring", + icon: BarChartHorizontalBigIcon, + // Only enabled in non-cloud environments + isEnabled: ({ isCloud }) => !isCloud, + }, + { + isSingle: true, + title: "Schedules", + url: "/dashboard/schedules", + icon: Clock, + // Only enabled in non-cloud environments + isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner", + }, + { + isSingle: true, + title: "Traefik File System", + url: "/dashboard/traefik", + icon: GalleryVerticalEnd, + // Only enabled for admins and users with access to Traefik files in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!( + (auth?.role === "owner" || auth?.canAccessToTraefikFiles) && + !isCloud + ), + }, + { + isSingle: true, + title: "Docker", + url: "/dashboard/docker", + icon: BlocksIcon, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), + }, + { + isSingle: true, + title: "Swarm", + url: "/dashboard/swarm", + icon: PieChart, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), + }, + { + isSingle: true, + title: "Requests", + url: "/dashboard/requests", + icon: Forward, + // Only enabled for admins and users with access to Docker in non-cloud environments + isEnabled: ({ auth, isCloud }) => + !!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud), + }, - // Legacy unused menu, adjusted to the new structure - // { - // isSingle: true, - // title: "Projects", - // url: "/dashboard/projects", - // icon: Folder, - // }, - // { - // isSingle: true, - // title: "Monitoring", - // icon: BarChartHorizontalBigIcon, - // url: "/dashboard/settings/monitoring", - // }, - // { - // isSingle: false, - // title: "Settings", - // icon: Settings2, - // items: [ - // { - // title: "Profile", - // url: "/dashboard/settings/profile", - // }, - // { - // title: "Users", - // url: "/dashboard/settings/users", - // }, - // { - // title: "SSH Key", - // url: "/dashboard/settings/ssh-keys", - // }, - // { - // title: "Git", - // url: "/dashboard/settings/git-providers", - // }, - // ], - // }, - // { - // isSingle: false, - // title: "Integrations", - // icon: BlocksIcon, - // items: [ - // { - // title: "S3 Destinations", - // url: "/dashboard/settings/destinations", - // }, - // { - // title: "Registry", - // url: "/dashboard/settings/registry", - // }, - // { - // title: "Notifications", - // url: "/dashboard/settings/notifications", - // }, - // ], - // }, - ], + // Legacy unused menu, adjusted to the new structure + // { + // isSingle: true, + // title: "Projects", + // url: "/dashboard/projects", + // icon: Folder, + // }, + // { + // isSingle: true, + // title: "Monitoring", + // icon: BarChartHorizontalBigIcon, + // url: "/dashboard/settings/monitoring", + // }, + // { + // isSingle: false, + // title: "Settings", + // icon: Settings2, + // items: [ + // { + // title: "Profile", + // url: "/dashboard/settings/profile", + // }, + // { + // title: "Users", + // url: "/dashboard/settings/users", + // }, + // { + // title: "SSH Key", + // url: "/dashboard/settings/ssh-keys", + // }, + // { + // title: "Git", + // url: "/dashboard/settings/git-providers", + // }, + // ], + // }, + // { + // isSingle: false, + // title: "Integrations", + // icon: BlocksIcon, + // items: [ + // { + // title: "S3 Destinations", + // url: "/dashboard/settings/destinations", + // }, + // { + // title: "Registry", + // url: "/dashboard/settings/registry", + // }, + // { + // title: "Notifications", + // url: "/dashboard/settings/notifications", + // }, + // ], + // }, + ], - settings: [ - { - isSingle: true, - title: "Web Server", - url: "/dashboard/settings/server", - icon: Activity, - // Only enabled for admins in non-cloud environments - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), - }, - { - isSingle: true, - title: "Profile", - url: "/dashboard/settings/profile", - icon: User, - }, - { - isSingle: true, - title: "Remote Servers", - url: "/dashboard/settings/servers", - icon: Server, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Users", - icon: Users, - url: "/dashboard/settings/users", - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "SSH Keys", - icon: KeyRound, - url: "/dashboard/settings/ssh-keys", - // Only enabled for admins and users with access to SSH keys - isEnabled: ({ auth }) => - !!(auth?.role === "owner" || auth?.canAccessToSSHKeys), - }, - { - title: "AI", - icon: BotIcon, - url: "/dashboard/settings/ai", - isSingle: true, - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Git", - url: "/dashboard/settings/git-providers", - icon: GitBranch, - // Only enabled for admins and users with access to Git providers - isEnabled: ({ auth }) => - !!(auth?.role === "owner" || auth?.canAccessToGitProviders), - }, - { - isSingle: true, - title: "Registry", - url: "/dashboard/settings/registry", - icon: Package, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "S3 Destinations", - url: "/dashboard/settings/destinations", - icon: Database, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, + settings: [ + { + isSingle: true, + title: "Web Server", + url: "/dashboard/settings/server", + icon: Activity, + // Only enabled for admins in non-cloud environments + isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), + }, + { + isSingle: true, + title: "Profile", + url: "/dashboard/settings/profile", + icon: User, + }, + { + isSingle: true, + title: "Remote Servers", + url: "/dashboard/settings/servers", + icon: Server, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Users", + icon: Users, + url: "/dashboard/settings/users", + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "SSH Keys", + icon: KeyRound, + url: "/dashboard/settings/ssh-keys", + // Only enabled for admins and users with access to SSH keys + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.canAccessToSSHKeys), + }, + { + title: "AI", + icon: BotIcon, + url: "/dashboard/settings/ai", + isSingle: true, + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Git", + url: "/dashboard/settings/git-providers", + icon: GitBranch, + // Only enabled for admins and users with access to Git providers + isEnabled: ({ auth }) => + !!(auth?.role === "owner" || auth?.canAccessToGitProviders), + }, + { + isSingle: true, + title: "Registry", + url: "/dashboard/settings/registry", + icon: Package, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "S3 Destinations", + url: "/dashboard/settings/destinations", + icon: Database, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, - { - isSingle: true, - title: "Certificates", - url: "/dashboard/settings/certificates", - icon: ShieldCheck, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Cluster", - url: "/dashboard/settings/cluster", - icon: Boxes, - // Only enabled for admins in non-cloud environments - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), - }, - { - isSingle: true, - title: "Notifications", - url: "/dashboard/settings/notifications", - icon: Bell, - // Only enabled for admins - isEnabled: ({ auth }) => !!(auth?.role === "owner"), - }, - { - isSingle: true, - title: "Billing", - url: "/dashboard/settings/billing", - icon: CreditCard, - // Only enabled for admins in cloud environments - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud), - }, - ], + { + isSingle: true, + title: "Certificates", + url: "/dashboard/settings/certificates", + icon: ShieldCheck, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Cluster", + url: "/dashboard/settings/cluster", + icon: Boxes, + // Only enabled for admins in non-cloud environments + isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), + }, + { + isSingle: true, + title: "Notifications", + url: "/dashboard/settings/notifications", + icon: Bell, + // Only enabled for admins + isEnabled: ({ auth }) => !!(auth?.role === "owner"), + }, + { + isSingle: true, + title: "Billing", + url: "/dashboard/settings/billing", + icon: CreditCard, + // Only enabled for admins in cloud environments + isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud), + }, + ], - help: [ - { - name: "Documentation", - url: "https://docs.dokploy.com/docs/core", - icon: BookIcon, - }, - { - name: "Support", - url: "https://discord.gg/2tBnJ3jDJc", - icon: CircleHelp, - }, - { - name: "Sponsor", - url: "https://opencollective.com/dokploy", - icon: ({ className }) => ( - - ), - }, - ], + help: [ + { + name: "Documentation", + url: "https://docs.dokploy.com/docs/core", + icon: BookIcon, + }, + { + name: "Support", + url: "https://discord.gg/2tBnJ3jDJc", + icon: CircleHelp, + }, + { + name: "Sponsor", + url: "https://opencollective.com/dokploy", + icon: ({ className }) => ( + + ), + }, + ], } as const; /** @@ -394,41 +394,41 @@ const MENU: Menu = { * @returns a menu object with the home, settings, and help items */ function createMenuForAuthUser(opts: { - auth?: AuthQueryOutput; - isCloud: boolean; + auth?: AuthQueryOutput; + isCloud: boolean; }): Menu { - return { - // Filter the home items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - home: MENU.home.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }) - ), - // Filter the settings items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - settings: MENU.settings.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }) - ), - // Filter the help items based on the user's role and permissions - // Calls the `isEnabled` function if it exists to determine if the item should be displayed - help: MENU.help.filter((item) => - !item.isEnabled - ? true - : item.isEnabled({ - auth: opts.auth, - isCloud: opts.isCloud, - }) - ), - }; + return { + // Filter the home items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + home: MENU.home.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + isCloud: opts.isCloud, + }), + ), + // Filter the settings items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + settings: MENU.settings.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + isCloud: opts.isCloud, + }), + ), + // Filter the help items based on the user's role and permissions + // Calls the `isEnabled` function if it exists to determine if the item should be displayed + help: MENU.help.filter((item) => + !item.isEnabled + ? true + : item.isEnabled({ + auth: opts.auth, + isCloud: opts.isCloud, + }), + ), + }; } /** @@ -436,24 +436,24 @@ function createMenuForAuthUser(opts: { * @returns true if the item url is active, false otherwise */ function isActiveRoute(opts: { - /** The url of the item. Usually obtained from `item.url` */ - itemUrl: string; - /** The current pathname. Usually obtained from `usePathname()` */ - pathname: string; + /** The url of the item. Usually obtained from `item.url` */ + itemUrl: string; + /** The current pathname. Usually obtained from `usePathname()` */ + pathname: string; }): boolean { - const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project"); - const normalizedPathname = opts.pathname?.replace("/projects", "/project"); + const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project"); + const normalizedPathname = opts.pathname?.replace("/projects", "/project"); - if (!normalizedPathname) return false; + if (!normalizedPathname) return false; - if (normalizedPathname === normalizedItemUrl) return true; + if (normalizedPathname === normalizedItemUrl) return true; - if (normalizedPathname.startsWith(normalizedItemUrl)) { - const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); - return nextChar === "/"; - } + if (normalizedPathname.startsWith(normalizedItemUrl)) { + const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); + return nextChar === "/"; + } - return false; + return false; } /** @@ -461,587 +461,615 @@ function isActiveRoute(opts: { * @returns the active nav item with `SingleNavItem` type or undefined if none is active */ function findActiveNavItem( - navItems: NavItem[], - pathname: string + navItems: NavItem[], + pathname: string, ): SingleNavItem | undefined { - const found = navItems.find((item) => - item.isSingle !== false - ? // The current item is single, so check if the item url is active - isActiveRoute({ itemUrl: item.url, pathname }) - : // The current item is not single, so check if any of the sub items are active - item.items.some((item) => - isActiveRoute({ itemUrl: item.url, pathname }) - ) - ); + const found = navItems.find((item) => + item.isSingle !== false + ? // The current item is single, so check if the item url is active + isActiveRoute({ itemUrl: item.url, pathname }) + : // The current item is not single, so check if any of the sub items are active + item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ), + ); - if (found?.isSingle !== false) { - // The found item is single, so return it - return found; - } + if (found?.isSingle !== false) { + // The found item is single, so return it + return found; + } - // The found item is not single, so find the active sub item - return found?.items.find((item) => - isActiveRoute({ itemUrl: item.url, pathname }) - ); + // The found item is not single, so find the active sub item + return found?.items.find((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ); } interface Props { - children: React.ReactNode; + children: React.ReactNode; } function LogoWrapper() { - return ; + return ; } function SidebarLogo() { - const { state } = useSidebar(); - const { data: isCloud } = api.settings.isCloud.useQuery(); - const { data: user } = api.user.get.useQuery(); - const { data: session } = authClient.useSession(); + const { state } = useSidebar(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const { data: user } = api.user.get.useQuery(); + const { data: session } = authClient.useSession(); - const { - data: organizations, - refetch, - isLoading, - } = api.organization.all.useQuery(); - const { mutateAsync: deleteOrganization, isLoading: isRemoving } = - api.organization.delete.useMutation(); - const { isMobile } = useSidebar(); - const { data: activeOrganization } = authClient.useActiveOrganization(); - const _utils = api.useUtils(); + const { + data: organizations, + refetch, + isLoading, + } = api.organization.all.useQuery(); + const { mutateAsync: deleteOrganization, isLoading: isRemoving } = + api.organization.delete.useMutation(); + const { isMobile } = useSidebar(); + const { data: activeOrganization } = authClient.useActiveOrganization(); + const _utils = api.useUtils(); - const { data: invitations, refetch: refetchInvitations } = - api.user.getInvitations.useQuery(); + const { data: invitations, refetch: refetchInvitations } = + api.user.getInvitations.useQuery(); - const [_activeTeam, setActiveTeam] = useState< - typeof activeOrganization | null - >(null); + const [_activeTeam, setActiveTeam] = useState< + typeof activeOrganization | null + >(null); - useEffect(() => { - if (activeOrganization) { - setActiveTeam(activeOrganization); - } - }, [activeOrganization]); + useEffect(() => { + if (activeOrganization) { + setActiveTeam(activeOrganization); + } + }, [activeOrganization]); - return ( - <> - {isLoading ? ( -
- -
- ) : ( - - {/* Organization Logo and Selector */} - - - - -
-
- -
-
-

- {activeOrganization?.name ?? "Select Organization"} -

-
-
- -
-
- - - Organizations - - {organizations?.map((org) => ( -
- { - await authClient.organization.setActive({ - organizationId: org.id, - }); - window.location.reload(); - }} - className="w-full gap-2 p-2"> -
{org.name}
-
- -
-
- {org.ownerId === session?.user?.id && ( -
- - { - await deleteOrganization({ - organizationId: org.id, - }) - .then(() => { - refetch(); - toast.success( - "Organization deleted successfully" - ); - }) - .catch((error) => { - toast.error( - error?.message || - "Error deleting organization" - ); - }); - }}> - - -
- )} -
- ))} - {(user?.role === "owner" || isCloud) && ( - <> - - - - )} -
-
-
+ return ( + <> + {isLoading ? ( +
+ +
+ ) : ( + + {/* Organization Logo and Selector */} + + + + +
+
+ +
+
+

+ {activeOrganization?.name ?? "Select Organization"} +

+
+
+ +
+
+ + + Organizations + + {organizations?.map((org) => ( +
+ { + await authClient.organization.setActive({ + organizationId: org.id, + }); + window.location.reload(); + }} + className="w-full gap-2 p-2" + > +
{org.name}
+
+ +
+
+ {org.ownerId === session?.user?.id && ( +
+ + { + await deleteOrganization({ + organizationId: org.id, + }) + .then(() => { + refetch(); + toast.success( + "Organization deleted successfully", + ); + }) + .catch((error) => { + toast.error( + error?.message || + "Error deleting organization", + ); + }); + }} + > + + +
+ )} +
+ ))} + {(user?.role === "owner" || isCloud) && ( + <> + + + + )} +
+
+
- {/* Notification Bell */} - - - - - - - Pending Invitations -
- {invitations && invitations.length > 0 ? ( - invitations.map((invitation) => ( -
- e.preventDefault()}> -
- {invitation?.organization?.name} -
-
- Expires:{" "} - {new Date(invitation.expiresAt).toLocaleString()} -
-
- Role: {invitation.role} -
-
- { - const { error } = - await authClient.organization.acceptInvitation({ - invitationId: invitation.id, - }); + {/* Notification Bell */} + + + + + + + Pending Invitations +
+ {invitations && invitations.length > 0 ? ( + invitations.map((invitation) => ( +
+ e.preventDefault()} + > +
+ {invitation?.organization?.name} +
+
+ Expires:{" "} + {new Date(invitation.expiresAt).toLocaleString()} +
+
+ Role: {invitation.role} +
+
+ { + const { error } = + await authClient.organization.acceptInvitation({ + invitationId: invitation.id, + }); - if (error) { - toast.error( - error.message || "Error accepting invitation" - ); - } else { - toast.success("Invitation accepted successfully"); - await refetchInvitations(); - await refetch(); - } - }}> - - -
- )) - ) : ( - - No pending invitations - - )} -
-
-
-
- - )} - - ); + if (error) { + toast.error( + error.message || "Error accepting invitation", + ); + } else { + toast.success("Invitation accepted successfully"); + await refetchInvitations(); + await refetch(); + } + }} + > + +
+
+ )) + ) : ( + + No pending invitations + + )} +
+
+
+
+
+ )} + + ); } export default function Page({ children }: Props) { - const [defaultOpen, setDefaultOpen] = useState( - undefined - ); - const [isLoaded, setIsLoaded] = useState(false); + const [defaultOpen, setDefaultOpen] = useState( + undefined, + ); + const [isLoaded, setIsLoaded] = useState(false); - useEffect(() => { - const cookieValue = document.cookie - .split("; ") - .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`)) - ?.split("=")[1]; + useEffect(() => { + const cookieValue = document.cookie + .split("; ") + .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`)) + ?.split("=")[1]; - setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true"); - setIsLoaded(true); - }, []); + setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true"); + setIsLoaded(true); + }, []); - const pathname = usePathname(); - const { data: auth } = api.user.get.useQuery(); - const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); + const pathname = usePathname(); + const { data: auth } = api.user.get.useQuery(); + const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); - const includesProjects = pathname?.includes("/dashboard/project"); - const { data: isCloud } = api.settings.isCloud.useQuery(); + const includesProjects = pathname?.includes("/dashboard/project"); + const { data: isCloud } = api.settings.isCloud.useQuery(); - const { - home: filteredHome, - settings: filteredSettings, - help, - } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); + const { + home: filteredHome, + settings: filteredSettings, + help, + } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); - const activeItem = findActiveNavItem( - [...filteredHome, ...filteredSettings], - pathname - ); + const activeItem = findActiveNavItem( + [...filteredHome, ...filteredSettings], + pathname, + ); - if (!isLoaded) { - return
; // Placeholder mientras se carga - } + if (!isLoaded) { + return
; // Placeholder mientras se carga + } - return ( - { - setDefaultOpen(open); + return ( + { + setDefaultOpen(open); - document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`; - }} - style={ - { - "--sidebar-width": "19.5rem", - "--sidebar-width-mobile": "19.5rem", - } as React.CSSProperties - }> - - - {/* + + + {/* */} - - {/* */} - - - - Home - - {filteredHome.map((item) => { - const isSingle = item.isSingle !== false; - const isActive = isSingle - ? isActiveRoute({ itemUrl: item.url, pathname }) - : item.items.some((item) => - isActiveRoute({ itemUrl: item.url, pathname }) - ); + + {/* */} + + + + Home + + {filteredHome.map((item) => { + const isSingle = item.isSingle !== false; + const isActive = isSingle + ? isActiveRoute({ itemUrl: item.url, pathname }) + : item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ); - return ( - - - {isSingle ? ( - - - {item.icon && ( - - )} - {item.title} - - - ) : ( - <> - - - {item.icon && } + return ( + + + {isSingle ? ( + + + {item.icon && ( + + )} + {item.title} + + + ) : ( + <> + + + {item.icon && } - {item.title} - {item.items?.length && ( - - )} - - - - - {item.items?.map((subItem) => ( - - - - {subItem.icon && ( - - - - )} - {subItem.title} - - - - ))} - - - - )} - - - ); - })} - - - - Settings - - {filteredSettings.map((item) => { - const isSingle = item.isSingle !== false; - const isActive = isSingle - ? isActiveRoute({ itemUrl: item.url, pathname }) - : item.items.some((item) => - isActiveRoute({ itemUrl: item.url, pathname }) - ); + {item.title} + {item.items?.length && ( + + )} + + + + + {item.items?.map((subItem) => ( + + + + {subItem.icon && ( + + + + )} + {subItem.title} + + + + ))} + + + + )} + + + ); + })} + + + + Settings + + {filteredSettings.map((item) => { + const isSingle = item.isSingle !== false; + const isActive = isSingle + ? isActiveRoute({ itemUrl: item.url, pathname }) + : item.items.some((item) => + isActiveRoute({ itemUrl: item.url, pathname }), + ); - return ( - - - {isSingle ? ( - - - {item.icon && ( - - )} - {item.title} - - - ) : ( - <> - - - {item.icon && } + return ( + + + {isSingle ? ( + + + {item.icon && ( + + )} + {item.title} + + + ) : ( + <> + + + {item.icon && } - {item.title} - {item.items?.length && ( - - )} - - - - - {item.items?.map((subItem) => ( - - - - {subItem.icon && ( - - - - )} - {subItem.title} - - - - ))} - - - - )} - - - ); - })} - - - - Extra - - {help.map((item: ExternalLink) => ( - - - - - - - {item.name} - - - - ))} - - - - - - {!isCloud && auth?.role === "owner" && ( - - - - )} - - - - {dokployVersion && ( - <> -
- Version {dokployVersion} -
-
- {dokployVersion} -
- - )} -
-
- -
- - {!includesProjects && ( -
-
-
- - - - - - - - {activeItem?.title} - - - - - -
- -
-
- )} + {item.title} + {item.items?.length && ( + + )} + + + + + {item.items?.map((subItem) => ( + + + + {subItem.icon && ( + + + + )} + {subItem.title} + + + + ))} + + + + )} + + + ); + })} + + + + Extra + + {help.map((item: ExternalLink) => ( + + + + + + + {item.name} + + + + ))} + + + + + + {!isCloud && auth?.role === "owner" && ( + + + + )} + + + + {dokployVersion && ( + <> +
+ Version {dokployVersion} +
+
+ {dokployVersion} +
+ + )} +
+
+ + + + {!includesProjects && ( +
+
+
+ + + + + + + + {activeItem?.title} + + + + + +
+ +
+
+ )} -
{children}
-
-
- ); +
{children}
+ +
+ ); } diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx index 8d47297ea..b409bf703 100644 --- a/apps/dokploy/components/ui/time-badge.tsx +++ b/apps/dokploy/components/ui/time-badge.tsx @@ -4,57 +4,57 @@ import { useEffect, useState } from "react"; import { api } from "@/utils/api"; export function TimeBadge() { - const { data: serverTime } = api.server.getServerTime.useQuery(undefined, { - refetchInterval: 60000, // Refetch every 60 seconds - }); - const [time, setTime] = useState(null); + const { data: serverTime } = api.server.getServerTime.useQuery(undefined, { + refetchInterval: 60000, // Refetch every 60 seconds + }); + const [time, setTime] = useState(null); - useEffect(() => { - if (serverTime?.time) { - setTime(new Date(serverTime.time)); - } - }, [serverTime]); + useEffect(() => { + if (serverTime?.time) { + setTime(new Date(serverTime.time)); + } + }, [serverTime]); - useEffect(() => { - const timer = setInterval(() => { - setTime((prevTime) => { - if (!prevTime) return null; - const newTime = new Date(prevTime.getTime() + 1000); - return newTime; - }); - }, 1000); + useEffect(() => { + const timer = setInterval(() => { + setTime((prevTime) => { + if (!prevTime) return null; + const newTime = new Date(prevTime.getTime() + 1000); + return newTime; + }); + }, 1000); - return () => { - clearInterval(timer); - }; - }, []); + return () => { + clearInterval(timer); + }; + }, []); - if (!time || !serverTime?.timezone) { - return null; - } + if (!time || !serverTime?.timezone) { + return null; + } - const getUtcOffset = (timeZone: string) => { - const date = new Date(); - const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); - const tzDate = new Date(date.toLocaleString("en-US", { timeZone })); - const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60); - const sign = offset >= 0 ? "+" : "-"; - const hours = Math.floor(Math.abs(offset)); - const minutes = (Math.abs(offset) * 60) % 60; - return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes - .toString() - .padStart(2, "0")}`; - }; + const getUtcOffset = (timeZone: string) => { + const date = new Date(); + const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" })); + const tzDate = new Date(date.toLocaleString("en-US", { timeZone })); + const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60); + const sign = offset >= 0 ? "+" : "-"; + const hours = Math.floor(Math.abs(offset)); + const minutes = (Math.abs(offset) * 60) % 60; + return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}`; + }; - return ( -
- Server Time: - - {time.toLocaleTimeString()} - - - ({serverTime.timezone} | {getUtcOffset(serverTime.timezone)}) - -
- ); + return ( +
+ Server Time: + + {time.toLocaleTimeString()} + + + ({serverTime.timezone} | {getUtcOffset(serverTime.timezone)}) + +
+ ); } From 63568a4887fa6cb973e91770b1f779dfe7d92472 Mon Sep 17 00:00:00 2001 From: spacewaterbear Date: Mon, 3 Nov 2025 23:27:18 +0100 Subject: [PATCH 05/79] feat: display environnement in notification --- .../src/emails/emails/build-success.tsx | 5 ++++ packages/server/src/services/application.ts | 2 ++ packages/server/src/services/compose.ts | 2 ++ .../src/utils/notifications/build-success.ts | 25 +++++++++++++++++-- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/server/src/emails/emails/build-success.tsx b/packages/server/src/emails/emails/build-success.tsx index d9e500ab9..e5e1d1bb4 100644 --- a/packages/server/src/emails/emails/build-success.tsx +++ b/packages/server/src/emails/emails/build-success.tsx @@ -19,6 +19,7 @@ export type TemplateProps = { applicationType: string; buildLink: string; date: string; + environmentName: string; }; export const BuildSuccessEmail = ({ @@ -27,6 +28,7 @@ export const BuildSuccessEmail = ({ applicationType = "application", buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test", date = "2023-05-01T00:00:00.000Z", + environmentName = "production", }: TemplateProps) => { const previewText = `Build success for ${applicationName}`; return ( @@ -74,6 +76,9 @@ export const BuildSuccessEmail = ({ Application Name: {applicationName} + + Environment: {environmentName} + Application Type: {applicationType} diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 1891f9b6b..6b4f41fd8 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -237,6 +237,7 @@ export const deployApplication = async ({ buildLink, organizationId: application.environment.project.organizationId, domains: application.domains, + environmentName: application.environment.name, }); } catch (error) { await updateDeploymentStatus(deployment.deploymentId, "error"); @@ -373,6 +374,7 @@ export const deployRemoteApplication = async ({ buildLink, organizationId: application.environment.project.organizationId, domains: application.domains, + environmentName: application.environment.name, }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 1436c52cc..03c44e937 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -265,6 +265,7 @@ export const deployCompose = async ({ buildLink, organizationId: compose.environment.project.organizationId, domains: compose.domains, + environmentName: compose.environment.name, }); } catch (error) { await updateDeploymentStatus(deployment.deploymentId, "error"); @@ -397,6 +398,7 @@ export const deployRemoteCompose = async ({ buildLink, organizationId: compose.environment.project.organizationId, domains: compose.domains, + environmentName: compose.environment.name, }); } catch (error) { // @ts-ignore diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index a93b3d547..5b5d6f518 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -22,6 +22,7 @@ interface Props { buildLink: string; organizationId: string; domains: Domain[]; + environmentName: string; } export const sendBuildSuccessNotifications = async ({ @@ -31,6 +32,7 @@ export const sendBuildSuccessNotifications = async ({ buildLink, organizationId, domains, + environmentName, }: Props) => { const date = new Date(); const unixDate = ~~(Number(date) / 1000); @@ -62,6 +64,7 @@ export const sendBuildSuccessNotifications = async ({ applicationType, buildLink, date: date.toLocaleString(), + environmentName, }), ).catch(); await sendEmailNotification(email, "Build success for dokploy", template); @@ -72,7 +75,7 @@ export const sendBuildSuccessNotifications = async ({ `${discord.decoration ? decoration : ""} ${text}`.trim(); await sendDiscordNotification(discord, { - title: decorate(">", "`✅` Build Success"), + title: decorate(">", "`✅` Build Successes"), color: 0x57f287, fields: [ { @@ -85,6 +88,11 @@ export const sendBuildSuccessNotifications = async ({ value: applicationName, inline: true, }, + { + name: decorate("`🌍`", "Environment"), + value: environmentName, + inline: true, + }, { name: decorate("`❔`", "Type"), value: applicationType, @@ -125,6 +133,7 @@ export const sendBuildSuccessNotifications = async ({ decorate("✅", "Build Success"), `${decorate("🛠️", `Project: ${projectName}`)}` + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("🌍", `Environment: ${environmentName}`)}` + `${decorate("❔", `Type: ${applicationType}`)}` + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + `${decorate("🔗", `Build details:\n${buildLink}`)}`, @@ -139,6 +148,7 @@ export const sendBuildSuccessNotifications = async ({ `view, Build details, ${buildLink}, clear=true;`, `🛠Project: ${projectName}\n` + `⚙️Application: ${applicationName}\n` + + `🌍Environment: ${environmentName}\n` + `❔Type: ${applicationType}\n` + `🕒Date: ${date.toLocaleString()}`, ); @@ -167,7 +177,7 @@ export const sendBuildSuccessNotifications = async ({ await sendTelegramNotification( telegram, - `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, inlineButton, ); } @@ -191,6 +201,11 @@ export const sendBuildSuccessNotifications = async ({ value: applicationName, short: true, }, + { + title: "Environment", + value: environmentName, + short: true, + }, { title: "Type", value: applicationType, @@ -260,6 +275,12 @@ export const sendBuildSuccessNotifications = async ({ text_align: "left", text_size: "normal_v2", }, + { + tag: "markdown", + content: `**Environment:**\n${environmentName}`, + text_align: "left", + text_size: "normal_v2", + }, { tag: "markdown", content: `**Type:**\n${applicationType}`, From 7b398939f72a37b2a47b95b0e9dfe464af62dec8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:12:49 -0600 Subject: [PATCH 06/79] Refactor compose and deployment services: streamline cloning and execution commands, remove redundant remote functions, and enhance error handling. Update database schema to include application build server ID for better tracking of deployments. --- apps/dokploy/server/api/routers/compose.ts | 9 +- .../server/queues/deployments-queue.ts | 85 ++-- packages/server/src/db/schema/deployment.ts | 6 + packages/server/src/services/application.ts | 260 +++--------- packages/server/src/services/compose.ts | 235 ++--------- packages/server/src/services/deployment.ts | 16 +- packages/server/src/utils/builders/compose.ts | 28 +- .../server/src/utils/builders/docker-file.ts | 17 +- packages/server/src/utils/builders/heroku.ts | 13 +- packages/server/src/utils/builders/index.ts | 19 +- .../server/src/utils/builders/nixpacks.ts | 51 ++- packages/server/src/utils/builders/paketo.ts | 13 +- .../server/src/utils/builders/railpack.ts | 21 +- packages/server/src/utils/builders/static.ts | 18 +- packages/server/src/utils/cluster/upload.ts | 25 +- packages/server/src/utils/docker/collision.ts | 7 +- packages/server/src/utils/docker/domain.ts | 69 +--- .../server/src/utils/process/execAsync.ts | 6 +- .../server/src/utils/providers/bitbucket.ts | 205 +-------- packages/server/src/utils/providers/git.ts | 391 +++--------------- packages/server/src/utils/providers/gitea.ts | 235 +---------- packages/server/src/utils/providers/github.ts | 232 ++--------- packages/server/src/utils/providers/gitlab.ts | 235 +---------- packages/server/src/utils/providers/raw.ts | 31 +- 24 files changed, 386 insertions(+), 1841 deletions(-) diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index e2f25b763..026b6e8ad 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -3,13 +3,14 @@ import { addNewService, checkServiceAccess, cloneCompose, - cloneComposeRemote, createCommand, createCompose, createComposeByTemplate, createDomain, createMount, deleteMount, + execAsync, + execAsyncRemote, findComposeById, findDomainsByComposeId, findEnvironmentById, @@ -302,10 +303,12 @@ export const composeRouter = createTRPCRouter({ message: "You are not authorized to fetch this compose", }); } + + const command = await cloneCompose(compose); if (compose.serverId) { - await cloneComposeRemote(compose); + await execAsyncRemote(compose.serverId, command); } else { - await cloneCompose(compose); + await execAsync(command); } return compose.sourceType; } catch (err) { diff --git a/apps/dokploy/server/queues/deployments-queue.ts b/apps/dokploy/server/queues/deployments-queue.ts index b8dfb8cd0..a1d9d29f1 100644 --- a/apps/dokploy/server/queues/deployments-queue.ts +++ b/apps/dokploy/server/queues/deployments-queue.ts @@ -2,13 +2,9 @@ import { deployApplication, deployCompose, deployPreviewApplication, - deployRemoteApplication, - deployRemoteCompose, deployRemotePreviewApplication, rebuildApplication, rebuildCompose, - rebuildRemoteApplication, - rebuildRemoteCompose, updateApplicationStatus, updateCompose, updatePreviewDeployment, @@ -24,68 +20,35 @@ export const deploymentWorker = new Worker( if (job.data.applicationType === "application") { await updateApplicationStatus(job.data.applicationId, "running"); - if (job.data.server) { - if (job.data.type === "redeploy") { - await rebuildRemoteApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "deploy") { - await deployRemoteApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } - } else { - if (job.data.type === "redeploy") { - await rebuildApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "deploy") { - await deployApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } + if (job.data.type === "redeploy") { + await rebuildApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); + } else if (job.data.type === "deploy") { + await deployApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); } } else if (job.data.applicationType === "compose") { await updateCompose(job.data.composeId, { composeStatus: "running", }); - - if (job.data.server) { - if (job.data.type === "redeploy") { - await rebuildRemoteCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "deploy") { - await deployRemoteCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } - } else { - if (job.data.type === "deploy") { - await deployCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } else if (job.data.type === "redeploy") { - await rebuildCompose({ - composeId: job.data.composeId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - }); - } + if (job.data.type === "deploy") { + await deployCompose({ + composeId: job.data.composeId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); + } else if (job.data.type === "redeploy") { + await rebuildCompose({ + composeId: job.data.composeId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + }); } } else if (job.data.applicationType === "application-preview") { await updatePreviewDeployment(job.data.previewDeploymentId, { diff --git a/packages/server/src/db/schema/deployment.ts b/packages/server/src/db/schema/deployment.ts index d6b0ddbcc..c49bb0760 100644 --- a/packages/server/src/db/schema/deployment.ts +++ b/packages/server/src/db/schema/deployment.ts @@ -34,6 +34,12 @@ export const deployments = pgTable("deployment", { status: deploymentStatus("status").default("running"), logPath: text("logPath").notNull(), pid: text("pid"), + applicationBuildServerId: text("applicationBuildServerId").references( + () => server.serverId, + { + onDelete: "cascade", + }, + ), applicationId: text("applicationId").references( () => applications.applicationId, { onDelete: "cascade" }, diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 1891f9b6b..e2c38a9fa 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -13,35 +13,23 @@ import { } from "@dokploy/server/utils/builders"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; -import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { - cloneBitbucketRepository, - getBitbucketCloneCommand, -} from "@dokploy/server/utils/providers/bitbucket"; + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; +import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; import { buildDocker, buildRemoteDocker, } from "@dokploy/server/utils/providers/docker"; -import { - cloneGitRepository, - getCustomGitCloneCommand, -} from "@dokploy/server/utils/providers/git"; -import { - cloneGiteaRepository, - getGiteaCloneCommand, -} from "@dokploy/server/utils/providers/gitea"; -import { - cloneGithubRepository, - getGithubCloneCommand, -} from "@dokploy/server/utils/providers/github"; -import { - cloneGitlabRepository, - getGitlabCloneCommand, -} from "@dokploy/server/utils/providers/gitlab"; +import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; +import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; +import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; +import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { encodeBase64 } from "../utils/docker/utils"; +import { cleanUpSystemPrune, encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeployment, @@ -192,43 +180,43 @@ export const deployApplication = async ({ }); try { + let command = "set -e;"; if (application.sourceType === "github") { - await cloneGithubRepository({ - ...application, - logPath: deployment.logPath, - }); - await buildApplication(application, deployment.logPath); + command += await cloneGithubRepository(application); } else if (application.sourceType === "gitlab") { - await cloneGitlabRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); + command += await cloneGitlabRepository(application); } else if (application.sourceType === "gitea") { - await cloneGiteaRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); + command += await cloneGiteaRepository(application); } else if (application.sourceType === "bitbucket") { - await cloneBitbucketRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "docker") { - await buildDocker(application, deployment.logPath); + command += await cloneBitbucketRepository(application); } else if (application.sourceType === "git") { - await cloneGitRepository(application, deployment.logPath); - await buildApplication(application, deployment.logPath); + command += await cloneGitRepository(application); } else if (application.sourceType === "drop") { await buildApplication(application, deployment.logPath); } + command += getBuildCommand(application); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (application.serverId) { + await execAsyncRemote(application.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + + await mechanizeDockerContainer(application); await updateDeploymentStatus(deployment.deploymentId, "done"); await updateApplicationStatus(applicationId, "done"); - if (application.rollbackActive) { - const tagImage = - application.sourceType === "docker" - ? application.dockerImage - : application.appName; - await createRollback({ - appName: tagImage || "", - deploymentId: deployment.deploymentId, - }); - } + // if (application.rollbackActive) { + // const tagImage = + // application.sourceType === "docker" + // ? application.dockerImage + // : application.appName; + // await createRollback({ + // appName: tagImage || "", + // deploymentId: deployment.deploymentId, + // }); + // } await sendBuildSuccessNotifications({ projectName: application.environment.project.name, @@ -239,6 +227,12 @@ export const deployApplication = async ({ domains: application.domains, }); } catch (error) { + const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (application.serverId) { + await execAsyncRemote(application.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); @@ -254,7 +248,6 @@ export const deployApplication = async ({ throw error; } - return true; }; @@ -276,129 +269,21 @@ export const rebuildApplication = async ({ }); try { - if (application.sourceType === "github") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "gitlab") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "bitbucket") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "docker") { - await buildDocker(application, deployment.logPath); - } else if (application.sourceType === "git") { - await buildApplication(application, deployment.logPath); - } else if (application.sourceType === "drop") { - await buildApplication(application, deployment.logPath); - } - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateApplicationStatus(applicationId, "done"); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateApplicationStatus(applicationId, "error"); - throw error; - } - - return true; -}; - -export const deployRemoteApplication = async ({ - applicationId, - titleLog = "Manual deployment", - descriptionLog = "", -}: { - applicationId: string; - titleLog: string; - descriptionLog: string; -}) => { - const application = await findApplicationById(applicationId); - - const buildLink = `${await getDokployUrl()}/dashboard/project/${application.environment.projectId}/environment/${application.environmentId}/services/application/${application.applicationId}?tab=deployments`; - const deployment = await createDeployment({ - applicationId: applicationId, - title: titleLog, - description: descriptionLog, - }); - - try { + let command = "set -e;"; + // Check case for docker only + command += getBuildCommand(application); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (application.serverId) { - let command = "set -e;"; - if (application.sourceType === "github") { - command += await getGithubCloneCommand({ - ...application, - serverId: application.serverId, - logPath: deployment.logPath, - }); - } else if (application.sourceType === "gitlab") { - command += await getGitlabCloneCommand(application, deployment.logPath); - } else if (application.sourceType === "bitbucket") { - command += await getBitbucketCloneCommand( - application, - deployment.logPath, - ); - } else if (application.sourceType === "gitea") { - command += await getGiteaCloneCommand(application, deployment.logPath); - } else if (application.sourceType === "git") { - command += await getCustomGitCloneCommand( - application, - deployment.logPath, - ); - } else if (application.sourceType === "docker") { - command += await buildRemoteDocker(application, deployment.logPath); - } - - if (application.sourceType !== "docker") { - command += getBuildCommand(application, deployment.logPath); - } - await execAsyncRemote(application.serverId, command); - await mechanizeDockerContainer(application); + await execAsyncRemote(application.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); } - + await mechanizeDockerContainer(application); await updateDeploymentStatus(deployment.deploymentId, "done"); await updateApplicationStatus(applicationId, "done"); - - if (application.rollbackActive) { - const tagImage = - application.sourceType === "docker" - ? application.dockerImage - : application.appName; - await createRollback({ - appName: tagImage || "", - deploymentId: deployment.deploymentId, - }); - } - - await sendBuildSuccessNotifications({ - projectName: application.environment.project.name, - applicationName: application.name, - applicationType: "application", - buildLink, - organizationId: application.environment.project.organizationId, - domains: application.domains, - }); } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - const encodedContent = encodeBase64(errorMessage); - - await execAsyncRemote( - application.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); - - await sendBuildErrorNotifications({ - projectName: application.environment.project.name, - applicationName: application.name, - applicationType: "application", - errorMessage: `Please check the logs for details: ${errorMessage}`, - buildLink, - organizationId: application.environment.project.organizationId, - }); - throw error; } @@ -586,7 +471,7 @@ export const deployRemotePreviewApplication = async ({ if (application.serverId) { let command = "set -e;"; if (application.sourceType === "github") { - command += await getGithubCloneCommand({ + command += await cloneGithubRepository({ ...application, appName: previewDeployment.appName, branch: previewDeployment.branch, @@ -629,53 +514,6 @@ export const deployRemotePreviewApplication = async ({ return true; }; -export const rebuildRemoteApplication = async ({ - applicationId, - titleLog = "Rebuild deployment", - descriptionLog = "", -}: { - applicationId: string; - titleLog: string; - descriptionLog: string; -}) => { - const application = await findApplicationById(applicationId); - - const deployment = await createDeployment({ - applicationId: applicationId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (application.serverId) { - if (application.sourceType !== "docker") { - let command = "set -e;"; - command += getBuildCommand(application, deployment.logPath); - await execAsyncRemote(application.serverId, command); - } - await mechanizeDockerContainer(application); - } - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateApplicationStatus(applicationId, "done"); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - application.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateApplicationStatus(applicationId, "error"); - throw error; - } - - return true; -}; - export const getApplicationStats = async (appName: string) => { const filter = { status: ["running"], diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 1436c52cc..4b96f443d 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -7,14 +7,10 @@ import { cleanAppName, compose, } from "@dokploy/server/db/schema"; -import { - buildCompose, - getBuildComposeCommand, -} from "@dokploy/server/utils/builders/compose"; +import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose"; import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose"; import { cloneCompose, - cloneComposeRemote, loadDockerCompose, loadDockerComposeRemote, } from "@dokploy/server/utils/docker/domain"; @@ -25,33 +21,14 @@ import { execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; -import { - cloneBitbucketRepository, - getBitbucketCloneCommand, -} from "@dokploy/server/utils/providers/bitbucket"; -import { - cloneGitRepository, - getCustomGitCloneCommand, -} from "@dokploy/server/utils/providers/git"; -import { - cloneGiteaRepository, - getGiteaCloneCommand, -} from "@dokploy/server/utils/providers/gitea"; -import { - cloneGithubRepository, - getGithubCloneCommand, -} from "@dokploy/server/utils/providers/github"; -import { - cloneGitlabRepository, - getGitlabCloneCommand, -} from "@dokploy/server/utils/providers/gitlab"; -import { - createComposeFile, - getCreateComposeFileCommand, -} from "@dokploy/server/utils/providers/raw"; +import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; +import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; +import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; +import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; +import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; +import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { validUniqueServerAppName } from "./project"; @@ -163,10 +140,11 @@ export const loadServices = async ( const compose = await findComposeById(composeId); if (type === "fetch") { + const command = await cloneCompose(compose); if (compose.serverId) { - await cloneComposeRemote(compose); + await execAsyncRemote(compose.serverId, command); } else { - await cloneCompose(compose); + await execAsync(command); } } @@ -235,24 +213,33 @@ export const deployCompose = async ({ }); try { + const entity = { + ...compose, + type: "compose" as const, + }; + let command = "set -e;"; if (compose.sourceType === "github") { - await cloneGithubRepository({ - ...compose, - logPath: deployment.logPath, - type: "compose", - }); + command += await cloneGithubRepository(entity); } else if (compose.sourceType === "gitlab") { - await cloneGitlabRepository(compose, deployment.logPath, true); + command += await cloneGitlabRepository(entity); } else if (compose.sourceType === "bitbucket") { - await cloneBitbucketRepository(compose, deployment.logPath, true); + command += await cloneBitbucketRepository(entity); } else if (compose.sourceType === "git") { - await cloneGitRepository(compose, deployment.logPath, true); + command += await cloneGitRepository(entity); } else if (compose.sourceType === "gitea") { - await cloneGiteaRepository(compose, deployment.logPath, true); + command += await cloneGiteaRepository(entity); } else if (compose.sourceType === "raw") { - await createComposeFile(compose, deployment.logPath); + command += getCreateComposeFileCommand(entity); } - await buildCompose(compose, deployment.logPath); + + command += await getBuildComposeCommand(entity); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); await updateCompose(composeId, { composeStatus: "done", @@ -302,154 +289,16 @@ export const rebuildCompose = async ({ }); try { + let command = "set -e;"; if (compose.sourceType === "raw") { - await createComposeFile(compose, deployment.logPath); + command += getCreateComposeFileCommand(compose); } - await buildCompose(compose, deployment.logPath); - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; -}; - -export const deployRemoteCompose = async ({ - composeId, - titleLog = "Manual deployment", - descriptionLog = "", -}: { - composeId: string; - titleLog: string; - descriptionLog: string; -}) => { - const compose = await findComposeById(composeId); - - const buildLink = `${await getDokployUrl()}/dashboard/project/${ - compose.environment.projectId - }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`; - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - try { + command += await getBuildComposeCommand(compose); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (compose.serverId) { - let command = "set -e;"; - - if (compose.sourceType === "github") { - command += await getGithubCloneCommand({ - ...compose, - logPath: deployment.logPath, - type: "compose", - serverId: compose.serverId, - }); - } else if (compose.sourceType === "gitlab") { - command += await getGitlabCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "bitbucket") { - command += await getBitbucketCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "git") { - command += await getCustomGitCloneCommand( - compose, - deployment.logPath, - true, - ); - console.log(command); - } else if (compose.sourceType === "raw") { - command += getCreateComposeFileCommand(compose, deployment.logPath); - } else if (compose.sourceType === "gitea") { - command += await getGiteaCloneCommand( - compose, - deployment.logPath, - true, - ); - } - - await execAsyncRemote(compose.serverId, command); - await getBuildComposeCommand(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - - await sendBuildSuccessNotifications({ - projectName: compose.environment.project.name, - applicationName: compose.name, - applicationType: "compose", - buildLink, - organizationId: compose.environment.project.organizationId, - domains: compose.domains, - }); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - await sendBuildErrorNotifications({ - projectName: compose.environment.project.name, - applicationName: compose.name, - applicationType: "compose", - // @ts-ignore - errorMessage: error?.message || "Error building", - buildLink, - organizationId: compose.environment.project.organizationId, - }); - throw error; - } -}; - -export const rebuildRemoteCompose = async ({ - composeId, - titleLog = "Rebuild deployment", - descriptionLog = "", -}: { - composeId: string; - titleLog: string; - descriptionLog: string; -}) => { - const compose = await findComposeById(composeId); - - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.sourceType === "raw") { - const command = getCreateComposeFileCommand(compose, deployment.logPath); - await execAsyncRemote(compose.serverId, command); - } - if (compose.serverId) { - await getBuildComposeCommand(compose, deployment.logPath); + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); } await updateDeploymentStatus(deployment.deploymentId, "done"); @@ -457,16 +306,6 @@ export const rebuildRemoteCompose = async ({ composeStatus: "done", }); } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` - echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; - echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); await updateDeploymentStatus(deployment.deploymentId, "error"); await updateCompose(composeId, { composeStatus: "error", diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index ed03b32fc..95d543dd0 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -74,20 +74,21 @@ export const createDeployment = async ( >, ) => { const application = await findApplicationById(deployment.applicationId); - try { await removeLastTenDeployments( deployment.applicationId, "application", application.serverId, ); - const { LOGS_PATH } = paths(!!application.serverId); + const serverId = application.serverId; + + const { LOGS_PATH } = paths(!!serverId); const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); const fileName = `${application.appName}-${formattedDateTime}.log`; const logFilePath = path.join(LOGS_PATH, application.appName, fileName); - if (application.serverId) { - const server = await findServerById(application.serverId); + if (serverId) { + const server = await findServerById(serverId); const command = ` mkdir -p ${LOGS_PATH}/${application.appName}; @@ -99,7 +100,7 @@ export const createDeployment = async ( await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), { recursive: true, }); - await fsPromises.writeFile(logFilePath, "Initializing deployment"); + await fsPromises.writeFile(logFilePath, "Initializing deployment\n"); } const deploymentCreate = await db @@ -111,6 +112,7 @@ export const createDeployment = async ( logPath: logFilePath, description: deployment.description || "", startedAt: new Date().toISOString(), + // applicationBuildServerId: application.serverBuildId, }) .returning(); if (deploymentCreate.length === 0 || !deploymentCreate[0]) { @@ -249,7 +251,7 @@ export const createDeploymentCompose = async ( const command = ` mkdir -p ${LOGS_PATH}/${compose.appName}; -echo "Initializing deployment" >> ${logFilePath}; +echo "Initializing deployment\n" >> ${logFilePath}; `; await execAsyncRemote(server.serverId, command); @@ -257,7 +259,7 @@ echo "Initializing deployment" >> ${logFilePath}; await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), { recursive: true, }); - await fsPromises.writeFile(logFilePath, "Initializing deployment"); + await fsPromises.writeFile(logFilePath, "Initializing deployment\n"); } const deploymentCreate = await db diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 667b46b74..6b26a0d1c 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -17,7 +17,7 @@ import { getEnviromentVariablesObject, prepareEnvironmentVariables, } from "../docker/utils"; -import { execAsync, execAsyncRemote } from "../process/execAsync"; +import { execAsync } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< @@ -96,22 +96,15 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => { } }; -export const getBuildComposeCommand = async ( - compose: ComposeNested, - logPath: string, -) => { - const { COMPOSE_PATH } = paths(true); +export const getBuildComposeCommand = async (compose: ComposeNested) => { + const { COMPOSE_PATH } = paths(!!compose.serverId); const { sourceType, appName, mounts, composeType, domains } = compose; const command = createCommand(compose); const envCommand = getCreateEnvFileCommand(compose); const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const exportEnvCommand = getExportEnvCommand(compose); - const newCompose = await writeDomainsToComposeRemote( - compose, - domains, - logPath, - ); + const newCompose = await writeDomainsToComposeRemote(compose, domains); const logContent = ` App Name: ${appName} Build Compose 🐳 @@ -133,7 +126,7 @@ Compose Type: ${composeType} ✅`; const bashCommand = ` set -e { - echo "${logBox}" >> "${logPath}" + echo "${logBox}"; ${newCompose} @@ -143,17 +136,18 @@ Compose Type: ${composeType} ✅`; ${exportEnvCommand} ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""} - docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; } + docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } ${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""} - echo "Docker Compose Deployed: ✅" >> "${logPath}" + echo "Docker Compose Deployed: ✅"; } || { - echo "Error: ❌ Script execution failed" >> "${logPath}" + echo "Error: ❌ Script execution failed"; exit 1 } `; - return await execAsyncRemote(compose.serverId, bashCommand); + return bashCommand; + // return await execAsyncRemote(compose.serverId, bashCommand); }; const sanitizeCommand = (command: string) => { @@ -216,7 +210,7 @@ const createEnvFile = (compose: ComposeNested) => { }; export const getCreateEnvFileCommand = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(true); + const { COMPOSE_PATH } = paths(!!compose.serverId); const { env, composePath, appName } = compose; const composeFilePath = join(COMPOSE_PATH, appName, "code", composePath) || diff --git a/packages/server/src/utils/builders/docker-file.ts b/packages/server/src/utils/builders/docker-file.ts index b5c2b59c3..4636cb22c 100644 --- a/packages/server/src/utils/builders/docker-file.ts +++ b/packages/server/src/utils/builders/docker-file.ts @@ -100,10 +100,7 @@ export const buildCustomDocker = async ( } }; -export const getDockerCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getDockerCommand = (application: ApplicationNested) => { const { appName, env, @@ -176,17 +173,17 @@ export const getDockerCommand = ( } command += ` -echo "Building ${appName}" >> ${logPath}; -cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || { - echo "❌ The path ${dockerContextPath} does not exist" >> ${logPath}; +echo "Building ${appName}" ; +cd ${dockerContextPath} || { + echo "❌ The path ${dockerContextPath} does not exist" ; exit 1; } -${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { - echo "❌ Docker build failed" >> ${logPath}; +${joinedSecrets} docker ${commandArgs.join(" ")} || { + echo "❌ Docker build failed" ; exit 1; } -echo "✅ Docker build completed." >> ${logPath}; +echo "✅ Docker build completed." ; `; return command; diff --git a/packages/server/src/utils/builders/heroku.ts b/packages/server/src/utils/builders/heroku.ts index 3306f2fc2..a0a8da153 100644 --- a/packages/server/src/utils/builders/heroku.ts +++ b/packages/server/src/utils/builders/heroku.ts @@ -45,10 +45,7 @@ export const buildHeroku = async ( } }; -export const getHerokuCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getHerokuCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); @@ -77,12 +74,12 @@ export const getHerokuCommand = ( const command = `pack ${args.join(" ")}`; const bashCommand = ` -echo "Starting heroku build..." >> ${logPath}; -${command} >> ${logPath} 2>> ${logPath} || { - echo "❌ Heroku build failed" >> ${logPath}; +echo "Starting heroku build..." ; +${command} || { + echo "❌ Heroku build failed" ; exit 1; } -echo "✅ Heroku build completed." >> ${logPath}; +echo "✅ Heroku build completed." ; `; return bashCommand; diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 5ae0704c5..35172789c 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -76,34 +76,31 @@ export const buildApplication = async ( } }; -export const getBuildCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getBuildCommand = (application: ApplicationNested) => { let command = ""; const { buildType, registry } = application; switch (buildType) { case "nixpacks": - command = getNixpacksCommand(application, logPath); + command = getNixpacksCommand(application); break; case "heroku_buildpacks": - command = getHerokuCommand(application, logPath); + command = getHerokuCommand(application); break; case "paketo_buildpacks": - command = getPaketoCommand(application, logPath); + command = getPaketoCommand(application); break; case "static": - command = getStaticCommand(application, logPath); + command = getStaticCommand(application); break; case "dockerfile": - command = getDockerCommand(application, logPath); + command = getDockerCommand(application); break; case "railpack": - command = getRailpackCommand(application, logPath); + command = getRailpackCommand(application); break; } if (registry) { - command += uploadImageRemoteCommand(application, logPath); + command += uploadImageRemoteCommand(application); } return command; diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index 76905d0e7..705539f06 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -92,10 +92,7 @@ export const buildNixpacks = async ( } }; -export const getNixpacksCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getNixpacksCommand = (application: ApplicationNested) => { const { env, appName, publishDirectory, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); @@ -121,13 +118,13 @@ export const getNixpacksCommand = ( args.push("--no-error-without-start"); } const command = `nixpacks ${args.join(" ")}`; - let bashCommand = ` -echo "Starting nixpacks build..." >> ${logPath}; -${command} >> ${logPath} 2>> ${logPath} || { - echo "❌ Nixpacks build failed" >> ${logPath}; - exit 1; -} -echo "✅ Nixpacks build completed." >> ${logPath}; + const bashCommand = ` + echo "Starting nixpacks build..." ; + ${command} || { + echo "❌ Nixpacks build failed" ; + exit 1; + } + echo "✅ Nixpacks build completed." ; `; /* @@ -135,23 +132,23 @@ echo "✅ Nixpacks build completed." >> ${logPath}; and copy the artifacts on the host filesystem. Then, remove the container and create a static build. */ - if (publishDirectory) { - const localPath = path.join(buildAppDirectory, publishDirectory); - const isDirectory = - publishDirectory.endsWith("/") || !path.extname(publishDirectory); + // if (publishDirectory) { + // const localPath = path.join(buildAppDirectory, publishDirectory); + // const isDirectory = + // publishDirectory.endsWith("/") || !path.extname(publishDirectory); - bashCommand += ` -docker create --name ${buildContainerId} ${appName} -mkdir -p ${localPath} -docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} >> ${logPath} 2>> ${logPath} || { - docker rm ${buildContainerId} - echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" >> ${logPath}; - exit 1; -} -docker rm ${buildContainerId} -${getStaticCommand(application, logPath)} - `; - } + // bashCommand += ` + // docker create --name ${buildContainerId} ${appName} + // mkdir -p ${localPath} + // docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || { + // docker rm ${buildContainerId} + // echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ; + // exit 1; + // } + // docker rm ${buildContainerId} + // ${getStaticCommand(application)} + // `; + // } return bashCommand; }; diff --git a/packages/server/src/utils/builders/paketo.ts b/packages/server/src/utils/builders/paketo.ts index b95a1bb31..51e2301f9 100644 --- a/packages/server/src/utils/builders/paketo.ts +++ b/packages/server/src/utils/builders/paketo.ts @@ -44,10 +44,7 @@ export const buildPaketo = async ( } }; -export const getPaketoCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getPaketoCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); @@ -76,12 +73,12 @@ export const getPaketoCommand = ( const command = `pack ${args.join(" ")}`; const bashCommand = ` -echo "Starting Paketo build..." >> ${logPath}; -${command} >> ${logPath} 2>> ${logPath} || { - echo "❌ Paketo build failed" >> ${logPath}; +echo "Starting Paketo build..." ; +${command} || { + echo "❌ Paketo build failed" ; exit 1; } -echo "✅ Paketo build completed." >> ${logPath}; +echo "✅ Paketo build completed." ; `; return bashCommand; diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 4adc9ca1c..822257773 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -116,10 +116,7 @@ export const buildRailpack = async ( } }; -export const getRailpackCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getRailpackCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); const envVariables = prepareEnvironmentVariables( @@ -183,21 +180,21 @@ export const getRailpackCommand = ( docker buildx create --use --name builder-containerd --driver docker-container || true docker buildx use builder-containerd -echo "Preparing Railpack build plan..." >> "${logPath}"; -railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { - echo "❌ Railpack prepare failed" >> ${logPath}; +echo "Preparing Railpack build plan..." ; +railpack ${prepareArgs.join(" ")} || { + echo "❌ Railpack prepare failed" ; exit 1; } -echo "✅ Railpack prepare completed." >> ${logPath}; +echo "✅ Railpack prepare completed." ; -echo "Building with Railpack frontend..." >> "${logPath}"; +echo "Building with Railpack frontend..." ; # Export environment variables for secrets ${exportEnvs.join("\n")} -docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { - echo "❌ Railpack build failed" >> ${logPath}; +docker ${buildArgs.join(" ")} || { + echo "❌ Railpack build failed" ; exit 1; } -echo "✅ Railpack build completed." >> ${logPath}; +echo "✅ Railpack build completed." ; docker buildx rm builder-containerd `; diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index e59faa711..5e1f10cc3 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -83,10 +83,7 @@ export const buildStatic = async ( } }; -export const getStaticCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const getStaticCommand = (application: ApplicationNested) => { const { publishDirectory } = application; const buildAppDirectory = getBuildAppDirectory(application); @@ -100,13 +97,10 @@ export const getStaticCommand = ( ].join("\n"), ); - command += getDockerCommand( - { - ...application, - buildType: "dockerfile", - dockerfile: "Dockerfile", - }, - logPath, - ); + command += getDockerCommand({ + ...application, + buildType: "dockerfile", + dockerfile: "Dockerfile", + }); return command; }; diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index c13a2701c..6a2dfd66f 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -59,10 +59,7 @@ export const uploadImage = async ( } }; -export const uploadImageRemoteCommand = ( - application: ApplicationNested, - logPath: string, -) => { +export const uploadImageRemoteCommand = (application: ApplicationNested) => { const registry = application.registry; if (!registry) { @@ -82,22 +79,22 @@ export const uploadImageRemoteCommand = ( try { const command = ` - echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath}; - echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || { - echo "❌ DockerHub Failed" >> ${logPath}; + echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ; + echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin || { + echo "❌ DockerHub Failed" ; exit 1; } - echo "✅ Registry Login Success" >> ${logPath}; - docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || { - echo "❌ Error tagging image" >> ${logPath}; + echo "✅ Registry Login Success" ; + docker tag ${imageName} ${registryTag} || { + echo "❌ Error tagging image" ; exit 1; } - echo "✅ Image Tagged" >> ${logPath}; - docker push ${registryTag} 2>> ${logPath} || { - echo "❌ Error pushing image" >> ${logPath}; + echo "✅ Image Tagged" ; + docker push ${registryTag} || { + echo "❌ Error pushing image" ; exit 1; } - echo "✅ Image Pushed" >> ${logPath}; + echo "✅ Image Pushed" ; `; return command; } catch (error) { diff --git a/packages/server/src/utils/docker/collision.ts b/packages/server/src/utils/docker/collision.ts index 9752100ca..88d20d4d8 100644 --- a/packages/server/src/utils/docker/collision.ts +++ b/packages/server/src/utils/docker/collision.ts @@ -1,11 +1,11 @@ import { findComposeById } from "@dokploy/server/services/compose"; import { stringify } from "yaml"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; import { addAppNameToAllServiceNames } from "./collision/root-network"; import { generateRandomHash } from "./compose"; import { addSuffixToAllVolumes } from "./compose/volume"; import { cloneCompose, - cloneComposeRemote, loadDockerCompose, loadDockerComposeRemote, } from "./domain"; @@ -31,10 +31,11 @@ export const randomizeIsolatedDeploymentComposeFile = async ( ) => { const compose = await findComposeById(composeId); + const command = await cloneCompose(compose); if (compose.serverId) { - await cloneComposeRemote(compose); + await execAsyncRemote(compose.serverId, command); } else { - await cloneCompose(compose); + await execAsync(command); } let composeData: ComposeSpecification | null; diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 7a9521d1d..e8c66e697 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -6,30 +6,12 @@ import type { Compose } from "@dokploy/server/services/compose"; import type { Domain } from "@dokploy/server/services/domain"; import { parse, stringify } from "yaml"; import { execAsyncRemote } from "../process/execAsync"; -import { - cloneRawBitbucketRepository, - cloneRawBitbucketRepositoryRemote, -} from "../providers/bitbucket"; -import { - cloneGitRawRepository, - cloneRawGitRepositoryRemote, -} from "../providers/git"; -import { - cloneRawGiteaRepository, - cloneRawGiteaRepositoryRemote, -} from "../providers/gitea"; -import { - cloneRawGithubRepository, - cloneRawGithubRepositoryRemote, -} from "../providers/github"; -import { - cloneRawGitlabRepository, - cloneRawGitlabRepositoryRemote, -} from "../providers/gitlab"; -import { - createComposeFileRaw, - createComposeFileRawRemote, -} from "../providers/raw"; +import { cloneBitbucketRepository } from "../providers/bitbucket"; +import { cloneGitRepository } from "../providers/git"; +import { cloneGiteaRepository } from "../providers/gitea"; +import { cloneGithubRepository } from "../providers/github"; +import { cloneGitlabRepository } from "../providers/gitlab"; +import { getCreateComposeFileCommand } from "../providers/raw"; import { randomizeDeployableSpecificationFile } from "./collision"; import { randomizeSpecificationFile } from "./compose"; import type { @@ -40,35 +22,25 @@ import type { import { encodeBase64 } from "./utils"; export const cloneCompose = async (compose: Compose) => { + let command = "set -e;"; + const entity = { + ...compose, + type: "compose" as const, + }; if (compose.sourceType === "github") { - await cloneRawGithubRepository(compose); + command += await cloneGithubRepository(entity); } else if (compose.sourceType === "gitlab") { - await cloneRawGitlabRepository(compose); + command += await cloneGitlabRepository(entity); } else if (compose.sourceType === "bitbucket") { - await cloneRawBitbucketRepository(compose); + command += await cloneBitbucketRepository(entity); } else if (compose.sourceType === "git") { - await cloneGitRawRepository(compose); + command += await cloneGitRepository(entity); } else if (compose.sourceType === "gitea") { - await cloneRawGiteaRepository(compose); + command += await cloneGiteaRepository(entity); } else if (compose.sourceType === "raw") { - await createComposeFileRaw(compose); - } -}; - -export const cloneComposeRemote = async (compose: Compose) => { - if (compose.sourceType === "github") { - await cloneRawGithubRepositoryRemote(compose); - } else if (compose.sourceType === "gitlab") { - await cloneRawGitlabRepositoryRemote(compose); - } else if (compose.sourceType === "bitbucket") { - await cloneRawBitbucketRepositoryRemote(compose); - } else if (compose.sourceType === "git") { - await cloneRawGitRepositoryRemote(compose); - } else if (compose.sourceType === "gitea") { - await cloneRawGiteaRepositoryRemote(compose); - } else if (compose.sourceType === "raw") { - await createComposeFileRawRemote(compose); + command += getCreateComposeFileCommand(compose); } + return command; }; export const getComposePath = (compose: Compose) => { @@ -152,7 +124,6 @@ export const writeDomainsToCompose = async ( export const writeDomainsToComposeRemote = async ( compose: Compose, domains: Domain[], - logPath: string, ) => { if (!domains.length) { return ""; @@ -164,7 +135,7 @@ export const writeDomainsToComposeRemote = async ( if (!composeConverted) { return ` -echo "❌ Error: Compose file not found" >> ${logPath}; +echo "❌ Error: Compose file not found"; exit 1; `; } @@ -175,7 +146,7 @@ exit 1; } } catch (error) { // @ts-ignore - return `echo "❌ Has occured an error: ${error?.message || error}" >> ${logPath}; + return `echo "❌ Has occured an error: ${error?.message || error}"; exit 1; `; } diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index 84f0701d9..13b06c6c4 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -116,11 +116,7 @@ export const execAsyncRemote = async ( if (code === 0) { resolve({ stdout, stderr }); } else { - reject( - new Error( - `Command exited with code ${code}. Stderr: ${stderr}, command: ${command}`, - ), - ); + reject(new Error(`Error occurred ❌: ${stderr}`)); } }) .on("data", (data: string) => { diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index ed6cd8c31..0267ec9b3 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -1,4 +1,3 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { @@ -9,12 +8,8 @@ import { type Bitbucket, findBitbucketById, } from "@dokploy/server/services/bitbucket"; -import type { Compose } from "@dokploy/server/services/compose"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export type ApplicationWithBitbucket = InferResultType< "applications", @@ -81,13 +76,16 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => { }; }; -export const cloneBitbucketRepository = async ( - entity: ApplicationWithBitbucket | ComposeWithBitbucket, - logPath: string, - isCompose = false, -) => { - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(); - const writeStream = createWriteStream(logPath, { flags: "a" }); +type BitbucketClone = (ApplicationWithBitbucket | ComposeWithBitbucket) & { + serverId: string | null; + type?: "application" | "compose"; +}; + +export const cloneBitbucketRepository = async ({ + type = "application", + ...entity +}: BitbucketClone) => { + let command = "set -e;"; const { appName, bitbucketRepository, @@ -96,187 +94,24 @@ export const cloneBitbucketRepository = async ( bitbucketId, bitbucket, enableSubmodules, + serverId, } = entity; + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); if (!bitbucketId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); + command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`; + return command; } - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); - try { - writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); - const cloneArgs = [ - "clone", - "--branch", - bitbucketBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const cloneRawBitbucketRepository = async (entity: Compose) => { - const { COMPOSE_PATH } = paths(); - const { - appName, - bitbucketRepository, - bitbucketOwner, - bitbucketBranch, - bitbucketId, - enableSubmodules, - } = entity; - - if (!bitbucketId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); - } - - const bitbucketProvider = await findBitbucketById(bitbucketId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); - - try { - const cloneArgs = [ - "clone", - "--branch", - bitbucketBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs); - } catch (error) { - throw error; - } -}; - -export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => { - const { COMPOSE_PATH } = paths(true); - const { - appName, - bitbucketRepository, - bitbucketOwner, - bitbucketBranch, - bitbucketId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!bitbucketId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); - } - - const bitbucketProvider = await findBitbucketById(bitbucketId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); - - try { - const cloneCommand = ` - rm -rf ${outputPath}; - git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, cloneCommand); - } catch (error) { - throw error; - } -}; - -export const getBitbucketCloneCommand = async ( - entity: ApplicationWithBitbucket | ComposeWithBitbucket, - logPath: string, - isCompose = false, -) => { - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - const { - appName, - bitbucketRepository, - bitbucketOwner, - bitbucketBranch, - bitbucketId, - serverId, - enableSubmodules, - } = entity; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!bitbucketId) { - const command = ` - echo "Error: ❌ Bitbucket Provider not found" >> ${logPath}; - exit 1; - `; - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "Bitbucket Provider not found", - }); - } - - const bitbucketProvider = await findBitbucketById(bitbucketId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; - const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone); - - const cloneCommand = ` -rm -rf ${outputPath}; -mkdir -p ${outputPath}; -if ! git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone the repository ${repoclone}" >> ${logPath}; - exit 1; -fi -echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; - `; - - return cloneCommand; + command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`; + command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + return command; }; export const getBitbucketRepositories = async (bitbucketId?: string) => { diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 5779252db..19c8ab8a0 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -1,159 +1,64 @@ -import { createWriteStream } from "node:fs"; import path, { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import type { Compose } from "@dokploy/server/services/compose"; import { findSSHKeyById, updateSSHKeyById, } from "@dokploy/server/services/ssh-key"; -import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; -export const cloneGitRepository = async ( - entity: { - appName: string; - customGitUrl?: string | null; - customGitBranch?: string | null; - customGitSSHKeyId?: string | null; - enableSubmodules?: boolean; - }, - logPath: string, - isCompose = false, -) => { - const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(); +interface CloneGitRepository { + appName: string; + customGitUrl?: string | null; + customGitBranch?: string | null; + customGitSSHKeyId?: string | null; + enableSubmodules?: boolean; + serverId: string | null; + type?: "application" | "compose"; +} + +export const cloneGitRepository = async ({ + type = "application", + ...entity +}: CloneGitRepository) => { + let command = "set -e;"; const { appName, customGitUrl, customGitBranch, customGitSSHKeyId, enableSubmodules, + serverId, } = entity; + const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); if (!customGitUrl || !customGitBranch) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: Repository not found", - }); + command += `echo "Error: ❌ Repository not found"; exit 1;`; + return command; } - const writeStream = createWriteStream(logPath, { flags: "a" }); const temporalKeyPath = path.join("/tmp", "id_rsa"); if (customGitSSHKeyId) { const sshKey = await findSSHKeyById(customGitSSHKeyId); - await execAsync(` + command += ` echo "${sshKey.privateKey}" > ${temporalKeyPath} - chmod 600 ${temporalKeyPath} - `); + chmod 600 ${temporalKeyPath}; + `; } - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - try { - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key", - }); - } - await addHostToKnownHosts(customGitUrl); + if (!isHttpOrHttps(customGitUrl)) { + if (!customGitSSHKeyId) { + command += `echo "Error: ❌ You are trying to clone a ssh repository without a ssh key, please set a ssh key"; exit 1;`; + return command; } - await recreateDirectory(outputPath); - writeStream.write( - `\nCloning Repo Custom ${customGitUrl} to ${outputPath}: ✅\n`, - ); - - if (customGitSSHKeyId) { - await updateSSHKeyById({ - sshKeyId: customGitSSHKeyId, - lastUsedAt: new Date().toISOString(), - }); - } - - const { port } = sanitizeRepoPathSSH(customGitUrl); - const cloneArgs = [ - "clone", - "--branch", - customGitBranch, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - customGitUrl, - outputPath, - "--progress", - ]; - - await spawnAsync( - "git", - cloneArgs, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { - ...process.env, - ...(customGitSSHKeyId && { - GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, - }), - }, - }, - ); - - writeStream.write(`\nCloned Custom Git ${customGitUrl}: ✅\n`); - } catch (error) { - writeStream.write(`\nERROR Cloning Custom Git: ${error}: ❌\n`); - throw error; - } finally { - writeStream.end(); + command += addHostToKnownHostsCommand(customGitUrl); } -}; - -export const getCustomGitCloneCommand = async ( - entity: { - appName: string; - customGitUrl?: string | null; - customGitBranch?: string | null; - customGitSSHKeyId?: string | null; - serverId: string | null; - enableSubmodules: boolean; - }, - logPath: string, - isCompose = false, -) => { - const { SSH_PATH, COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - const { - appName, - customGitUrl, - customGitBranch, - customGitSSHKeyId, - serverId, - enableSubmodules, - } = entity; - - if (!customGitUrl || !customGitBranch) { - const command = ` - echo "Error: ❌ Repository not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: Repository not found", - }); - } - - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; + command += `echo "Cloning Repo Custom ${customGitUrl} to ${outputPath}: ✅";`; if (customGitSSHKeyId) { await updateSSHKeyById({ @@ -161,48 +66,22 @@ export const getCustomGitCloneCommand = async ( lastUsedAt: new Date().toISOString(), }); } - try { - const command = []; - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - command.push( - `echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" >> ${logPath}; - exit 1; - `, - ); - } - command.push(addHostToKnownHostsCommand(customGitUrl)); - } - command.push(`rm -rf ${outputPath};`); - command.push(`mkdir -p ${outputPath};`); - command.push( - `echo "Cloning Custom Git ${customGitUrl}" to ${outputPath}: ✅ >> ${logPath};`, - ); - if (customGitSSHKeyId) { - const sshKey = await findSSHKeyById(customGitSSHKeyId); - const { port } = sanitizeRepoPathSSH(customGitUrl); - const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; - command.push( - ` - echo "${sshKey.privateKey}" > /tmp/id_rsa - chmod 600 /tmp/id_rsa - export GIT_SSH_COMMAND="${gitSshCommand}" - `, - ); - } - command.push( - `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}" >> ${logPath}; + if (customGitSSHKeyId) { + const sshKey = await findSSHKeyById(customGitSSHKeyId); + const { port } = sanitizeRepoPathSSH(customGitUrl); + const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; + command += `echo "${sshKey.privateKey}" > /tmp/id_rsa;`; + command += "chmod 600 /tmp/id_rsa;"; + command += `export GIT_SSH_COMMAND="${gitSshCommand}";`; + } + command += `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath}; then + echo "❌ [ERROR] Fail to clone the repository ${customGitUrl}"; exit 1; fi - `, - ); - command.push(`echo "Cloned Custom Git ${customGitUrl}: ✅" >> ${logPath};`); - return command.join("\n"); - } catch (error) { - throw error; - } + `; + + return command; }; const isHttpOrHttps = (url: string): boolean => { @@ -210,19 +89,19 @@ const isHttpOrHttps = (url: string): boolean => { return regex.test(url); }; -const addHostToKnownHosts = async (repositoryURL: string) => { - const { SSH_PATH } = paths(); - const { domain, port } = sanitizeRepoPathSSH(repositoryURL); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); +// const addHostToKnownHosts = async (repositoryURL: string) => { +// const { SSH_PATH } = paths(); +// const { domain, port } = sanitizeRepoPathSSH(repositoryURL); +// const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`; - try { - await execAsync(command); - } catch (error) { - console.error(`Error adding host to known_hosts: ${error}`); - throw error; - } -}; +// const command = `ssh-keyscan -p ${port} ${domain} >> ${knownHostsPath}`; +// try { +// await execAsync(command); +// } catch (error) { +// console.error(`Error adding host to known_hosts: ${error}`); +// throw error; +// } +// }; const addHostToKnownHostsCommand = (repositoryURL: string) => { const { SSH_PATH } = paths(true); @@ -266,161 +145,3 @@ const sanitizeRepoPathSSH = (input: string) => { }, }; }; - -export const cloneGitRawRepository = async (entity: { - appName: string; - customGitUrl?: string | null; - customGitBranch?: string | null; - customGitSSHKeyId?: string | null; - enableSubmodules?: boolean; -}) => { - const { - appName, - customGitUrl, - customGitBranch, - customGitSSHKeyId, - enableSubmodules, - } = entity; - - if (!customGitUrl || !customGitBranch) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: Repository not found", - }); - } - - const { SSH_PATH, COMPOSE_PATH } = paths(); - const temporalKeyPath = path.join("/tmp", "id_rsa"); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - - if (customGitSSHKeyId) { - const sshKey = await findSSHKeyById(customGitSSHKeyId); - - await execAsync(` - echo "${sshKey.privateKey}" > ${temporalKeyPath} - chmod 600 ${temporalKeyPath} - `); - } - - try { - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: - "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key", - }); - } - await addHostToKnownHosts(customGitUrl); - } - await recreateDirectory(outputPath); - - if (customGitSSHKeyId) { - await updateSSHKeyById({ - sshKeyId: customGitSSHKeyId, - lastUsedAt: new Date().toISOString(), - }); - } - - const { port } = sanitizeRepoPathSSH(customGitUrl); - const cloneArgs = [ - "clone", - "--branch", - customGitBranch, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - customGitUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs, (_data) => {}, { - env: { - ...process.env, - ...(customGitSSHKeyId && { - GIT_SSH_COMMAND: `ssh -i ${temporalKeyPath}${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`, - }), - }, - }); - } catch (error) { - throw error; - } -}; - -export const cloneRawGitRepositoryRemote = async (compose: Compose) => { - const { - appName, - customGitBranch, - customGitUrl, - customGitSSHKeyId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!customGitUrl) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Git Provider not found", - }); - } - - const { SSH_PATH, COMPOSE_PATH } = paths(true); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const knownHostsPath = path.join(SSH_PATH, "known_hosts"); - - if (customGitSSHKeyId) { - await updateSSHKeyById({ - sshKeyId: customGitSSHKeyId, - lastUsedAt: new Date().toISOString(), - }); - } - try { - const command = []; - if (!isHttpOrHttps(customGitUrl)) { - if (!customGitSSHKeyId) { - command.push( - `echo "Error: you are trying to clone a ssh repository without a ssh key, please set a ssh key ❌" ; - exit 1; - `, - ); - } - command.push(addHostToKnownHostsCommand(customGitUrl)); - } - command.push(`rm -rf ${outputPath};`); - command.push(`mkdir -p ${outputPath};`); - if (customGitSSHKeyId) { - const sshKey = await findSSHKeyById(customGitSSHKeyId); - const { port } = sanitizeRepoPathSSH(customGitUrl); - const gitSshCommand = `ssh -i /tmp/id_rsa${port ? ` -p ${port}` : ""} -o UserKnownHostsFile=${knownHostsPath}`; - command.push( - ` - echo "${sshKey.privateKey}" > /tmp/id_rsa - chmod 600 /tmp/id_rsa - export GIT_SSH_COMMAND="${gitSshCommand}" - `, - ); - } - - command.push( - `if ! git clone --branch ${customGitBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${customGitUrl} ${outputPath} ; then - echo "[ERROR] Fail to clone the repository "; - exit 1; - fi - `, - ); - - await execAsyncRemote(serverId, command.join("\n")); - } catch (error) { - throw error; - } -}; diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index db6dcbb78..5b69f6826 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -1,7 +1,5 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; -import type { Compose } from "@dokploy/server/services/compose"; import { findGiteaById, type Gitea, @@ -9,9 +7,6 @@ import { } from "@dokploy/server/services/gitea"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export const getErrorCloneRequirements = (entity: { giteaRepository?: string | null; @@ -119,79 +114,16 @@ export type ApplicationWithGitea = InferResultType< export type ComposeWithGitea = InferResultType<"compose", { gitea: true }>; -export const getGiteaCloneCommand = async ( - entity: ApplicationWithGitea | ComposeWithGitea, - logPath: string, - isCompose = false, -) => { - const { - appName, - giteaBranch, - giteaId, - giteaOwner, - giteaRepository, - serverId, - enableSubmodules, - } = entity; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!giteaId) { - const command = ` - echo "Error: ❌ Gitlab Provider not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); - } - - // Use paths(true) for remote operations - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - await refreshGiteaToken(giteaId); - const gitea = await findGiteaById(giteaId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - - const repoClone = `${giteaOwner}/${giteaRepository}.git`; - const cloneUrl = buildGiteaCloneUrl( - gitea?.giteaUrl!, - gitea?.accessToken!, - giteaOwner!, - giteaRepository!, - ); - - const cloneCommand = ` - rm -rf ${outputPath}; - mkdir -p ${outputPath}; - - if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath}; - exit 1; - fi - - echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath}; - `; - - return cloneCommand; +type GiteaClone = (ApplicationWithGitea | ComposeWithGitea) & { + serverId: string | null; + type?: "application" | "compose"; }; -export const cloneGiteaRepository = async ( - entity: ApplicationWithGitea | ComposeWithGitea, - logPath: string, - isCompose = false, -) => { - const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); - - const writeStream = createWriteStream(logPath, { flags: "a" }); +export const cloneGiteaRepository = async ({ + type = "application", + ...entity +}: GiteaClone) => { + let command = "set -e;"; const { appName, giteaBranch, @@ -199,27 +131,27 @@ export const cloneGiteaRepository = async ( giteaOwner, giteaRepository, enableSubmodules, + serverId, } = entity; + const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); if (!giteaId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); + command += `echo "Error: ❌ Gitea Provider not found"; exit 1;`; + return command; } await refreshGiteaToken(giteaId); const giteaProvider = await findGiteaById(giteaId); + if (!giteaProvider) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea provider not found in the database", - }); + command += `echo "❌ [ERROR] Gitea provider not found in the database"; exit 1;`; + return command; } - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const repoClone = `${giteaOwner}/${giteaRepository}.git`; const cloneUrl = buildGiteaCloneUrl( @@ -229,134 +161,9 @@ export const cloneGiteaRepository = async ( giteaRepository!, ); - writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`); - - try { - await spawnAsync( - "git", - [ - "clone", - "--branch", - giteaBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ], - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - ); - writeStream.write(`\nCloned ${repoClone}: ✅\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const cloneRawGiteaRepository = async (entity: Compose) => { - const { - appName, - giteaRepository, - giteaOwner, - giteaBranch, - giteaId, - enableSubmodules, - } = entity; - const { COMPOSE_PATH } = paths(); - - if (!giteaId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); - } - await refreshGiteaToken(giteaId); - const giteaProvider = await findGiteaById(giteaId); - if (!giteaProvider) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea provider not found in the database", - }); - } - - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - - const cloneUrl = buildGiteaCloneUrl( - giteaProvider.giteaUrl, - giteaProvider.accessToken!, - giteaOwner!, - giteaRepository!, - ); - - try { - await spawnAsync("git", [ - "clone", - "--branch", - giteaBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]); - } catch (error) { - throw error; - } -}; - -export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => { - const { - appName, - giteaRepository, - giteaOwner, - giteaBranch, - giteaId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!giteaId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitea Provider not found", - }); - } - const { COMPOSE_PATH } = paths(true); - const giteaProvider = await findGiteaById(giteaId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const cloneUrl = buildGiteaCloneUrl( - giteaProvider.giteaUrl, - giteaProvider.accessToken!, - giteaOwner!, - giteaRepository!, - ); - - try { - const command = ` - rm -rf ${outputPath}; - git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } + command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`; + command += `git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + return command; }; export const haveGiteaRequirements = (giteaProvider: Gitea) => { diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index 30125db8b..c0f1b651a 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -1,16 +1,11 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { apiFindGithubBranches } from "@dokploy/server/db/schema"; -import type { Compose } from "@dokploy/server/services/compose"; import { findGithubById, type Github } from "@dokploy/server/services/github"; import type { InferResultType } from "@dokploy/server/types/with"; import { createAppAuth } from "@octokit/auth-app"; import { TRPCError } from "@trpc/server"; import { Octokit } from "octokit"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export const authGithub = (githubProvider: Github): Octokit => { if (!haveGithubRequirements(githubProvider)) { @@ -123,42 +118,39 @@ interface CloneGithubRepository { branch: string | null; githubId: string | null; repository: string | null; - logPath: string; type?: "application" | "compose"; enableSubmodules: boolean; + serverId: string | null; } export const cloneGithubRepository = async ({ - logPath, type = "application", ...entity }: CloneGithubRepository) => { + let command = "set -e;"; const isCompose = type === "compose"; - const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(); - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { appName, repository, owner, branch, githubId, enableSubmodules } = - entity; + const { + appName, + repository, + owner, + branch, + githubId, + enableSubmodules, + serverId, + } = entity; + const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); if (!githubId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); + command += `echo "Error: ❌ Github Provider not found"; exit 1;`; + + return command; } const requirements = getErrorCloneRequirements(entity); // Check if requirements are met if (requirements.length > 0) { - writeStream.write( - `\nGitHub Repository configuration failed for application: ${appName}\n`, - ); - writeStream.write("Reasons:\n"); - writeStream.write(requirements.join("\n")); - writeStream.end(); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: GitHub repository information is incomplete.", - }); + command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`; + return command; } const githubProvider = await findGithubById(githubId); @@ -167,193 +159,15 @@ export const cloneGithubRepository = async ({ const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; - await recreateDirectory(outputPath); + // await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; - try { - writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`); - const cloneArgs = [ - "clone", - "--branch", - branch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; + command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`; + command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; - await spawnAsync("git", cloneArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - writeStream.write(`\nCloned ${repoclone}: ✅\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getGithubCloneCommand = async ({ - logPath, - type = "application", - ...entity -}: CloneGithubRepository & { serverId: string }) => { - const { - appName, - repository, - owner, - branch, - githubId, - serverId, - enableSubmodules, - } = entity; - const isCompose = type === "compose"; - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!githubId) { - const command = ` - echo "Error: ❌ Github Provider not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); - } - - const requirements = getErrorCloneRequirements(entity); - - // Build log messages - let logMessages = ""; - if (requirements.length > 0) { - logMessages += `\nGitHub Repository configuration failed for application: ${appName}\n`; - logMessages += "Reasons:\n"; - logMessages += requirements.join("\n"); - const escapedLogMessages = logMessages - .replace(/\\/g, "\\\\") - .replace(/"/g, '\\"') - .replace(/\n/g, "\\n"); - - const bashCommand = ` - echo "${escapedLogMessages}" >> ${logPath}; - exit 1; # Exit with error code - `; - - await execAsyncRemote(serverId, bashCommand); - return; - } - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - const githubProvider = await findGithubById(githubId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - const octokit = authGithub(githubProvider); - const token = await getGithubToken(octokit); - const repoclone = `github.com/${owner}/${repository}.git`; - const cloneUrl = `https://oauth2:${token}@${repoclone}`; - - const cloneCommand = ` -rm -rf ${outputPath}; -mkdir -p ${outputPath}; -if ! git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone repository ${repoclone}" >> ${logPath}; - exit 1; -fi -echo "Cloned ${repoclone} to ${outputPath}: ✅" >> ${logPath}; - `; - - return cloneCommand; -}; - -export const cloneRawGithubRepository = async (entity: Compose) => { - const { appName, repository, owner, branch, githubId, enableSubmodules } = - entity; - - if (!githubId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); - } - const { COMPOSE_PATH } = paths(); - const githubProvider = await findGithubById(githubId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const octokit = authGithub(githubProvider); - const token = await getGithubToken(octokit); - const repoclone = `github.com/${owner}/${repository}.git`; - await recreateDirectory(outputPath); - const cloneUrl = `https://oauth2:${token}@${repoclone}`; - try { - const cloneArgs = [ - "clone", - "--branch", - branch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - await spawnAsync("git", cloneArgs); - } catch (error) { - throw error; - } -}; - -export const cloneRawGithubRepositoryRemote = async (compose: Compose) => { - const { - appName, - repository, - owner, - branch, - githubId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!githubId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "GitHub Provider not found", - }); - } - - const { COMPOSE_PATH } = paths(true); - const githubProvider = await findGithubById(githubId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const octokit = authGithub(githubProvider); - const token = await getGithubToken(octokit); - const repoclone = `github.com/${owner}/${repository}.git`; - const cloneUrl = `https://oauth2:${token}@${repoclone}`; - try { - const command = ` - rm -rf ${outputPath}; - git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } + return command; }; export const getGithubRepositories = async (githubId?: string) => { diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 840347fdb..9c75cd239 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -1,8 +1,6 @@ -import { createWriteStream } from "node:fs"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { apiGitlabTestConnection } from "@dokploy/server/db/schema"; -import type { Compose } from "@dokploy/server/services/compose"; import { findGitlabById, type Gitlab, @@ -10,9 +8,6 @@ import { } from "@dokploy/server/services/gitlab"; import type { InferResultType } from "@dokploy/server/types/with"; import { TRPCError } from "@trpc/server"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export const refreshGitlabToken = async (gitlabProviderId: string) => { const gitlabProvider = await findGitlabById(gitlabProviderId); @@ -102,25 +97,29 @@ const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => { return cloneUrl; }; -export const cloneGitlabRepository = async ( - entity: ApplicationWithGitlab | ComposeWithGitlab, - logPath: string, - isCompose = false, -) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); +type GitlabClone = (ApplicationWithGitlab | ComposeWithGitlab) & { + serverId: string | null; + type?: "application" | "compose"; +}; + +export const cloneGitlabRepository = async ({ + type = "application", + ...entity +}: GitlabClone) => { + let command = "set -e;"; const { appName, gitlabBranch, gitlabId, gitlabPathNamespace, enableSubmodules, + serverId, } = entity; + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); if (!gitlabId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); + command += `echo "Error: ❌ Gitlab Provider not found"; exit 1;`; + return command; } await refreshGitlabToken(gitlabId); @@ -130,127 +129,19 @@ export const cloneGitlabRepository = async ( // Check if requirements are met if (requirements.length > 0) { - writeStream.write( - `\nGitLab Repository configuration failed for application: ${appName}\n`, - ); - writeStream.write("Reasons:\n"); - writeStream.write(requirements.join("\n")); - writeStream.end(); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error: GitLab repository information is incomplete.", - }); + command += `echo "❌ [ERROR] GitLab Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`; + return command; } - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); + command += `rm -rf ${outputPath};`; + command += `mkdir -p ${outputPath};`; const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); - try { - writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}: ✅\n`); - const cloneArgs = [ - "clone", - "--branch", - gitlabBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - - await spawnAsync("git", cloneArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - writeStream.write(`\nCloned ${repoClone}: ✅\n`); - } catch (error) { - writeStream.write(`ERROR Cloning: ${error}: ❌`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getGitlabCloneCommand = async ( - entity: ApplicationWithGitlab | ComposeWithGitlab, - logPath: string, - isCompose = false, -) => { - const { - appName, - gitlabPathNamespace, - gitlabBranch, - gitlabId, - serverId, - enableSubmodules, - } = entity; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - - if (!gitlabId) { - const command = ` - echo "Error: ❌ Gitlab Provider not found" >> ${logPath}; - exit 1; - `; - - await execAsyncRemote(serverId, command); - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); - } - - const requirements = getErrorCloneRequirements(entity); - - // Build log messages - let logMessages = ""; - if (requirements.length > 0) { - logMessages += `\nGitLab Repository configuration failed for application: ${appName}\n`; - logMessages += "Reasons:\n"; - logMessages += requirements.join("\n"); - const escapedLogMessages = logMessages - .replace(/\\/g, "\\\\") - .replace(/"/g, '\\"') - .replace(/\n/g, "\\n"); - - const bashCommand = ` - echo "${escapedLogMessages}" >> ${logPath}; - exit 1; # Exit with error code - `; - - await execAsyncRemote(serverId, bashCommand); - return; - } - - const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(true); - await refreshGitlabToken(gitlabId); - const gitlab = await findGitlabById(gitlabId); - const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoClone = getGitlabRepoClone(gitlab, gitlabPathNamespace); - const cloneUrl = getGitlabCloneUrl(gitlab, repoClone); - const cloneCommand = ` -rm -rf ${outputPath}; -mkdir -p ${outputPath}; -if ! git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} --progress ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then - echo "❌ [ERROR] Fail to clone the repository ${repoClone}" >> ${logPath}; - exit 1; -fi -echo "Cloned ${repoClone} to ${outputPath}: ✅" >> ${logPath}; - `; - - return cloneCommand; + command += `echo "Cloning Repo ${repoClone} to ${outputPath}: ✅";`; + command += `git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; + return command; }; export const getGitlabRepositories = async (gitlabId?: string) => { @@ -355,88 +246,6 @@ export const getGitlabBranches = async (input: { }[]; }; -export const cloneRawGitlabRepository = async (entity: Compose) => { - const { - appName, - gitlabBranch, - gitlabId, - gitlabPathNamespace, - enableSubmodules, - } = entity; - - if (!gitlabId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); - } - - const { COMPOSE_PATH } = paths(); - await refreshGitlabToken(gitlabId); - const gitlabProvider = await findGitlabById(gitlabId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - await recreateDirectory(outputPath); - const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace); - const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone); - try { - const cloneArgs = [ - "clone", - "--branch", - gitlabBranch!, - "--depth", - "1", - ...(enableSubmodules ? ["--recurse-submodules"] : []), - cloneUrl, - outputPath, - "--progress", - ]; - await spawnAsync("git", cloneArgs); - } catch (error) { - throw error; - } -}; - -export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => { - const { - appName, - gitlabPathNamespace, - gitlabBranch, - gitlabId, - serverId, - enableSubmodules, - } = compose; - - if (!serverId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Server not found", - }); - } - if (!gitlabId) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Gitlab Provider not found", - }); - } - const { COMPOSE_PATH } = paths(true); - await refreshGitlabToken(gitlabId); - const gitlabProvider = await findGitlabById(gitlabId); - const basePath = COMPOSE_PATH; - const outputPath = join(basePath, appName, "code"); - const repoClone = getGitlabRepoClone(gitlabProvider, gitlabPathNamespace); - const cloneUrl = getGitlabCloneUrl(gitlabProvider, repoClone); - try { - const command = ` - rm -rf ${outputPath}; - git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } -}; - export const testGitlabConnection = async ( input: typeof apiGitlabTestConnection._type, ) => { diff --git a/packages/server/src/utils/providers/raw.ts b/packages/server/src/utils/providers/raw.ts index 34ba0012a..7b541867d 100644 --- a/packages/server/src/utils/providers/raw.ts +++ b/packages/server/src/utils/providers/raw.ts @@ -1,4 +1,3 @@ -import { createWriteStream } from "node:fs"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; @@ -7,33 +6,7 @@ import { encodeBase64 } from "../docker/utils"; import { recreateDirectory } from "../filesystem/directory"; import { execAsyncRemote } from "../process/execAsync"; -export const createComposeFile = async (compose: Compose, logPath: string) => { - const { COMPOSE_PATH } = paths(); - const { appName, composeFile } = compose; - const writeStream = createWriteStream(logPath, { flags: "a" }); - const outputPath = join(COMPOSE_PATH, appName, "code"); - - try { - await recreateDirectory(outputPath); - writeStream.write( - `\nCreating File 'docker-compose.yml' to ${outputPath}: ✅\n`, - ); - - await writeFile(join(outputPath, "docker-compose.yml"), composeFile); - - writeStream.write(`\nFile 'docker-compose.yml' created: ✅\n`); - } catch (error) { - writeStream.write(`\nERROR Creating Compose File: ${error}: ❌\n`); - throw error; - } finally { - writeStream.end(); - } -}; - -export const getCreateComposeFileCommand = ( - compose: Compose, - logPath: string, -) => { +export const getCreateComposeFileCommand = (compose: Compose) => { const { COMPOSE_PATH } = paths(true); const { appName, composeFile } = compose; const outputPath = join(COMPOSE_PATH, appName, "code"); @@ -43,7 +16,7 @@ export const getCreateComposeFileCommand = ( rm -rf ${outputPath}; mkdir -p ${outputPath}; echo "${encodedContent}" | base64 -d > "${filePath}"; - echo "File 'docker-compose.yml' created: ✅" >> ${logPath}; + echo "File 'docker-compose.yml' created: ✅"; `; return bashCommand; }; From 5ac32f9f24a6b56b20a0397042e799424b68391e Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:16:18 -0600 Subject: [PATCH 07/79] Refactor repository cloning interfaces: standardize parameters for Bitbucket, Gitea, and GitLab repository cloning functions to improve consistency and maintainability across the codebase. --- packages/server/src/services/application.ts | 1 - packages/server/src/utils/providers/bitbucket.ts | 14 +++++++++++++- packages/server/src/utils/providers/gitea.ts | 13 ++++++++++++- packages/server/src/utils/providers/gitlab.ts | 11 ++++++++--- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index e2c38a9fa..c3508352c 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -29,7 +29,6 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; -import { cleanUpSystemPrune, encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeployment, diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index 0267ec9b3..8a62c5d22 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -81,10 +81,22 @@ type BitbucketClone = (ApplicationWithBitbucket | ComposeWithBitbucket) & { type?: "application" | "compose"; }; +interface CloneBitbucketRepository { + appName: string; + bitbucketRepository: string | null; + bitbucketOwner: string | null; + bitbucketBranch: string | null; + bitbucketId: string | null; + bitbucket: Bitbucket | null; + enableSubmodules: boolean; + serverId: string | null; + type?: "application" | "compose"; +} + export const cloneBitbucketRepository = async ({ type = "application", ...entity -}: BitbucketClone) => { +}: CloneBitbucketRepository) => { let command = "set -e;"; const { appName, diff --git a/packages/server/src/utils/providers/gitea.ts b/packages/server/src/utils/providers/gitea.ts index 5b69f6826..ec8946ab3 100644 --- a/packages/server/src/utils/providers/gitea.ts +++ b/packages/server/src/utils/providers/gitea.ts @@ -119,10 +119,21 @@ type GiteaClone = (ApplicationWithGitea | ComposeWithGitea) & { type?: "application" | "compose"; }; +interface CloneGiteaRepository { + appName: string; + giteaBranch: string | null; + giteaId: string | null; + giteaOwner: string | null; + giteaRepository: string | null; + enableSubmodules: boolean; + serverId: string | null; + type?: "application" | "compose"; +} + export const cloneGiteaRepository = async ({ type = "application", ...entity -}: GiteaClone) => { +}: CloneGiteaRepository) => { let command = "set -e;"; const { appName, diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index 9c75cd239..a3106b8c0 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -97,15 +97,20 @@ const getGitlabCloneUrl = (gitlab: GitlabInfo, repoClone: string) => { return cloneUrl; }; -type GitlabClone = (ApplicationWithGitlab | ComposeWithGitlab) & { +interface CloneGitlabRepository { + appName: string; + gitlabBranch: string | null; + gitlabId: string | null; + gitlabPathNamespace: string | null; + enableSubmodules: boolean; serverId: string | null; type?: "application" | "compose"; -}; +} export const cloneGitlabRepository = async ({ type = "application", ...entity -}: GitlabClone) => { +}: CloneGitlabRepository) => { let command = "set -e;"; const { appName, From f96114ad80b97b2fef9b9e3e8d3b1b0c6f38e5d0 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:18:07 -0600 Subject: [PATCH 08/79] Refactor Bitbucket repository cloning logic: remove unused parameter and enhance error handling by retrieving Bitbucket provider directly within the function. --- packages/server/src/utils/providers/bitbucket.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index 8a62c5d22..217dba271 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -87,7 +87,6 @@ interface CloneBitbucketRepository { bitbucketOwner: string | null; bitbucketBranch: string | null; bitbucketId: string | null; - bitbucket: Bitbucket | null; enableSubmodules: boolean; serverId: string | null; type?: "application" | "compose"; @@ -104,7 +103,6 @@ export const cloneBitbucketRepository = async ({ bitbucketOwner, bitbucketBranch, bitbucketId, - bitbucket, enableSubmodules, serverId, } = entity; @@ -114,7 +112,12 @@ export const cloneBitbucketRepository = async ({ command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`; return command; } + const bitbucket = await findBitbucketById(bitbucketId); + if (!bitbucket) { + command += `echo "Error: ❌ Bitbucket Provider not found"; exit 1;`; + return command; + } const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; const outputPath = join(basePath, appName, "code"); command += `rm -rf ${outputPath};`; From a05b75fc6792e0c0ad5e752df6d0e813cf53b5a1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:24:13 -0600 Subject: [PATCH 09/79] Refactor deployment logic: remove unused remote preview deployment function, streamline deployment commands, and enhance error handling for Docker image pulling. Update build command generation for Docker source type. --- .../server/queues/deployments-queue.ts | 27 +--- packages/server/src/services/application.ts | 152 +++--------------- packages/server/src/utils/builders/index.ts | 4 + packages/server/src/utils/providers/docker.ts | 70 +------- 4 files changed, 42 insertions(+), 211 deletions(-) diff --git a/apps/dokploy/server/queues/deployments-queue.ts b/apps/dokploy/server/queues/deployments-queue.ts index a1d9d29f1..4c117e7e3 100644 --- a/apps/dokploy/server/queues/deployments-queue.ts +++ b/apps/dokploy/server/queues/deployments-queue.ts @@ -2,7 +2,6 @@ import { deployApplication, deployCompose, deployPreviewApplication, - deployRemotePreviewApplication, rebuildApplication, rebuildCompose, updateApplicationStatus, @@ -54,24 +53,14 @@ export const deploymentWorker = new Worker( await updatePreviewDeployment(job.data.previewDeploymentId, { previewStatus: "running", }); - if (job.data.server) { - if (job.data.type === "deploy") { - await deployRemotePreviewApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - previewDeploymentId: job.data.previewDeploymentId, - }); - } - } else { - if (job.data.type === "deploy") { - await deployPreviewApplication({ - applicationId: job.data.applicationId, - titleLog: job.data.titleLog, - descriptionLog: job.data.descriptionLog, - previewDeploymentId: job.data.previewDeploymentId, - }); - } + + if (job.data.type === "deploy") { + await deployPreviewApplication({ + applicationId: job.data.applicationId, + titleLog: job.data.titleLog, + descriptionLog: job.data.descriptionLog, + previewDeploymentId: job.data.previewDeploymentId, + }); } } } catch (error) { diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index c3508352c..181467a76 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -18,10 +18,7 @@ import { execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; -import { - buildDocker, - buildRemoteDocker, -} from "@dokploy/server/utils/providers/docker"; +import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker"; import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; @@ -192,9 +189,12 @@ export const deployApplication = async ({ command += await cloneGitRepository(application); } else if (application.sourceType === "drop") { await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "docker") { + command += await buildRemoteDocker(application); } command += getBuildCommand(application); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (application.serverId) { await execAsyncRemote(application.serverId, commandWithLog); @@ -206,16 +206,16 @@ export const deployApplication = async ({ await updateDeploymentStatus(deployment.deploymentId, "done"); await updateApplicationStatus(applicationId, "done"); - // if (application.rollbackActive) { - // const tagImage = - // application.sourceType === "docker" - // ? application.dockerImage - // : application.appName; - // await createRollback({ - // appName: tagImage || "", - // deploymentId: deployment.deploymentId, - // }); - // } + if (application.rollbackActive) { + const tagImage = + application.sourceType === "docker" + ? application.dockerImage + : application.appName; + await createRollback({ + appName: tagImage || "", + deploymentId: deployment.deploymentId, + }); + } await sendBuildSuccessNotifications({ projectName: application.environment.project.name, @@ -359,131 +359,23 @@ export const deployPreviewApplication = async ({ application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; + let command = "set -e;"; if (application.sourceType === "github") { - await cloneGithubRepository({ + command += await cloneGithubRepository({ ...application, appName: previewDeployment.appName, branch: previewDeployment.branch, - logPath: deployment.logPath, }); - await buildApplication(application, deployment.logPath); - } - const successComment = getIssueComment( - application.name, - "success", - previewDomain, - ); - await updateIssueComment({ - ...issueParams, - body: `### Dokploy Preview Deployment\n\n${successComment}`, - }); - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updatePreviewDeployment(previewDeploymentId, { - previewStatus: "done", - }); - } catch (error) { - const comment = getIssueComment(application.name, "error", previewDomain); - await updateIssueComment({ - ...issueParams, - body: `### Dokploy Preview Deployment\n\n${comment}`, - }); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updatePreviewDeployment(previewDeploymentId, { - previewStatus: "error", - }); - throw error; - } + command += getBuildCommand(application); - return true; -}; - -export const deployRemotePreviewApplication = async ({ - applicationId, - titleLog = "Preview Deployment", - descriptionLog = "", - previewDeploymentId, -}: { - applicationId: string; - titleLog: string; - descriptionLog: string; - previewDeploymentId: string; -}) => { - const application = await findApplicationById(applicationId); - - const deployment = await createDeploymentPreview({ - title: titleLog, - description: descriptionLog, - previewDeploymentId: previewDeploymentId, - }); - - const previewDeployment = - await findPreviewDeploymentById(previewDeploymentId); - - await updatePreviewDeployment(previewDeploymentId, { - createdAt: new Date().toISOString(), - }); - - const previewDomain = getDomainHost(previewDeployment?.domain as Domain); - const issueParams = { - owner: application?.owner || "", - repository: application?.repository || "", - issue_number: previewDeployment.pullRequestNumber, - comment_id: Number.parseInt(previewDeployment.pullRequestCommentId), - githubId: application?.githubId || "", - }; - try { - const commentExists = await issueCommentExists({ - ...issueParams, - }); - if (!commentExists) { - const result = await createPreviewDeploymentComment({ - ...issueParams, - previewDomain, - appName: previewDeployment.appName, - githubId: application?.githubId || "", - previewDeploymentId, - }); - - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Pull request comment not found", - }); + const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (application.serverId) { + await execAsyncRemote(application.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); } - - issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId); - } - const buildingComment = getIssueComment( - application.name, - "running", - previewDomain, - ); - await updateIssueComment({ - ...issueParams, - body: `### Dokploy Preview Deployment\n\n${buildingComment}`, - }); - application.appName = previewDeployment.appName; - application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; - application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; - application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; - - if (application.serverId) { - let command = "set -e;"; - if (application.sourceType === "github") { - command += await cloneGithubRepository({ - ...application, - appName: previewDeployment.appName, - branch: previewDeployment.branch, - serverId: application.serverId, - logPath: deployment.logPath, - }); - } - - command += getBuildCommand(application, deployment.logPath); - await execAsyncRemote(application.serverId, command); await mechanizeDockerContainer(application); } - const successComment = getIssueComment( application.name, "success", diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 35172789c..d3637ca6b 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -79,6 +79,10 @@ export const buildApplication = async ( export const getBuildCommand = (application: ApplicationNested) => { let command = ""; const { buildType, registry } = application; + + if (application.sourceType === "docker") { + return ""; + } switch (buildType) { case "nixpacks": command = getNixpacksCommand(application); diff --git a/packages/server/src/utils/providers/docker.ts b/packages/server/src/utils/providers/docker.ts index 56341b7d6..06f962dc7 100644 --- a/packages/server/src/utils/providers/docker.ts +++ b/packages/server/src/utils/providers/docker.ts @@ -1,60 +1,6 @@ -import { createWriteStream } from "node:fs"; -import { type ApplicationNested, mechanizeDockerContainer } from "../builders"; -import { pullImage } from "../docker/utils"; +import type { ApplicationNested } from "../builders"; -interface RegistryAuth { - username: string; - password: string; - registryUrl: string; -} - -export const buildDocker = async ( - application: ApplicationNested, - logPath: string, -): Promise => { - const { buildType, dockerImage, username, password } = application; - const authConfig: Partial = { - username: username || "", - password: password || "", - registryUrl: application.registryUrl || "", - }; - - const writeStream = createWriteStream(logPath, { flags: "a" }); - - writeStream.write(`\nBuild ${buildType}\n`); - - writeStream.write(`Pulling ${dockerImage}: ✅\n`); - - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } - - await pullImage( - dockerImage, - (data) => { - if (writeStream.writable) { - writeStream.write(`${data}\n`); - } - }, - authConfig, - ); - await mechanizeDockerContainer(application); - writeStream.write("\nDocker Deployed: ✅\n"); - } catch (error) { - writeStream.write( - `❌ Error: ${error instanceof Error ? error.message : String(error)}`, - ); - throw error; - } finally { - writeStream.end(); - } -}; - -export const buildRemoteDocker = async ( - application: ApplicationNested, - logPath: string, -) => { +export const buildRemoteDocker = async (application: ApplicationNested) => { const { registryUrl, dockerImage, username, password } = application; try { @@ -62,25 +8,25 @@ export const buildRemoteDocker = async ( throw new Error("Docker image not found"); } let command = ` -echo "Pulling ${dockerImage}" >> ${logPath}; +echo "Pulling ${dockerImage}"; `; if (username && password) { command += ` -if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then - echo "❌ Login failed" >> ${logPath}; +if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" 2>&1; then + echo "❌ Login failed"; exit 1; fi `; } command += ` -docker pull ${dockerImage} >> ${logPath} 2>> ${logPath} || { - echo "❌ Pulling image failed" >> ${logPath}; +docker pull ${dockerImage} 2>&1 || { + echo "❌ Pulling image failed"; exit 1; } -echo "✅ Pulling image completed." >> ${logPath}; +echo "✅ Pulling image completed."; `; return command; } catch (error) { From ef10996dd86279f65f915148240a26ae2f515b69 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:28:32 -0600 Subject: [PATCH 10/79] Refactor builder utilities: remove unused build functions for Docker, Heroku, Nixpacks, Paketo, and Railpack, streamlining the codebase. Update static command generation to enhance clarity and maintainability. --- .../server/src/utils/builders/docker-file.ts | 93 +------------ packages/server/src/utils/builders/heroku.ts | 43 ------ .../server/src/utils/builders/nixpacks.ts | 123 +++--------------- packages/server/src/utils/builders/paketo.ts | 42 ------ .../server/src/utils/builders/railpack.ts | 101 -------------- packages/server/src/utils/builders/static.ts | 59 +-------- 6 files changed, 21 insertions(+), 440 deletions(-) diff --git a/packages/server/src/utils/builders/docker-file.ts b/packages/server/src/utils/builders/docker-file.ts index 4636cb22c..a0acf5e6c 100644 --- a/packages/server/src/utils/builders/docker-file.ts +++ b/packages/server/src/utils/builders/docker-file.ts @@ -1,4 +1,3 @@ -import type { WriteStream } from "node:fs"; import { getEnviromentVariablesObject, prepareEnvironmentVariables, @@ -7,98 +6,8 @@ import { getBuildAppDirectory, getDockerContextPath, } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -import { createEnvFile, createEnvFileCommand } from "./utils"; - -export const buildCustomDocker = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { - appName, - env, - publishDirectory, - buildArgs, - buildSecrets, - dockerBuildStage, - cleanCache, - } = application; - const dockerFilePath = getBuildAppDirectory(application); - try { - const image = `${appName}`; - - const defaultContextPath = - dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; - - const dockerContextPath = getDockerContextPath(application); - - const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; - - if (cleanCache) { - commandArgs.push("--no-cache"); - } - - if (dockerBuildStage) { - commandArgs.push("--target", dockerBuildStage); - } - - const args = prepareEnvironmentVariables( - buildArgs, - application.environment.project.env, - application.environment.env, - ); - - for (const arg of args) { - commandArgs.push("--build-arg", arg); - } - - const secrets = getEnviromentVariablesObject( - buildSecrets, - application.environment.project.env, - application.environment.env, - ); - - for (const key in secrets) { - // Although buildx is smart enough to know we may be referring to an environment variable name, - // we still make sure it doesn't fall back to type=file. - // See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret - commandArgs.push("--secret", `type=env,id=${key}`); - } - - /* - Do not generate an environment file when publishDirectory is specified, - as it could be publicly exposed. - */ - if (!publishDirectory) { - createEnvFile( - dockerFilePath, - env, - application.environment.project.env, - application.environment.env, - ); - } - - await spawnAsync( - "docker", - commandArgs, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - cwd: dockerContextPath || defaultContextPath, - env: { - ...process.env, - ...secrets, - }, - }, - ); - } catch (error) { - throw error; - } -}; +import { createEnvFileCommand } from "./utils"; export const getDockerCommand = (application: ApplicationNested) => { const { diff --git a/packages/server/src/utils/builders/heroku.ts b/packages/server/src/utils/builders/heroku.ts index a0a8da153..e1ab4dff4 100644 --- a/packages/server/src/utils/builders/heroku.ts +++ b/packages/server/src/utils/builders/heroku.ts @@ -1,50 +1,7 @@ -import type { WriteStream } from "node:fs"; import { prepareEnvironmentVariables } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker -export const buildHeroku = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { env, appName, cleanCache } = application; - const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - try { - const args = [ - "build", - appName, - "--path", - buildAppDirectory, - "--builder", - `heroku/builder:${application.herokuVersion || "24"}`, - ]; - - for (const env of envVariables) { - args.push("--env", env); - } - - if (cleanCache) { - args.push("--clear-cache"); - } - - await spawnAsync("pack", args, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - return true; - } catch (e) { - throw e; - } -}; - export const getHerokuCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index 705539f06..37f1953a4 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -1,97 +1,10 @@ -import { existsSync, mkdirSync, type WriteStream } from "node:fs"; import path from "node:path"; -import { - buildStatic, - getStaticCommand, -} from "@dokploy/server/utils/builders/static"; +import { getStaticCommand } from "@dokploy/server/utils/builders/static"; import { nanoid } from "nanoid"; import { prepareEnvironmentVariables } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -export const buildNixpacks = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { env, appName, publishDirectory, cleanCache } = application; - - const buildAppDirectory = getBuildAppDirectory(application); - const buildContainerId = `${appName}-${nanoid(10)}`; - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - - const writeToStream = (data: string) => { - if (writeStream.writable) { - writeStream.write(data); - } - }; - - try { - const args = ["build", buildAppDirectory, "--name", appName]; - - if (cleanCache) { - args.push("--no-cache"); - } - - for (const env of envVariables) { - args.push("--env", env); - } - - if (publishDirectory) { - /* No need for any start command, since we'll use nginx later on */ - args.push("--no-error-without-start"); - } - - await spawnAsync("nixpacks", args, writeToStream); - - /* - Run the container with the image created by nixpacks, - and copy the artifacts on the host filesystem. - Then, remove the container and create a static build. - */ - if (publishDirectory) { - await spawnAsync( - "docker", - ["create", "--name", buildContainerId, appName], - writeToStream, - ); - - const localPath = path.join(buildAppDirectory, publishDirectory); - - if (!existsSync(path.dirname(localPath))) { - mkdirSync(path.dirname(localPath), { recursive: true }); - } - - // https://docs.docker.com/reference/cli/docker/container/cp/ - const isDirectory = - publishDirectory.endsWith("/") || !path.extname(publishDirectory); - - await spawnAsync( - "docker", - [ - "cp", - `${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""}`, - localPath, - ], - writeToStream, - ); - - await spawnAsync("docker", ["rm", buildContainerId], writeToStream); - - await buildStatic(application, writeStream); - } - return true; - } catch (e) { - await spawnAsync("docker", ["rm", buildContainerId], writeToStream); - - throw e; - } -}; - export const getNixpacksCommand = (application: ApplicationNested) => { const { env, appName, publishDirectory, cleanCache } = application; @@ -118,7 +31,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => { args.push("--no-error-without-start"); } const command = `nixpacks ${args.join(" ")}`; - const bashCommand = ` + let bashCommand = ` echo "Starting nixpacks build..." ; ${command} || { echo "❌ Nixpacks build failed" ; @@ -132,23 +45,23 @@ export const getNixpacksCommand = (application: ApplicationNested) => { and copy the artifacts on the host filesystem. Then, remove the container and create a static build. */ - // if (publishDirectory) { - // const localPath = path.join(buildAppDirectory, publishDirectory); - // const isDirectory = - // publishDirectory.endsWith("/") || !path.extname(publishDirectory); + if (publishDirectory) { + const localPath = path.join(buildAppDirectory, publishDirectory); + const isDirectory = + publishDirectory.endsWith("/") || !path.extname(publishDirectory); - // bashCommand += ` - // docker create --name ${buildContainerId} ${appName} - // mkdir -p ${localPath} - // docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || { - // docker rm ${buildContainerId} - // echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ; - // exit 1; - // } - // docker rm ${buildContainerId} - // ${getStaticCommand(application)} - // `; - // } + bashCommand += ` + docker create --name ${buildContainerId} ${appName} + mkdir -p ${localPath} + docker cp ${buildContainerId}:/app/${publishDirectory}${isDirectory ? "/." : ""} ${path.join(buildAppDirectory, publishDirectory)} || { + docker rm ${buildContainerId} + echo "❌ Copying ${publishDirectory} to ${path.join(buildAppDirectory, publishDirectory)} failed" ; + exit 1; + } + docker rm ${buildContainerId} + ${getStaticCommand(application)} + `; + } return bashCommand; }; diff --git a/packages/server/src/utils/builders/paketo.ts b/packages/server/src/utils/builders/paketo.ts index 51e2301f9..eb9767e7f 100644 --- a/packages/server/src/utils/builders/paketo.ts +++ b/packages/server/src/utils/builders/paketo.ts @@ -1,49 +1,7 @@ -import type { WriteStream } from "node:fs"; import { prepareEnvironmentVariables } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; -export const buildPaketo = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { env, appName, cleanCache } = application; - const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - try { - const args = [ - "build", - appName, - "--path", - buildAppDirectory, - "--builder", - "paketobuildpacks/builder-jammy-full", - ]; - - if (cleanCache) { - args.push("--clear-cache"); - } - - for (const env of envVariables) { - args.push("--env", env); - } - - await spawnAsync("pack", args, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - return true; - } catch (e) { - throw e; - } -}; - export const getPaketoCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 822257773..cb188fd09 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -1,13 +1,10 @@ import { createHash } from "node:crypto"; -import type { WriteStream } from "node:fs"; import { nanoid } from "nanoid"; import { parseEnvironmentKeyValuePair, prepareEnvironmentVariables, } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; -import { execAsync } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; import type { ApplicationNested } from "."; const calculateSecretsHash = (envVariables: string[]): string => { @@ -18,104 +15,6 @@ const calculateSecretsHash = (envVariables: string[]): string => { return hash.digest("hex"); }; -export const buildRailpack = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { env, appName, cleanCache } = application; - const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( - env, - application.environment.project.env, - application.environment.env, - ); - - try { - await execAsync( - "docker buildx create --use --name builder-containerd --driver docker-container || true", - ); - - await execAsync("docker buildx use builder-containerd"); - - // First prepare the build plan and info - const prepareArgs = [ - "prepare", - buildAppDirectory, - "--plan-out", - `${buildAppDirectory}/railpack-plan.json`, - "--info-out", - `${buildAppDirectory}/railpack-info.json`, - ]; - - // Add environment variables to prepare command - for (const env of envVariables) { - prepareArgs.push("--env", env); - } - - // Run prepare command - await spawnAsync("railpack", prepareArgs, (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - - // Calculate secrets hash for layer invalidation - const secretsHash = calculateSecretsHash(envVariables); - - // Build with BuildKit using the Railpack frontend - const cacheKey = cleanCache ? nanoid(10) : undefined; - const buildArgs = [ - "buildx", - "build", - ...(cacheKey - ? [ - "--build-arg", - `secrets-hash=${secretsHash}`, - "--build-arg", - `cache-key=${cacheKey}`, - ] - : []), - "--build-arg", - `BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`, - "-f", - `${buildAppDirectory}/railpack-plan.json`, - "--output", - `type=docker,name=${appName}`, - ]; - - // Add secrets properly formatted - const env: { [key: string]: string } = {}; - for (const pair of envVariables) { - const [key, value] = parseEnvironmentKeyValuePair(pair); - if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); - env[key] = value; - } - } - - buildArgs.push(buildAppDirectory); - - await spawnAsync( - "docker", - buildArgs, - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - { - env: { ...process.env, ...env }, - }, - ); - - return true; - } catch (e) { - throw e; - } finally { - await execAsync("docker buildx rm builder-containerd"); - } -}; - export const getRailpackCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index 5e1f10cc3..4ddb290be 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -1,9 +1,5 @@ -import type { WriteStream } from "node:fs"; -import { - buildCustomDocker, - getDockerCommand, -} from "@dokploy/server/utils/builders/docker-file"; -import { createFile, getCreateFileCommand } from "../docker/utils"; +import { getDockerCommand } from "@dokploy/server/utils/builders/docker-file"; +import { getCreateFileCommand } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -32,57 +28,6 @@ http { } `; -export const buildStatic = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const { publishDirectory, isStaticSpa } = application; - const buildAppDirectory = getBuildAppDirectory(application); - - try { - if (isStaticSpa) { - createFile(buildAppDirectory, "nginx.conf", nginxSpaConfig); - } - - createFile( - buildAppDirectory, - ".dockerignore", - [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), - ); - - createFile( - buildAppDirectory, - "Dockerfile", - [ - "FROM nginx:alpine", - "WORKDIR /usr/share/nginx/html/", - isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "", - `COPY ${publishDirectory || "."} .`, - 'CMD ["nginx", "-g", "daemon off;"]', - ].join("\n"), - ); - - createFile( - buildAppDirectory, - ".dockerignore", - [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), - ); - - await buildCustomDocker( - { - ...application, - buildType: "dockerfile", - dockerfile: "Dockerfile", - }, - writeStream, - ); - - return true; - } catch (e) { - throw e; - } -}; - export const getStaticCommand = (application: ApplicationNested) => { const { publishDirectory } = application; const buildAppDirectory = getBuildAppDirectory(application); From 668aaf9a91b17fe118455dc585e4c64da408c334 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:29:40 -0600 Subject: [PATCH 11/79] Refactor deployment utilities: rename remote deployment functions for clarity and consistency, enhancing the deployment logic in the application. Streamline the build application function by commenting out unused build types to improve maintainability. --- apps/api/src/utils.ts | 20 +++++------ packages/server/src/utils/builders/index.ts | 38 ++++++++++----------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts index ee2ac3e50..0d0b574fc 100644 --- a/apps/api/src/utils.ts +++ b/apps/api/src/utils.ts @@ -1,9 +1,9 @@ import { - deployRemoteApplication, - deployRemoteCompose, - deployRemotePreviewApplication, - rebuildRemoteApplication, - rebuildRemoteCompose, + deployApplication, + deployCompose, + deployPreviewApplication, + rebuildApplication, + rebuildCompose, updateApplicationStatus, updateCompose, updatePreviewDeployment, @@ -16,13 +16,13 @@ export const deploy = async (job: DeployJob) => { await updateApplicationStatus(job.applicationId, "running"); if (job.server) { if (job.type === "redeploy") { - await rebuildRemoteApplication({ + await rebuildApplication({ applicationId: job.applicationId, titleLog: job.titleLog || "Rebuild deployment", descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { - await deployRemoteApplication({ + await deployApplication({ applicationId: job.applicationId, titleLog: job.titleLog || "Manual deployment", descriptionLog: job.descriptionLog || "", @@ -36,13 +36,13 @@ export const deploy = async (job: DeployJob) => { if (job.server) { if (job.type === "redeploy") { - await rebuildRemoteCompose({ + await rebuildCompose({ composeId: job.composeId, titleLog: job.titleLog || "Rebuild deployment", descriptionLog: job.descriptionLog || "", }); } else if (job.type === "deploy") { - await deployRemoteCompose({ + await deployCompose({ composeId: job.composeId, titleLog: job.titleLog || "Manual deployment", descriptionLog: job.descriptionLog || "", @@ -55,7 +55,7 @@ export const deploy = async (job: DeployJob) => { }); if (job.server) { if (job.type === "deploy") { - await deployRemotePreviewApplication({ + await deployPreviewApplication({ applicationId: job.applicationId, titleLog: job.titleLog || "Preview Deployment", descriptionLog: job.descriptionLog || "", diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index d3637ca6b..207238ca5 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -11,12 +11,12 @@ import { prepareEnvironmentVariables, } from "../docker/utils"; import { getRemoteDocker } from "../servers/remote-docker"; -import { buildCustomDocker, getDockerCommand } from "./docker-file"; -import { buildHeroku, getHerokuCommand } from "./heroku"; -import { buildNixpacks, getNixpacksCommand } from "./nixpacks"; -import { buildPaketo, getPaketoCommand } from "./paketo"; -import { buildRailpack, getRailpackCommand } from "./railpack"; -import { buildStatic, getStaticCommand } from "./static"; +import { getDockerCommand } from "./docker-file"; +import { getHerokuCommand } from "./heroku"; +import { getNixpacksCommand } from "./nixpacks"; +import { getPaketoCommand } from "./paketo"; +import { getRailpackCommand } from "./railpack"; +import { getStaticCommand } from "./static"; // NIXPACKS codeDirectory = where is the path of the code directory // HEROKU codeDirectory = where is the path of the code directory @@ -45,19 +45,19 @@ export const buildApplication = async ( `\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`, ); console.log(`Build ${buildType}: ✅`); - if (buildType === "nixpacks") { - await buildNixpacks(application, writeStream); - } else if (buildType === "heroku_buildpacks") { - await buildHeroku(application, writeStream); - } else if (buildType === "paketo_buildpacks") { - await buildPaketo(application, writeStream); - } else if (buildType === "dockerfile") { - await buildCustomDocker(application, writeStream); - } else if (buildType === "static") { - await buildStatic(application, writeStream); - } else if (buildType === "railpack") { - await buildRailpack(application, writeStream); - } + // if (buildType === "nixpacks") { + // await buildNixpacks(application, writeStream); + // } else if (buildType === "heroku_buildpacks") { + // await buildHeroku(application, writeStream); + // } else if (buildType === "paketo_buildpacks") { + // await buildPaketo(application, writeStream); + // } else if (buildType === "dockerfile") { + // await buildCustomDocker(application, writeStream); + // } else if (buildType === "static") { + // await buildStatic(application, writeStream); + // } else if (buildType === "railpack") { + // await buildRailpack(application, writeStream); + // } if (application.registryId) { await uploadImage(application, writeStream); From f718ab334e2ebb9f63b2090ca612b484ed8ddd57 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:42:43 -0600 Subject: [PATCH 12/79] Refactor compose utilities: remove unused functions and streamline the buildCompose logic for improved maintainability. Update domain handling by retaining only the necessary remote function, enhancing clarity in the codebase. --- packages/server/src/utils/builders/compose.ts | 114 +----------------- packages/server/src/utils/docker/domain.ts | 19 --- packages/server/src/utils/providers/raw.ts | 37 +----- 3 files changed, 2 insertions(+), 168 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 6b26a0d1c..7792ed11c 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -1,100 +1,18 @@ -import { - createWriteStream, - existsSync, - mkdirSync, - writeFileSync, -} from "node:fs"; import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; -import { - writeDomainsToCompose, - writeDomainsToComposeRemote, -} from "../docker/domain"; +import { writeDomainsToComposeRemote } from "../docker/domain"; import { encodeBase64, getEnviromentVariablesObject, prepareEnvironmentVariables, } from "../docker/utils"; -import { execAsync } from "../process/execAsync"; -import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< "compose", { environment: { with: { project: true } }; mounts: true; domains: true } >; -export const buildCompose = async (compose: ComposeNested, logPath: string) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { sourceType, appName, mounts, composeType, domains } = compose; - try { - const { COMPOSE_PATH } = paths(); - const command = createCommand(compose); - await writeDomainsToCompose(compose, domains); - createEnvFile(compose); - - if (compose.isolatedDeployment) { - await execAsync( - `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}`, - ); - } - - const logContent = ` - App Name: ${appName} - Build Compose 🐳 - Detected: ${mounts.length} mounts 📂 - Command: docker ${command} - Source Type: docker ${sourceType} ✅ - Compose Type: ${composeType} ✅`; - const logBox = boxen(logContent, { - padding: { - left: 1, - right: 1, - bottom: 1, - }, - width: 80, - borderStyle: "double", - }); - writeStream.write(`\n${logBox}\n`); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - - await spawnAsync( - "docker", - [...command.split(" ")], - (data) => { - if (writeStream.writable) { - writeStream.write(data.toString()); - } - }, - { - cwd: projectPath, - env: { - NODE_ENV: process.env.NODE_ENV, - PATH: process.env.PATH, - ...(composeType === "stack" && { - ...getEnviromentVariablesObject( - compose.env, - compose.environment.project.env, - ), - }), - }, - }, - ); - - if (compose.isolatedDeployment) { - await execAsync( - `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1`, - ).catch(() => {}); - } - - writeStream.write("Docker Compose Deployed: ✅"); - } catch (error) { - writeStream.write(`Error ❌ ${(error as Error).message}`); - throw error; - } finally { - writeStream.end(); - } -}; export const getBuildComposeCommand = async (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(!!compose.serverId); @@ -179,36 +97,6 @@ export const createCommand = (compose: ComposeNested) => { return command; }; -const createEnvFile = (compose: ComposeNested) => { - const { COMPOSE_PATH } = paths(); - const { env, composePath, appName } = compose; - const composeFilePath = - join(COMPOSE_PATH, appName, "code", composePath) || - join(COMPOSE_PATH, appName, "code", "docker-compose.yml"); - - const envFilePath = join(dirname(composeFilePath), ".env"); - let envContent = `APP_NAME=${appName}\n`; - envContent += env || ""; - if (!envContent.includes("DOCKER_CONFIG")) { - envContent += "\nDOCKER_CONFIG=/root/.docker"; - } - - if (compose.randomize) { - envContent += `\nCOMPOSE_PREFIX=${compose.suffix}`; - } - - const envFileContent = prepareEnvironmentVariables( - envContent, - compose.environment.project.env, - compose.environment.env, - ).join("\n"); - - if (!existsSync(dirname(envFilePath))) { - mkdirSync(dirname(envFilePath), { recursive: true }); - } - writeFileSync(envFilePath, envFileContent); -}; - export const getCreateEnvFileCommand = (compose: ComposeNested) => { const { COMPOSE_PATH } = paths(!!compose.serverId); const { env, composePath, appName } = compose; diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index e8c66e697..3d4167d2e 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -1,5 +1,4 @@ import fs, { existsSync, readFileSync } from "node:fs"; -import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; @@ -103,24 +102,6 @@ export const readComposeFile = async (compose: Compose) => { return null; }; -export const writeDomainsToCompose = async ( - compose: Compose, - domains: Domain[], -) => { - if (!domains.length) { - return; - } - const composeConverted = await addDomainToCompose(compose, domains); - - const path = getComposePath(compose); - const composeString = stringify(composeConverted, { lineWidth: 1000 }); - try { - await writeFile(path, composeString, "utf8"); - } catch (error) { - throw error; - } -}; - export const writeDomainsToComposeRemote = async ( compose: Compose, domains: Domain[], diff --git a/packages/server/src/utils/providers/raw.ts b/packages/server/src/utils/providers/raw.ts index 7b541867d..508df86ed 100644 --- a/packages/server/src/utils/providers/raw.ts +++ b/packages/server/src/utils/providers/raw.ts @@ -1,13 +1,10 @@ -import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; import { encodeBase64 } from "../docker/utils"; -import { recreateDirectory } from "../filesystem/directory"; -import { execAsyncRemote } from "../process/execAsync"; export const getCreateComposeFileCommand = (compose: Compose) => { - const { COMPOSE_PATH } = paths(true); + const { COMPOSE_PATH } = paths(!!compose.serverId); const { appName, composeFile } = compose; const outputPath = join(COMPOSE_PATH, appName, "code"); const filePath = join(outputPath, "docker-compose.yml"); @@ -20,35 +17,3 @@ export const getCreateComposeFileCommand = (compose: Compose) => { `; return bashCommand; }; - -export const createComposeFileRaw = async (compose: Compose) => { - const { COMPOSE_PATH } = paths(); - const { appName, composeFile } = compose; - const outputPath = join(COMPOSE_PATH, appName, "code"); - const filePath = join(outputPath, "docker-compose.yml"); - try { - await recreateDirectory(outputPath); - await writeFile(filePath, composeFile); - } catch (error) { - throw error; - } -}; - -export const createComposeFileRawRemote = async (compose: Compose) => { - const { COMPOSE_PATH } = paths(true); - const { appName, composeFile, serverId } = compose; - const outputPath = join(COMPOSE_PATH, appName, "code"); - const filePath = join(outputPath, "docker-compose.yml"); - - try { - const encodedContent = encodeBase64(composeFile); - const command = ` - rm -rf ${outputPath}; - mkdir -p ${outputPath}; - echo "${encodedContent}" | base64 -d > "${filePath}"; - `; - await execAsyncRemote(serverId, command); - } catch (error) { - throw error; - } -}; From d8ab7a59ffcebc267ed667a8772e1b05341d9a44 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:43:54 -0600 Subject: [PATCH 13/79] Refactor Bitbucket header utility: remove unused BitbucketClone type definition to streamline the code and enhance maintainability. --- packages/server/src/utils/providers/bitbucket.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/server/src/utils/providers/bitbucket.ts b/packages/server/src/utils/providers/bitbucket.ts index 217dba271..2248baaaf 100644 --- a/packages/server/src/utils/providers/bitbucket.ts +++ b/packages/server/src/utils/providers/bitbucket.ts @@ -76,11 +76,6 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => { }; }; -type BitbucketClone = (ApplicationWithBitbucket | ComposeWithBitbucket) & { - serverId: string | null; - type?: "application" | "compose"; -}; - interface CloneBitbucketRepository { appName: string; bitbucketRepository: string | null; From b814bdc612299643a392e59440e903c5e6e6f1ec Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 9 Nov 2025 11:13:39 -0600 Subject: [PATCH 14/79] Refactor application and compose deployment logic: remove unused buildApplication function, streamline command logging for deployment, and enhance static command generation for improved maintainability and clarity in the codebase. --- packages/server/src/services/application.ts | 3 - packages/server/src/services/compose.ts | 18 +++++- packages/server/src/utils/builders/index.ts | 45 +------------- packages/server/src/utils/builders/static.ts | 20 ++++++- packages/server/src/utils/cluster/upload.ts | 63 +------------------- packages/server/src/utils/docker/domain.ts | 3 +- 6 files changed, 38 insertions(+), 114 deletions(-) diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 181467a76..8dc67ddb6 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -7,7 +7,6 @@ import { } from "@dokploy/server/db/schema"; import { getAdvancedStats } from "@dokploy/server/monitoring/utils"; import { - buildApplication, getBuildCommand, mechanizeDockerContainer, } from "@dokploy/server/utils/builders"; @@ -187,8 +186,6 @@ export const deployApplication = async ({ command += await cloneBitbucketRepository(application); } else if (application.sourceType === "git") { command += await cloneGitRepository(application); - } else if (application.sourceType === "drop") { - await buildApplication(application, deployment.logPath); } else if (application.sourceType === "docker") { command += await buildRemoteDocker(application); } diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 4b96f443d..2e2a2fc59 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -232,8 +232,15 @@ export const deployCompose = async ({ command += getCreateComposeFileCommand(entity); } + let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } + command += await getBuildComposeCommand(entity); - const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (compose.serverId) { await execAsyncRemote(compose.serverId, commandWithLog); } else { @@ -293,8 +300,15 @@ export const rebuildCompose = async ({ if (compose.sourceType === "raw") { command += getCreateComposeFileCommand(compose); } + + let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, commandWithLog); + } else { + await execAsync(commandWithLog); + } command += await getBuildComposeCommand(compose); - const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; + commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (compose.serverId) { await execAsyncRemote(compose.serverId, commandWithLog); } else { diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 207238ca5..bfc3e894a 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -1,7 +1,6 @@ -import { createWriteStream } from "node:fs"; import type { InferResultType } from "@dokploy/server/types/with"; import type { CreateServiceOptions } from "dockerode"; -import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; +import { uploadImageRemoteCommand } from "../cluster/upload"; import { calculateResources, generateBindMounts, @@ -34,48 +33,6 @@ export type ApplicationNested = InferResultType< } >; -export const buildApplication = async ( - application: ApplicationNested, - logPath: string, -) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { buildType, sourceType } = application; - try { - writeStream.write( - `\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`, - ); - console.log(`Build ${buildType}: ✅`); - // if (buildType === "nixpacks") { - // await buildNixpacks(application, writeStream); - // } else if (buildType === "heroku_buildpacks") { - // await buildHeroku(application, writeStream); - // } else if (buildType === "paketo_buildpacks") { - // await buildPaketo(application, writeStream); - // } else if (buildType === "dockerfile") { - // await buildCustomDocker(application, writeStream); - // } else if (buildType === "static") { - // await buildStatic(application, writeStream); - // } else if (buildType === "railpack") { - // await buildRailpack(application, writeStream); - // } - - if (application.registryId) { - await uploadImage(application, writeStream); - } - await mechanizeDockerContainer(application); - writeStream.write("Docker Deployed: ✅"); - } catch (error) { - if (error instanceof Error) { - writeStream.write(`Error ❌\n${error?.message}`); - } else { - writeStream.write("Error ❌"); - } - throw error; - } finally { - writeStream.end(); - } -}; - export const getBuildCommand = (application: ApplicationNested) => { let command = ""; const { buildType, registry } = application; diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index 4ddb290be..99fa25285 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -29,16 +29,32 @@ http { `; export const getStaticCommand = (application: ApplicationNested) => { - const { publishDirectory } = application; + const { publishDirectory, isStaticSpa } = application; const buildAppDirectory = getBuildAppDirectory(application); + let command = ""; + if (isStaticSpa) { + command += getCreateFileCommand( + buildAppDirectory, + "nginx.conf", + nginxSpaConfig, + ); + } - let command = getCreateFileCommand( + command += getCreateFileCommand( + buildAppDirectory, + ".dockerignore", + [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + ); + + command += getCreateFileCommand( buildAppDirectory, "Dockerfile", [ "FROM nginx:alpine", "WORKDIR /usr/share/nginx/html/", + isStaticSpa ? "COPY nginx.conf /etc/nginx/nginx.conf" : "", `COPY ${publishDirectory || "."} .`, + 'CMD ["nginx", "-g", "daemon off;"]', ].join("\n"), ); diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index 6a2dfd66f..33be287e1 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -1,63 +1,4 @@ -import type { WriteStream } from "node:fs"; import type { ApplicationNested } from "../builders"; -import { spawnAsync } from "../process/spawnAsync"; - -export const uploadImage = async ( - application: ApplicationNested, - writeStream: WriteStream, -) => { - const registry = application.registry; - - if (!registry) { - throw new Error("Registry not found"); - } - - const { registryUrl, imagePrefix, username } = registry; - const { appName } = application; - const imageName = `${appName}:latest`; - - const finalURL = registryUrl; - - // Build registry tag in correct format: registry.com/owner/image:tag - // For ghcr.io: ghcr.io/username/image:tag - // For docker.io: docker.io/username/image:tag - const registryTag = imagePrefix - ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}` - : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`; - - try { - writeStream.write( - `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL} | ${registryTag}\n`, - ); - const loginCommand = spawnAsync( - "docker", - ["login", finalURL, "-u", registry.username, "--password-stdin"], - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - ); - loginCommand.child?.stdin?.write(registry.password); - loginCommand.child?.stdin?.end(); - await loginCommand; - - await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - - await spawnAsync("docker", ["push", registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - } catch (error) { - console.log(error); - throw error; - } -}; export const uploadImageRemoteCommand = (application: ApplicationNested) => { const registry = application.registry; @@ -74,8 +15,8 @@ export const uploadImageRemoteCommand = (application: ApplicationNested) => { // Build registry tag in correct format: registry.com/owner/image:tag const registryTag = imagePrefix - ? `${registryUrl}/${imagePrefix}/${imageName}` - : `${registryUrl}/${username}/${imageName}`; + ? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}` + : `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`; try { const command = ` diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 3d4167d2e..ffe900302 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -132,7 +132,6 @@ exit 1; `; } }; -// (node:59875) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 SIGTERM listeners added to [process]. Use emitter.setMaxListeners() to increase limit export const addDomainToCompose = async ( compose: Compose, domains: Domain[], @@ -142,7 +141,7 @@ export const addDomainToCompose = async ( let result: ComposeSpecification | null; if (compose.serverId) { - result = await loadDockerComposeRemote(compose); // aca hay que ir al servidor e ir a traer el compose file al servidor + result = await loadDockerComposeRemote(compose); } else { result = await loadDockerCompose(compose); } From c56ddf3ec1dae72cca60408ff9ad50db2fb27f55 Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Mon, 10 Nov 2025 11:12:00 +0100 Subject: [PATCH 15/79] fix: pr template checkboxes without a space they do not render as checkboxes on github --- .github/pull_request_template.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0b849afc0..d45c3dac0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,9 +6,9 @@ Please describe in a short paragraph what this PR is about. Before submitting this PR, please make sure that: -- [] You created a dedicated branch based on the `canary` branch. -- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request -- [] You have tested this PR in your local instance. +- [ ] You created a dedicated branch based on the `canary` branch. +- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request +- [ ] You have tested this PR in your local instance. ## Issues related (if applicable) From b202974a7d78a9f3152ffc17cdf6681b52584b3d Mon Sep 17 00:00:00 2001 From: HarikrishnanD Date: Tue, 11 Nov 2025 11:34:10 +0530 Subject: [PATCH 16/79] fix: add protocol prefix to invitation links --- apps/dokploy/server/api/routers/user.ts | 4 ++-- packages/server/src/services/admin.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index d30b99b3a..baec4d6ac 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -4,6 +4,7 @@ import { findNotificationById, findOrganizationById, findUserById, + getDokployUrl, getUserByToken, IS_CLOUD, removeUserById, @@ -419,11 +420,10 @@ export const userRouter = createTRPCRouter({ }); } - const admin = await findAdmin(); const host = process.env.NODE_ENV === "development" ? "http://localhost:3000" - : admin.user.host; + : await getDokployUrl(); const inviteLink = `${host}/invitation?token=${input.invitationId}`; const organization = await findOrganizationById( diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index 55ee5caee..0cbb20785 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -110,7 +110,8 @@ export const getDokployUrl = async () => { const admin = await findAdmin(); if (admin.user.host) { - return `https://${admin.user.host}`; + const protocol = admin.user.https ? "https" : "http"; + return `${protocol}://${admin.user.host}`; } return `http://${admin.user.serverIp}:${process.env.PORT}`; }; From 843313ddb9cbe6fee109eeab7f737a001400fe37 Mon Sep 17 00:00:00 2001 From: HarikrishnanD Date: Tue, 11 Nov 2025 13:10:47 +0530 Subject: [PATCH 17/79] feat: add expandable commit messages for deployment logs --- .../deployments/show-deployments.tsx | 106 ++++++++++++++++-- 1 file changed, 97 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 1045856c2..5848a046f 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -1,4 +1,4 @@ -import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; +import { ChevronDown, ChevronUp, Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; @@ -80,6 +80,52 @@ export const ShowDeployments = ({ } = api.compose.cancelDeployment.useMutation(); const [url, setUrl] = React.useState(""); + const [expandedDescriptions, setExpandedDescriptions] = useState>( + new Set(), + ); + + // Maximum character length before truncating commit messages + const MAX_DESCRIPTION_LENGTH = 150; + + // Helper function to truncate description intelligently + const truncateDescription = (description: string, maxLength: number): string => { + if (maxLength <= 0) { + return description; // Don't truncate if maxLength is 0 or negative + } + if (description.length <= maxLength) { + return description; + } + // Try to truncate at a word boundary if possible + const truncated = description.slice(0, maxLength); + const lastSpace = truncated.lastIndexOf(" "); + // If we find a space near the end (within last 20 chars), use it for cleaner truncation + if (lastSpace > maxLength - 20 && lastSpace > 0) { + return `${truncated.slice(0, lastSpace)}...`; + } + return `${truncated}...`; + }; + + // Check if description should be truncated + const shouldTruncate = (description: string): boolean => { + // Only truncate if MAX_DESCRIPTION_LENGTH is greater than 0 + if (MAX_DESCRIPTION_LENGTH <= 0) { + return false; + } + return description.length > MAX_DESCRIPTION_LENGTH; + }; + + // Toggle expand/collapse state for a specific deployment + const toggleDescription = (deploymentId: string) => { + setExpandedDescriptions((prev) => { + const next = new Set(prev); + if (next.has(deploymentId)) { + next.delete(deploymentId); + } else { + next.add(deploymentId); + } + return next; + }); + }; // Check for stuck deployment (more than 9 minutes) - only for the most recent deployment const stuckDeployment = useMemo(() => { @@ -230,14 +276,56 @@ export const ShowDeployments = ({ className="size-2.5" /> - - {deployment.title} - - {deployment.description && ( - - {deployment.description} - - )} + {(() => { + // The commit message is in the title field, so we truncate that + const titleText = deployment.title.trim(); + const needsTruncation = shouldTruncate(titleText); + const isExpanded = expandedDescriptions.has(deployment.deploymentId); + + return ( +
+ {/* Commit message (from title) - truncated */} + + {isExpanded || !needsTruncation + ? titleText + : truncateDescription( + titleText, + MAX_DESCRIPTION_LENGTH, + )} + + {needsTruncation && ( + + )} + {/* Hash (from description) - shown in compact form */} + {deployment.description && deployment.description.trim() && ( + + {deployment.description} + + )} +
+ ); + })()}
From 70bb32c59073f0756fce52b17c7eda1c6d2fb5e0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 07:42:12 +0000 Subject: [PATCH 18/79] [autofix.ci] apply automated fixes --- .../deployments/show-deployments.tsx | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 5848a046f..a5a8c5fd6 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -1,4 +1,12 @@ -import { ChevronDown, ChevronUp, Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; +import { + ChevronDown, + ChevronUp, + Clock, + Loader2, + RefreshCcw, + RocketIcon, + Settings, +} from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; @@ -88,7 +96,10 @@ export const ShowDeployments = ({ const MAX_DESCRIPTION_LENGTH = 150; // Helper function to truncate description intelligently - const truncateDescription = (description: string, maxLength: number): string => { + const truncateDescription = ( + description: string, + maxLength: number, + ): string => { if (maxLength <= 0) { return description; // Don't truncate if maxLength is 0 or negative } @@ -280,8 +291,10 @@ export const ShowDeployments = ({ // The commit message is in the title field, so we truncate that const titleText = deployment.title.trim(); const needsTruncation = shouldTruncate(titleText); - const isExpanded = expandedDescriptions.has(deployment.deploymentId); - + const isExpanded = expandedDescriptions.has( + deployment.deploymentId, + ); + return (
{/* Commit message (from title) - truncated */} @@ -296,7 +309,9 @@ export const ShowDeployments = ({ {needsTruncation && ( )} {/* Hash (from description) - shown in compact form */} - {deployment.description && deployment.description.trim() && ( - - {deployment.description} - - )} + {deployment.description && + deployment.description.trim() && ( + + {deployment.description} + + )}
); })()} From d22aa0583c330ead589b4d9ac7415f4f52ca6c7b Mon Sep 17 00:00:00 2001 From: Bima42 Date: Thu, 13 Nov 2025 16:17:21 +0100 Subject: [PATCH 19/79] chore: bump traefik to 3.6.1 --- apps/dokploy/setup.ts | 2 +- packages/server/src/setup/traefik-setup.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/setup.ts b/apps/dokploy/setup.ts index 55e1da87c..e0ccb86d8 100644 --- a/apps/dokploy/setup.ts +++ b/apps/dokploy/setup.ts @@ -22,7 +22,7 @@ import { await initializeNetwork(); createDefaultTraefikConfig(); createDefaultServerTraefikConfig(); - await execAsync("docker pull traefik:v3.5.0"); + await execAsync("docker pull traefik:v3.6.1"); await initializeStandaloneTraefik(); await initializeRedis(); await initializePostgres(); diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index fa9bf78d0..73cff0b1c 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -20,7 +20,7 @@ export const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80; export const TRAEFIK_HTTP3_PORT = Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; -export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.5.0"; +export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.1"; export interface TraefikOptions { env?: string[]; From d549aa6a623de978e0f14cf3b45da80e36a09dd8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 13 Nov 2025 22:35:16 -0600 Subject: [PATCH 20/79] feat: add last deployment date to services and update sorting logic - Introduced `lastDeployDate` property to track the most recent deployment for applications and compose services. - Updated the `extractServicesFromEnvironment` function to calculate and include the last deployment date. - Modified sorting logic to allow sorting by last deployment date, enhancing the user experience on the environment dashboard. - Adjusted local storage default sort preference to prioritize last deployment date. --- .../environment/[environmentId].tsx | 121 ++++++++++++++---- packages/server/src/services/environment.ts | 12 +- 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index c09111d20..886756ab2 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -115,6 +115,7 @@ export type Services = { id: string; createdAt: string; status?: "idle" | "running" | "done" | "error"; + lastDeployDate?: Date | null; }; type Project = Awaited>; @@ -128,16 +129,34 @@ export const extractServicesFromEnvironment = ( const allServices: Services[] = []; const applications: Services[] = - environment.applications?.map((item) => ({ - appName: item.appName, - name: item.name, - type: "application", - id: item.applicationId, - createdAt: item.createdAt, - status: item.applicationStatus, - description: item.description, - serverId: item.serverId, - })) || []; + environment.applications?.map((item) => { + // Get the most recent deployment date + let lastDeployDate: Date | null = null; + const deployments = (item as any).deployments; + if (deployments && deployments.length > 0) { + for (const deployment of deployments) { + const deployDate = new Date( + deployment.finishedAt || + deployment.startedAt || + deployment.createdAt, + ); + if (!lastDeployDate || deployDate > lastDeployDate) { + lastDeployDate = deployDate; + } + } + } + return { + appName: item.appName, + name: item.name, + type: "application", + id: item.applicationId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + lastDeployDate, + }; + }) || []; const mariadb: Services[] = environment.mariadb?.map((item) => ({ @@ -200,16 +219,34 @@ export const extractServicesFromEnvironment = ( })) || []; const compose: Services[] = - environment.compose?.map((item) => ({ - appName: item.appName, - name: item.name, - type: "compose", - id: item.composeId, - createdAt: item.createdAt, - status: item.composeStatus, - description: item.description, - serverId: item.serverId, - })) || []; + environment.compose?.map((item) => { + // Get the most recent deployment date + let lastDeployDate: Date | null = null; + const deployments = (item as any).deployments; + if (deployments && deployments.length > 0) { + for (const deployment of deployments) { + const deployDate = new Date( + deployment.finishedAt || + deployment.startedAt || + deployment.createdAt, + ); + if (!lastDeployDate || deployDate > lastDeployDate) { + lastDeployDate = deployDate; + } + } + } + return { + appName: item.appName, + name: item.name, + type: "compose", + id: item.composeId, + createdAt: item.createdAt, + status: item.composeStatus, + description: item.description, + serverId: item.serverId, + lastDeployDate, + }; + }) || []; allServices.push( ...applications, @@ -237,9 +274,9 @@ const EnvironmentPage = ( const { data: auth } = api.user.get.useQuery(); const [sortBy, setSortBy] = useState(() => { if (typeof window !== "undefined") { - return localStorage.getItem("servicesSort") || "createdAt-desc"; + return localStorage.getItem("servicesSort") || "lastDeploy-desc"; } - return "createdAt-desc"; + return "lastDeploy-desc"; }); useEffect(() => { @@ -261,10 +298,45 @@ const EnvironmentPage = ( comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); break; + case "lastDeploy": { + const aLastDeploy = a.lastDeployDate; + const bLastDeploy = b.lastDeployDate; + + if (direction === "desc") { + // For "desc" (newest first): services with deployments first, then those without + if (!aLastDeploy && !bLastDeploy) { + comparison = 0; + } else if (!aLastDeploy) { + comparison = 1; // a (no deploy) goes after b (has deploy) + } else if (!bLastDeploy) { + comparison = -1; // a (has deploy) goes before b (no deploy) + } else { + // Both have deployments: newest first (negative if a is newer) + comparison = bLastDeploy.getTime() - aLastDeploy.getTime(); + } + } else { + // For "asc" (oldest first): services with deployments first, then those without + if (!aLastDeploy && !bLastDeploy) { + comparison = 0; + } else if (!aLastDeploy) { + comparison = 1; // a (no deploy) goes after b (has deploy) + } else if (!bLastDeploy) { + comparison = -1; // a (has deploy) goes before b (no deploy) + } else { + // Both have deployments: oldest first + comparison = aLastDeploy.getTime() - bLastDeploy.getTime(); + } + } + break; + } default: comparison = 0; } - return direction === "asc" ? comparison : -comparison; + // For other fields, apply direction normally + if (field !== "lastDeploy") { + return direction === "asc" ? comparison : -comparison; + } + return comparison; }); }; @@ -1217,6 +1289,9 @@ const EnvironmentPage = ( + + Recently deployed + Newest first diff --git a/packages/server/src/services/environment.ts b/packages/server/src/services/environment.ts index 1d77510be..c35862714 100644 --- a/packages/server/src/services/environment.ts +++ b/packages/server/src/services/environment.ts @@ -34,13 +34,21 @@ export const findEnvironmentById = async (environmentId: string) => { const environment = await db.query.environments.findFirst({ where: eq(environments.environmentId, environmentId), with: { - applications: true, + applications: { + with: { + deployments: true, + }, + }, mariadb: true, mongo: true, mysql: true, postgres: true, redis: true, - compose: true, + compose: { + with: { + deployments: true, + }, + }, project: true, }, }); From c35fe0d457790093c96baecac99ab73f438a2f87 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 01:10:49 -0600 Subject: [PATCH 21/79] feat: enhance Docker image handling in deployment logic - Added functions to extract image name and tag from Docker images and webhook requests. - Implemented validation for Docker image names and tags during deployment. - Expanded test coverage for image tag extraction and commit message generation for GitHub Packages events. - Improved error handling for missing image names and tags in deployment requests. --- apps/dokploy/__test__/deploy/github.test.ts | 312 +++++++++++++++++- .../pages/api/deploy/[refreshToken].ts | 212 +++++++++--- 2 files changed, 483 insertions(+), 41 deletions(-) diff --git a/apps/dokploy/__test__/deploy/github.test.ts b/apps/dokploy/__test__/deploy/github.test.ts index 03805b08d..46be44883 100644 --- a/apps/dokploy/__test__/deploy/github.test.ts +++ b/apps/dokploy/__test__/deploy/github.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]"; +import { + extractCommitMessage, + extractImageName, + extractImageTag, + extractImageTagFromRequest, +} from "@/pages/api/deploy/[refreshToken]"; describe("GitHub Webhook Skip CI", () => { const mockGithubHeaders = { @@ -96,3 +101,308 @@ describe("GitHub Webhook Skip CI", () => { ); }); }); + +describe("GitHub Packages Docker Image Tag Extraction", () => { + it("should extract tag from container_metadata", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "v1.0.0", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:v1.0.0", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("v1.0.0"); + }); + + it("should extract tag from package_url when container_metadata tag matches version", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "sha256:abc123...", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("latest"); + }); + + it("should extract tag from package_url when container_metadata is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: "ghcr.io/owner/repo:1.2.3", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("1.2.3"); + }); + + it("should handle different tag formats in package_url", () => { + const headers = { "x-github-event": "registry_package" }; + const testCases = [ + { url: "ghcr.io/owner/repo:latest", expected: "latest" }, + { url: "ghcr.io/owner/repo:v1.0.0", expected: "v1.0.0" }, + { url: "ghcr.io/owner/repo:1.2.3", expected: "1.2.3" }, + { url: "ghcr.io/owner/repo:dev", expected: "dev" }, + ]; + + for (const testCase of testCases) { + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: testCase.url, + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe(testCase.expected); + } + }); + + it("should return null for non-registry_package events", () => { + const headers = { "x-github-event": "push" }; + const body = { + registry_package: { + package_version: { + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when package_version is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: {}, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when package_url has no tag", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: "ghcr.io/owner/repo", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when package_url ends with colon (no tag)", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + package_url: "ghcr.io/owner/repo:", + container_metadata: { + tag: { + name: "", + digest: "sha256:abc123...", + }, + }, + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should return null when tag name is empty string", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBeNull(); + }); + + it("should ignore tag if it matches the version (digest)", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + container_metadata: { + tag: { + name: "sha256:abc123...", + digest: "sha256:abc123...", + }, + }, + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const tag = extractImageTagFromRequest(headers, body); + expect(tag).toBe("latest"); + }); + + it("should handle registry_package commit message with package_url", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + package_url: "ghcr.io/owner/repo:latest", + }, + }, + }; + + const message = extractCommitMessage(headers, body); + expect(message).toBe("Docker GHCR image pushed: ghcr.io/owner/repo:latest"); + }); + + it("should handle registry_package commit message when package_url is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: { + package_version: { + version: "sha256:abc123...", + }, + }, + }; + + const message = extractCommitMessage(headers, body); + expect(message).toBe("Docker GHCR image pushed"); + }); + + it("should handle registry_package commit message when package_version is missing", () => { + const headers = { "x-github-event": "registry_package" }; + const body = { + registry_package: {}, + }; + + const message = extractCommitMessage(headers, body); + expect(message).toBe("NEW COMMIT"); + }); +}); + +describe("Docker Image Name and Tag Extraction", () => { + describe("extractImageName", () => { + it("should return image name without tag", () => { + expect(extractImageName("my-image:latest")).toBe("my-image"); + expect(extractImageName("my-image:1.0.0")).toBe("my-image"); + expect(extractImageName("ghcr.io/owner/repo:latest")).toBe( + "ghcr.io/owner/repo", + ); + }); + + it("should return full image name when no tag is present", () => { + expect(extractImageName("my-image")).toBe("my-image"); + expect(extractImageName("ghcr.io/owner/repo")).toBe("ghcr.io/owner/repo"); + }); + + it("should handle images with port numbers correctly", () => { + expect(extractImageName("registry:5000/image:tag")).toBe( + "registry:5000/image", + ); + expect(extractImageName("localhost:5000/my-app:latest")).toBe( + "localhost:5000/my-app", + ); + }); + + it("should handle complex image paths", () => { + expect( + extractImageName("myregistryhost:5000/fedora/httpd:version1.0"), + ).toBe("myregistryhost:5000/fedora/httpd"); + expect(extractImageName("registry.example.com:8080/ns/app:v1.2.3")).toBe( + "registry.example.com:8080/ns/app", + ); + }); + + it("should return null for invalid inputs", () => { + expect(extractImageName(null)).toBeNull(); + expect(extractImageName("")).toBeNull(); + }); + + it("should handle edge cases with multiple colons", () => { + expect(extractImageName("image:tag:extra")).toBe("image:tag"); + expect(extractImageName("registry:5000:invalid")).toBe("registry:5000"); + }); + }); + + describe("extractImageTag", () => { + it("should extract tag from image with tag", () => { + expect(extractImageTag("my-image:latest")).toBe("latest"); + expect(extractImageTag("my-image:1.0.0")).toBe("1.0.0"); + expect(extractImageTag("ghcr.io/owner/repo:v1.2.3")).toBe("v1.2.3"); + }); + + it("should return 'latest' when no tag is present", () => { + expect(extractImageTag("my-image")).toBe("latest"); + expect(extractImageTag("ghcr.io/owner/repo")).toBe("latest"); + }); + + it("should handle complex image paths with tags", () => { + expect( + extractImageTag("myregistryhost:5000/fedora/httpd:version1.0"), + ).toBe("version1.0"); + expect(extractImageTag("registry.example.com:8080/ns/app:v1.2.3")).toBe( + "v1.2.3", + ); + }); + + it("should return null for invalid inputs", () => { + expect(extractImageTag(null)).toBeNull(); + expect(extractImageTag("")).toBeNull(); + }); + + it("should handle edge cases with multiple colons", () => { + expect(extractImageTag("image:tag:extra")).toBe("extra"); + expect(extractImageTag("registry:5000/image:tag")).toBe("tag"); + }); + + it("should handle numeric tags", () => { + expect(extractImageTag("my-image:123")).toBe("123"); + expect(extractImageTag("my-image:1")).toBe("1"); + }); + }); +}); diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts index 797f13802..1441d9776 100644 --- a/apps/dokploy/pages/api/deploy/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts @@ -12,6 +12,17 @@ import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; +/** + * Helper function to get package_version from registry_package events + */ +const getPackageVersion = (headers: any, body: any) => { + const event = headers["x-github-event"]; + if (event === "registry_package") { + return body.registry_package?.package_version; + } + return null; +}; + export default async function handler( req: NextApiRequest, res: NextApiResponse, @@ -46,28 +57,66 @@ export default async function handler( } const deploymentTitle = extractCommitMessage(req.headers, req.body); - const deploymentHash = extractHash(req.headers, req.body); + const deploymentHash = extractHash(req.headers, req.body); const sourceType = application.sourceType; if (sourceType === "docker") { + const applicationImageName = extractImageName(application.dockerImage); const applicationDockerTag = extractImageTag(application.dockerImage); - const webhookDockerTags = extractImageTagFromRequest( + + const webhookImageName = extractImageNameFromRequest( + req.headers, + req.body, + ); + const webhookDockerTag = extractImageTagFromRequest( req.headers, req.body, ); - const isMismatch = - applicationDockerTag && - webhookDockerTags && - webhookDockerTags.length > 0 && - !webhookDockerTags.includes(applicationDockerTag); - if (isMismatch) { + if (!applicationImageName) { res.status(301).json({ - message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag(s) (${webhookDockerTags.join(", ")}).`, + message: "Application Docker Image Name Not Found", }); return; } + + if (!webhookImageName) { + res.status(301).json({ + message: "Webhook Docker Image Name Not Found", + }); + return; + } + + // Validate image name matches + if (webhookImageName !== applicationImageName) { + res.status(301).json({ + message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`, + }); + return; + } + + if (!applicationDockerTag) { + res.status(301).json({ + message: "Application Docker Tag Not Found", + }); + return; + } + + if (!webhookDockerTag) { + res.status(301).json({ + message: "Webhook Docker Tag Not Found", + }); + return; + } + + if (webhookDockerTag !== applicationDockerTag) { + res.status(301).json({ + message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`, + }); + return; + } + console.log("[END] Docker Deploy Validation"); } else if (sourceType === "github") { const normalizedCommits = req.body?.commits?.flatMap( (commit: any) => commit.modified, @@ -224,6 +273,39 @@ export default async function handler( } } +/** + * Return the image name without the tag + * Example: "my-image" => "my-image" + * Example: "my-image:latest" => "my-image" + * Example: "my-image:1.0.0" => "my-image" + * Example: "myregistryhost:5000/fedora/httpd:version1.0" => "myregistryhost:5000/fedora/httpd" + * @link https://docs.docker.com/reference/cli/docker/image/tag/ + */ +export function extractImageName(dockerImage: string | null): string | null { + if (!dockerImage || typeof dockerImage !== "string") { + return null; + } + + // Handle case where there's no tag (no colon or colon is part of port number) + const lastColonIndex = dockerImage.lastIndexOf(":"); + if (lastColonIndex === -1) { + return dockerImage; + } + + // Check if the part after the last colon looks like a tag (not a port number) + // Port numbers are typically 1-5 digits, tags are usually longer or contain letters + const afterColon = dockerImage.substring(lastColonIndex + 1); + const isPortNumber = /^\d{1,5}$/.test(afterColon); + + // If it's a port number (like registry:5000/image), don't split + if (isPortNumber) { + return dockerImage; + } + + // Otherwise, split at the last colon to get image name + return dockerImage.substring(0, lastColonIndex); +} + /** * Return the last part of the image name, which is the tag * Example: "my-image" => null @@ -232,7 +314,7 @@ export default async function handler( * Example: "myregistryhost:5000/fedora/httpd:version1.0" => "version1.0" * @link https://docs.docker.com/reference/cli/docker/image/tag/ */ -function extractImageTag(dockerImage: string | null) { +export function extractImageTag(dockerImage: string | null) { if (!dockerImage || typeof dockerImage !== "string") { return null; } @@ -242,49 +324,99 @@ function extractImageTag(dockerImage: string | null) { } /** + * Extract the image name (without tag) from webhook request * @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload + * @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package + */ +export const extractImageNameFromRequest = ( + headers: any, + body: any, +): string | null => { + // GitHub Packages: registry_package events (container registry) + const packageVersion = getPackageVersion(headers, body); + if (packageVersion?.package_url) { + const packageUrl = packageVersion.package_url; + // Remove tag if present (everything after the last colon) + if (packageUrl.includes(":")) { + const lastColonIndex = packageUrl.lastIndexOf(":"); + // Check if it's a port number (like registry:5000/image) + const afterColon = packageUrl.substring(lastColonIndex + 1); + const isPortNumber = /^\d{1,5}$/.test(afterColon); + if (isPortNumber) { + return packageUrl; + } + return packageUrl.substring(0, lastColonIndex); + } + return packageUrl; + } + + // Docker Hub + if (headers["user-agent"]?.includes("Go-http-client")) { + if (body.repository) { + const repoName = body.repository.repo_name; + return `${repoName}`; + } + } + return null; +}; + +/** + * @link https://docs.docker.com/docker-hub/webhooks/#example-webhook-payload + * @link https://docs.github.com/en/webhooks/webhook-events-and-payloads#registry_package */ export const extractImageTagFromRequest = ( headers: any, body: any, -): string[] | null => { - if (headers["user-agent"]?.includes("Go-http-client")) { - if (body.push_data && body.repository) { - return [body.push_data.tag] as string[]; +): string | null => { + // GitHub Packages: registry_package events (container registry) + const packageVersion = getPackageVersion(headers, body); + if (packageVersion) { + // Try to get tag from container_metadata first (most reliable) + // Only use it if it's not empty and not the same as the version (digest) + const tagName = packageVersion.container_metadata?.tag?.name?.trim() || ""; + if ( + tagName && + tagName !== packageVersion.version && + !tagName.startsWith("sha256:") + ) { + return tagName; + } + // Fallback: extract tag from package_url (e.g., "ghcr.io/owner/repo:tag") + if (packageVersion.package_url) { + const packageUrl = packageVersion.package_url; + // Handle case where package_url ends with colon (no tag) + if (packageUrl.endsWith(":")) { + return null; + } + const tagMatch = packageUrl.match(/:([^:]+)$/); + if (tagMatch?.[1]?.trim()) { + return tagMatch[1].trim(); + } } } - // GitHub Packages: package or registry_package events (container tags) - // See: https://docs.github.com/en/webhooks/webhook-events-and-payloads#package - const githubEvent = headers["x-github-event"]; - if (githubEvent === "package" || githubEvent === "registry_package") { - const pkg = body?.package ?? body?.registry_package?.package ?? null; - const packageVersion = - body?.package_version ?? body?.registry_package?.package_version ?? null; - const packageType = pkg?.package_type; - - if (packageType === "container" && packageVersion) { - const tags = - packageVersion?.metadata?.container?.tags ?? - packageVersion?.container?.tags ?? - null; - if (Array.isArray(tags) && tags.length > 0) { - return tags as string[]; - } - const singleTag = - packageVersion?.metadata?.container?.tag ?? - packageVersion?.metadata?.tag ?? - packageVersion?.tag ?? - null; - if (typeof singleTag === "string") { - return [singleTag] as string[]; - } + // Docker Hub + if (headers["user-agent"]?.includes("Go-http-client")) { + if (body.push_data && body.repository) { + return body.push_data.tag; } } return null; }; export const extractCommitMessage = (headers: any, body: any) => { + // GitHub Packages: registry_package events (container tags) + const githubEvent = headers["x-github-event"]; + if (githubEvent === "registry_package") { + const packageVersion = getPackageVersion(headers, body); + if (packageVersion) { + if (packageVersion.package_url) { + return `Docker GHCR image pushed: ${packageVersion.package_url}`; + } + return "Docker GHCR image pushed"; + } + // If package_version is missing, fall through to default behavior + } // GitHub if (headers["x-github-event"]) { return body.head_commit ? body.head_commit.message : "NEW COMMIT"; @@ -313,7 +445,7 @@ export const extractCommitMessage = (headers: any, body: any) => { if (headers["user-agent"]?.includes("Go-http-client")) { if (body.push_data && body.repository) { - return `Docker image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`; + return `DockerHub image pushed: ${body.repository.repo_name}:${body.push_data.tag} by ${body.push_data.pusher}`; } } From fbb1f1f266f222db2081f4eff578935714ede9af Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 01:11:52 -0600 Subject: [PATCH 22/79] fix: remove unnecessary log statement in Docker deploy validation --- apps/dokploy/pages/api/deploy/[refreshToken].ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts index 1441d9776..4d4258cb6 100644 --- a/apps/dokploy/pages/api/deploy/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts @@ -116,7 +116,6 @@ export default async function handler( }); return; } - console.log("[END] Docker Deploy Validation"); } else if (sourceType === "github") { const normalizedCommits = req.body?.commits?.flatMap( (commit: any) => commit.modified, From a9b9dd4b66710c6df9f08a647527c6044a7de4d4 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 01:14:35 -0600 Subject: [PATCH 23/79] fix: conditionally include deployment hash in job data logging --- apps/dokploy/pages/api/deploy/[refreshToken].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/pages/api/deploy/[refreshToken].ts b/apps/dokploy/pages/api/deploy/[refreshToken].ts index 4d4258cb6..2ab607736 100644 --- a/apps/dokploy/pages/api/deploy/[refreshToken].ts +++ b/apps/dokploy/pages/api/deploy/[refreshToken].ts @@ -241,7 +241,7 @@ export default async function handler( const jobData: DeploymentJob = { applicationId: application.applicationId as string, titleLog: deploymentTitle, - descriptionLog: `Hash: ${deploymentHash}`, + ...(deploymentHash && { descriptionLog: `Hash: ${deploymentHash}` }), type: "deploy", applicationType: "application", server: !!application.serverId, From 4d36741e50d9c6e52019ee4881e6f6b36ab2210a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 01:33:07 -0600 Subject: [PATCH 24/79] refactor: streamline service extraction logic in add-permissions component - Updated type definitions for Environment and Project to improve clarity and maintainability. - Refactored the extractServices function to use optional chaining and nullish coalescing for safer data handling. - Enhanced type safety by casting the mapped services to the Services type. --- .../settings/users/add-permissions.tsx | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index fb4d01547..7c6ef8b84 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -1,4 +1,3 @@ -import type { findEnvironmentById } from "@dokploy/server/index"; import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; @@ -27,12 +26,10 @@ import { FormMessage, } from "@/components/ui/form"; import { Switch } from "@/components/ui/switch"; -import { api } from "@/utils/api"; +import { api, type RouterOutputs } from "@/utils/api"; -type Environment = Omit< - Awaited>, - "project" ->; +type Project = RouterOutputs["project"]["all"][number]; +type Environment = Project["environments"][number]; export type Services = { appName: string; @@ -53,17 +50,16 @@ export type Services = { }; export const extractServices = (data: Environment | undefined) => { - const applications: Services[] = - data?.applications.map((item) => ({ - appName: item.appName, - name: item.name, - type: "application", - id: item.applicationId, - createdAt: item.createdAt, - status: item.applicationStatus, - description: item.description, - serverId: item.serverId, - })) || []; + const applications: Services[] = (data?.applications?.map((item) => ({ + appName: item.appName, + name: item.name, + type: "application", + id: item.applicationId, + createdAt: item.createdAt, + status: item.applicationStatus, + description: item.description, + serverId: item.serverId, + })) ?? []) as Services[]; const mariadb: Services[] = data?.mariadb.map((item) => ({ @@ -125,17 +121,16 @@ export const extractServices = (data: Environment | undefined) => { serverId: item.serverId, })) || []; - const compose: Services[] = - data?.compose.map((item) => ({ - appName: item.appName, - name: item.name, - type: "compose", - id: item.composeId, - createdAt: item.createdAt, - status: item.composeStatus, - description: item.description, - serverId: item.serverId, - })) || []; + const compose: Services[] = (data?.compose?.map((item) => ({ + appName: item.appName, + name: item.name, + type: "compose", + id: item.composeId, + createdAt: item.createdAt, + status: item.composeStatus, + description: item.description, + serverId: item.serverId, + })) ?? []) as Services[]; applications.push( ...mysql, From 61d9ae397adb0a2704626322f3b9ede40f717ded Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 22:27:38 -0600 Subject: [PATCH 25/79] feat: add git commit info extraction to deployment logic - Integrated `getGitCommitInfo` function to retrieve the latest commit message and hash for applications and compose services. - Updated deployment logic to conditionally include commit information in deployment updates, enhancing traceability. - Refactored import statements for better organization and clarity. --- packages/server/src/services/application.ts | 18 ++++++++- packages/server/src/services/compose.ts | 25 +++++++++++- packages/server/src/utils/docker/domain.ts | 2 + packages/server/src/utils/providers/git.ts | 42 +++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 8dc67ddb6..a3eb959b4 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -18,7 +18,10 @@ import { } from "@dokploy/server/utils/process/execAsync"; import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; import { buildRemoteDocker } from "@dokploy/server/utils/providers/docker"; -import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; +import { + cloneGitRepository, + getGitCommitInfo, +} from "@dokploy/server/utils/providers/git"; import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; @@ -29,6 +32,7 @@ import { getDokployUrl } from "./admin"; import { createDeployment, createDeploymentPreview, + updateDeployment, updateDeploymentStatus, } from "./deployment"; import { type Domain, getDomainHost } from "./domain"; @@ -243,6 +247,18 @@ export const deployApplication = async ({ }); throw error; + } finally { + // Only extract commit info for non-docker sources + if (application.sourceType !== "docker") { + const commitInfo = await getGitCommitInfo(application); + + if (commitInfo) { + await updateDeployment(deployment.deploymentId, { + title: commitInfo.message, + description: `Commit: ${commitInfo.hash}`, + }); + } + } } return true; }; diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 2e2a2fc59..519a0c404 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -22,7 +22,10 @@ import { execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { cloneBitbucketRepository } from "@dokploy/server/utils/providers/bitbucket"; -import { cloneGitRepository } from "@dokploy/server/utils/providers/git"; +import { + cloneGitRepository, + getGitCommitInfo, +} from "@dokploy/server/utils/providers/git"; import { cloneGiteaRepository } from "@dokploy/server/utils/providers/gitea"; import { cloneGithubRepository } from "@dokploy/server/utils/providers/github"; import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; @@ -30,7 +33,11 @@ import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { getDokployUrl } from "./admin"; -import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; +import { + createDeploymentCompose, + updateDeployment, + updateDeploymentStatus, +} from "./deployment"; import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; @@ -239,6 +246,7 @@ export const deployCompose = async ({ await execAsync(commandWithLog); } + command = "set -e;"; command += await getBuildComposeCommand(entity); commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`; if (compose.serverId) { @@ -275,6 +283,19 @@ export const deployCompose = async ({ organizationId: compose.environment.project.organizationId, }); throw error; + } finally { + if (compose.sourceType !== "raw") { + const commitInfo = await getGitCommitInfo({ + ...compose, + type: "compose", + }); + if (commitInfo) { + await updateDeployment(deployment.deploymentId, { + title: commitInfo.message, + description: `Commit: ${commitInfo.hash}`, + }); + } + } } }; diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index ffe900302..a176a4560 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -131,6 +131,8 @@ exit 1; exit 1; `; } + + return ""; }; export const addDomainToCompose = async ( compose: Compose, diff --git a/packages/server/src/utils/providers/git.ts b/packages/server/src/utils/providers/git.ts index 19c8ab8a0..8e640892d 100644 --- a/packages/server/src/utils/providers/git.ts +++ b/packages/server/src/utils/providers/git.ts @@ -4,6 +4,7 @@ import { findSSHKeyById, updateSSHKeyById, } from "@dokploy/server/services/ssh-key"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; interface CloneGitRepository { appName: string; @@ -145,3 +146,44 @@ const sanitizeRepoPathSSH = (input: string) => { }, }; }; + +interface Props { + appName: string; + type?: "application" | "compose"; + serverId: string | null; +} + +export const getGitCommitInfo = async ({ + appName, + type = "application", + serverId, +}: Props) => { + const { COMPOSE_PATH, APPLICATIONS_PATH } = paths(!!serverId); + const basePath = type === "compose" ? COMPOSE_PATH : APPLICATIONS_PATH; + const outputPath = join(basePath, appName, "code"); + let stdoutResult = ""; + const result = { + message: "", + hash: "", + }; + try { + const gitCommand = `git -C ${outputPath} log -1 --pretty=format:"%H---DELIMITER---%B"`; + if (serverId) { + const { stdout } = await execAsyncRemote(serverId, gitCommand); + stdoutResult = stdout.trim(); + } else { + const { stdout } = await execAsync(gitCommand); + stdoutResult = stdout.trim(); + } + + const parts = stdoutResult.split("---DELIMITER---"); + if (parts && parts.length === 2) { + result.hash = parts[0]?.trim() || ""; + result.message = parts[1]?.trim() || ""; + } + } catch (error) { + console.error(`Error getting git commit info: ${error}`); + return null; + } + return result; +}; From 04a1a84077eb768cdd65e336c26e68889f829ab5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 23:09:02 -0600 Subject: [PATCH 26/79] fix: ensure proper cleanup of Docker buildx builder container - Added commands to remove the builder container after Railpack build and prepare failures to prevent resource leaks. - Improved bash command structure for better readability and maintenance. --- packages/server/src/utils/builders/railpack.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index cb188fd09..305ff20e8 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -75,6 +75,7 @@ export const getRailpackCommand = (application: ApplicationNested) => { buildArgs.push(buildAppDirectory); const bashCommand = ` + # Ensure we have a builder with containerd docker buildx create --use --name builder-containerd --driver docker-container || true docker buildx use builder-containerd @@ -82,6 +83,7 @@ docker buildx use builder-containerd echo "Preparing Railpack build plan..." ; railpack ${prepareArgs.join(" ")} || { echo "❌ Railpack prepare failed" ; + docker buildx rm builder-containerd || true exit 1; } echo "✅ Railpack prepare completed." ; @@ -91,6 +93,7 @@ echo "Building with Railpack frontend..." ; ${exportEnvs.join("\n")} docker ${buildArgs.join(" ")} || { echo "❌ Railpack build failed" ; + docker buildx rm builder-containerd || true exit 1; } echo "✅ Railpack build completed." ; From 69b7777db4a050376d64f88a677c335f5a6e5d2b Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 15 Nov 2025 00:28:44 -0600 Subject: [PATCH 27/79] chore: update node-os-utils to version 2.0.1 and refactor lodash imports - Upgraded `node-os-utils` from version 1.3.7 to 2.0.1 across multiple package.json files. - Removed deprecated `@types/node-os-utils` dependency. - Refactored lodash imports to use a single import statement for consistency. - Enhanced Docker stats monitoring by integrating new features from `node-os-utils` version 2.0.1. --- .../database/backups/restore-backup.tsx | 4 +- .../dashboard/docker/logs/terminal-line.tsx | 4 +- apps/dokploy/package.json | 3 +- apps/dokploy/server/wss/docker-stats.ts | 79 +++++++++++++++++++ packages/server/package.json | 3 +- packages/server/src/monitoring/utils.ts | 29 ++++--- packages/server/src/services/application.ts | 3 + pnpm-lock.yaml | 26 ++---- 8 files changed, 112 insertions(+), 39 deletions(-) diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 6a0fb030a..01f6944e1 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -1,6 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import copy from "copy-to-clipboard"; -import { debounce } from "lodash"; +import _ from "lodash"; import { CheckIcon, ChevronsUpDown, @@ -236,7 +236,7 @@ export const RestoreBackup = ({ const currentDatabaseType = form.watch("databaseType"); const metadata = form.watch("metadata"); - const debouncedSetSearch = debounce((value: string) => { + const debouncedSetSearch = _.debounce((value: string) => { setDebouncedSearchTerm(value); }, 350); diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 5b929f3b6..a75f50386 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -1,5 +1,5 @@ import { FancyAnsi } from "fancy-ansi"; -import { escapeRegExp } from "lodash"; +import _ from "lodash"; import { Badge } from "@/components/ui/badge"; import { Tooltip, @@ -47,7 +47,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { } const htmlContent = fancyAnsi.toHtml(text); - const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); + const searchRegex = new RegExp(`(${_.escapeRegExp(term)})`, "gi"); const modifiedContent = htmlContent.replace( searchRegex, diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 183771f79..c9addf8de 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -120,7 +120,7 @@ "next": "^15.3.2", "next-i18next": "^15.4.2", "next-themes": "^0.2.1", - "node-os-utils": "1.3.7", + "node-os-utils": "2.0.1", "node-pty": "1.0.0", "node-schedule": "2.1.1", "nodemailer": "6.9.14", @@ -163,7 +163,6 @@ "@types/lodash": "4.17.4", "@types/micromatch": "4.0.9", "@types/node": "^18.19.104", - "@types/node-os-utils": "1.3.4", "@types/node-schedule": "2.1.6", "@types/nodemailer": "^6.4.17", "@types/qrcode": "^1.5.5", diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index 99e993dce..ebd53e93f 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -6,7 +6,9 @@ import { recordAdvancedStats, validateRequest, } from "@dokploy/server"; +import { OSUtils } from "node-os-utils"; import { WebSocketServer } from "ws"; +import { formatBytes } from "@/components/dashboard/database/backups/restore-backup"; export const setupDockerStatsMonitoringSocketServer = ( server: http.Server, @@ -49,6 +51,83 @@ export const setupDockerStatsMonitoringSocketServer = ( } const intervalId = setInterval(async () => { try { + // Special case: when monitoring "dokploy", get host system stats instead of container stats + if (appName === "dokploy") { + const osutils = new OSUtils(); + + // Get CPU usage + const cpuResult = await osutils.cpu.usage(); + const cpuUsage = cpuResult.success ? cpuResult.data : 0; + + // Get memory info + const memResult = await osutils.memory.info(); + let memUsedGB = 0; + let memTotalGB = 0; + let memUsedPercent = 0; + if (memResult.success) { + memTotalGB = memResult.data.total.toGB(); + memUsedGB = memResult.data.used.toGB(); + memUsedPercent = memResult.data.usagePercentage; + } + + // Get network stats from network.overview() or network.statsAsync() + let netInputBytes = 0; + let netOutputBytes = 0; + const networkOverview = await osutils.network.overview(); + if (networkOverview.success) { + netInputBytes = networkOverview.data.totalRxBytes.toBytes(); + netOutputBytes = networkOverview.data.totalTxBytes.toBytes(); + } + + // Get Block I/O from disk.stats() (available in v2.0!) + // If disk.stats() doesn't work in container, fallback to /proc/diskstats + let blockReadBytes = 0; + let blockWriteBytes = 0; + const diskStats = await osutils.disk.stats(); + if (diskStats.success && diskStats.data.length > 0) { + for (const stat of diskStats.data) { + blockReadBytes += stat.readBytes.toBytes(); + blockWriteBytes += stat.writeBytes.toBytes(); + } + } + + // Format memory usage similar to docker stats format: "used / total" + const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`; + const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`; + const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`; + + // Format network I/O + const netInputMb = netInputBytes / (1024 * 1024); + const netOutputMb = netOutputBytes / (1024 * 1024); + const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`; + + // Format Block I/O + const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`; + + // Create a stat object compatible with recordAdvancedStats + const stat = { + CPUPerc: `${cpuUsage.toFixed(2)}%`, + MemPerc: `${memUsedPercent.toFixed(2)}%`, + MemUsage: memUsageFormatted, + BlockIO: blockIOFormatted, + NetIO: netIOFormatted, + Container: "dokploy", + ID: "host-system", + Name: "dokploy", + }; + + await recordAdvancedStats(stat, appName); + const data = await getLastAdvancedStatsFile(appName); + console.log(data); + + ws.send( + JSON.stringify({ + data, + }), + ); + return; + } + const filter = { status: ["running"], ...(appType === "application" && { diff --git a/packages/server/package.json b/packages/server/package.json index 4d0f2e804..077ee3d5d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -61,7 +61,7 @@ "lodash": "4.17.21", "micromatch": "4.0.8", "nanoid": "3.3.11", - "node-os-utils": "1.3.7", + "node-os-utils": "2.0.1", "node-pty": "1.0.0", "node-schedule": "2.1.1", "nodemailer": "6.9.14", @@ -88,7 +88,6 @@ "@types/lodash": "4.17.4", "@types/micromatch": "4.0.9", "@types/node": "^18.19.104", - "@types/node-os-utils": "1.3.4", "@types/node-schedule": "2.1.6", "@types/nodemailer": "^6.4.17", "@types/qrcode": "^1.5.5", diff --git a/packages/server/src/monitoring/utils.ts b/packages/server/src/monitoring/utils.ts index 11ebb6169..23cb63f56 100644 --- a/packages/server/src/monitoring/utils.ts +++ b/packages/server/src/monitoring/utils.ts @@ -1,7 +1,6 @@ import { promises } from "node:fs"; -import osUtils from "node-os-utils"; +import { OSUtils } from "node-os-utils"; import { paths } from "../constants"; - export interface Container { BlockIO: string; CPUPerc: string; @@ -38,19 +37,23 @@ export const recordAdvancedStats = async ( }); if (appName === "dokploy") { - const disk = await osUtils.drive.info("/"); + const osutils = new OSUtils(); + const diskResult = await osutils.disk.usageByMountPoint("/"); - const diskUsage = disk.usedGb; - const diskTotal = disk.totalGb; - const diskUsedPercentage = disk.usedPercentage; - const diskFree = disk.freeGb; + if (diskResult.success && diskResult.data) { + const disk = diskResult.data; + const diskUsage = disk.used.toGB().toFixed(2); + const diskTotal = disk.total.toGB().toFixed(2); + const diskUsedPercentage = disk.usagePercentage; + const diskFree = disk.available.toGB().toFixed(2); - await updateStatsFile(appName, "disk", { - diskTotal: +diskTotal, - diskUsedPercentage: +diskUsedPercentage, - diskUsage: +diskUsage, - diskFree: +diskFree, - }); + await updateStatsFile(appName, "disk", { + diskTotal: +diskTotal, + diskUsedPercentage: +diskUsedPercentage, + diskUsage: +diskUsage, + diskFree: +diskFree, + }); + } } }; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index a3eb959b4..c10babe56 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -419,6 +419,9 @@ export const deployPreviewApplication = async ({ }; export const getApplicationStats = async (appName: string) => { + if (appName === "dokploy") { + return await getAdvancedStats(appName); + } const filter = { status: ["running"], label: [`com.docker.swarm.service.name=${appName}`], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1aae074f7..ba76d1b73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,8 +347,8 @@ importers: specifier: ^0.2.1 version: 0.2.1(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) node-os-utils: - specifier: 1.3.7 - version: 1.3.7 + specifier: 2.0.1 + version: 2.0.1 node-pty: specifier: 1.0.0 version: 1.0.0 @@ -473,9 +473,6 @@ importers: '@types/node': specifier: ^18.19.104 version: 18.19.104 - '@types/node-os-utils': - specifier: 1.3.4 - version: 1.3.4 '@types/node-schedule': specifier: 2.1.6 version: 2.1.6 @@ -688,8 +685,8 @@ importers: specifier: 3.3.11 version: 3.3.11 node-os-utils: - specifier: 1.3.7 - version: 1.3.7 + specifier: 2.0.1 + version: 2.0.1 node-pty: specifier: 1.0.0 version: 1.0.0 @@ -766,9 +763,6 @@ importers: '@types/node': specifier: ^18.19.104 version: 18.19.104 - '@types/node-os-utils': - specifier: 1.3.4 - version: 1.3.4 '@types/node-schedule': specifier: 2.1.6 version: 2.1.6 @@ -4000,9 +3994,6 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node-os-utils@1.3.4': - resolution: {integrity: sha512-BCUYrbdoO4FUbx6MB9atLNFnkxdliFaxdiTJMIPPiecXIApc5zf4NIqV5G1jWv/ReZvtYyHLs40RkBjHX+vykA==} - '@types/node-schedule@2.1.6': resolution: {integrity: sha512-6AlZSUiNTdaVmH5jXYxX9YgmF1zfOlbjUqw0EllTBmZCnN1R5RR/m/u3No1OiWR05bnQ4jM4/+w4FcGvkAtnKQ==} @@ -6312,8 +6303,9 @@ packages: '@types/node': optional: true - node-os-utils@1.3.7: - resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==} + node-os-utils@2.0.1: + resolution: {integrity: sha512-rH2N3qHZETLhdgTGhMMCE8zU3gsWO4we1MFtrSiAI7tYWrnJRc6dk2PseV4co3Lb0v/MbRONLQI2biHQYbpTpg==} + engines: {node: '>=18.0.0'} node-pty@1.0.0: resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==} @@ -11338,8 +11330,6 @@ snapshots: dependencies: '@types/node': 20.17.51 - '@types/node-os-utils@1.3.4': {} - '@types/node-schedule@2.1.6': dependencies: '@types/node': 20.17.51 @@ -13852,7 +13842,7 @@ snapshots: optionalDependencies: '@types/node': 18.19.104 - node-os-utils@1.3.7: {} + node-os-utils@2.0.1: {} node-pty@1.0.0: dependencies: From 969147cd59fc6bc783bf2eabb808b4dc4a6dbd59 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 15 Nov 2025 00:56:05 -0600 Subject: [PATCH 28/79] feat: enhance Docker stats monitoring with disk I/O statistics - Updated OSUtils instantiation to include disk I/O statistics. - Implemented filtering to exclude virtual devices from disk stats, ensuring only real disk devices are monitored. --- apps/dokploy/server/wss/docker-stats.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index ebd53e93f..f54ff7f69 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -53,7 +53,11 @@ export const setupDockerStatsMonitoringSocketServer = ( try { // Special case: when monitoring "dokploy", get host system stats instead of container stats if (appName === "dokploy") { - const osutils = new OSUtils(); + const osutils = new OSUtils({ + disk: { + includeStats: true, // Enable disk I/O statistics + }, + }); // Get CPU usage const cpuResult = await osutils.cpu.usage(); @@ -85,7 +89,17 @@ export const setupDockerStatsMonitoringSocketServer = ( let blockWriteBytes = 0; const diskStats = await osutils.disk.stats(); if (diskStats.success && diskStats.data.length > 0) { + // Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices + const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/]; for (const stat of diskStats.data) { + // Skip virtual devices + if ( + stat.device && + excludePatterns.some((pattern) => pattern.test(stat.device)) + ) { + continue; + } + // readBytes and writeBytes are DataSize objects with .toBytes() method blockReadBytes += stat.readBytes.toBytes(); blockWriteBytes += stat.writeBytes.toBytes(); } From a4caa47e106f6762876625d1555816ef3905591e Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 15 Nov 2025 00:59:00 -0600 Subject: [PATCH 29/79] feat: implement host system stats retrieval for Docker monitoring - Added a new function `getHostSystemStats` to encapsulate the logic for retrieving host system statistics using `node-os-utils`. - Refactored Docker stats monitoring to utilize the new function, improving code clarity and maintainability. - Removed redundant OSUtils instantiation from the Docker stats monitoring logic. --- apps/dokploy/server/wss/docker-stats.ts | 80 +------------------- packages/server/src/monitoring/utils.ts | 97 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 78 deletions(-) diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index f54ff7f69..02c834789 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -2,13 +2,12 @@ import type http from "node:http"; import { docker, execAsync, + getHostSystemStats, getLastAdvancedStatsFile, recordAdvancedStats, validateRequest, } from "@dokploy/server"; -import { OSUtils } from "node-os-utils"; import { WebSocketServer } from "ws"; -import { formatBytes } from "@/components/dashboard/database/backups/restore-backup"; export const setupDockerStatsMonitoringSocketServer = ( server: http.Server, @@ -53,82 +52,7 @@ export const setupDockerStatsMonitoringSocketServer = ( try { // Special case: when monitoring "dokploy", get host system stats instead of container stats if (appName === "dokploy") { - const osutils = new OSUtils({ - disk: { - includeStats: true, // Enable disk I/O statistics - }, - }); - - // Get CPU usage - const cpuResult = await osutils.cpu.usage(); - const cpuUsage = cpuResult.success ? cpuResult.data : 0; - - // Get memory info - const memResult = await osutils.memory.info(); - let memUsedGB = 0; - let memTotalGB = 0; - let memUsedPercent = 0; - if (memResult.success) { - memTotalGB = memResult.data.total.toGB(); - memUsedGB = memResult.data.used.toGB(); - memUsedPercent = memResult.data.usagePercentage; - } - - // Get network stats from network.overview() or network.statsAsync() - let netInputBytes = 0; - let netOutputBytes = 0; - const networkOverview = await osutils.network.overview(); - if (networkOverview.success) { - netInputBytes = networkOverview.data.totalRxBytes.toBytes(); - netOutputBytes = networkOverview.data.totalTxBytes.toBytes(); - } - - // Get Block I/O from disk.stats() (available in v2.0!) - // If disk.stats() doesn't work in container, fallback to /proc/diskstats - let blockReadBytes = 0; - let blockWriteBytes = 0; - const diskStats = await osutils.disk.stats(); - if (diskStats.success && diskStats.data.length > 0) { - // Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices - const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/]; - for (const stat of diskStats.data) { - // Skip virtual devices - if ( - stat.device && - excludePatterns.some((pattern) => pattern.test(stat.device)) - ) { - continue; - } - // readBytes and writeBytes are DataSize objects with .toBytes() method - blockReadBytes += stat.readBytes.toBytes(); - blockWriteBytes += stat.writeBytes.toBytes(); - } - } - - // Format memory usage similar to docker stats format: "used / total" - const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`; - const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`; - const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`; - - // Format network I/O - const netInputMb = netInputBytes / (1024 * 1024); - const netOutputMb = netOutputBytes / (1024 * 1024); - const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`; - - // Format Block I/O - const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`; - - // Create a stat object compatible with recordAdvancedStats - const stat = { - CPUPerc: `${cpuUsage.toFixed(2)}%`, - MemPerc: `${memUsedPercent.toFixed(2)}%`, - MemUsage: memUsageFormatted, - BlockIO: blockIOFormatted, - NetIO: netIOFormatted, - Container: "dokploy", - ID: "host-system", - Name: "dokploy", - }; + const stat = await getHostSystemStats(); await recordAdvancedStats(stat, appName); const data = await getLastAdvancedStatsFile(appName); diff --git a/packages/server/src/monitoring/utils.ts b/packages/server/src/monitoring/utils.ts index 23cb63f56..2c42b99a6 100644 --- a/packages/server/src/monitoring/utils.ts +++ b/packages/server/src/monitoring/utils.ts @@ -1,6 +1,7 @@ import { promises } from "node:fs"; import { OSUtils } from "node-os-utils"; import { paths } from "../constants"; + export interface Container { BlockIO: string; CPUPerc: string; @@ -57,6 +58,102 @@ export const recordAdvancedStats = async ( } }; +/** + * Get host system statistics using node-os-utils + * This is used when monitoring "dokploy" to show host stats instead of container stats + */ +export const getHostSystemStats = async (): Promise => { + const osutils = new OSUtils({ + disk: { + includeStats: true, // Enable disk I/O statistics + }, + }); + + // Get CPU usage + const cpuResult = await osutils.cpu.usage(); + const cpuUsage = cpuResult.success ? cpuResult.data : 0; + + // Get memory info + const memResult = await osutils.memory.info(); + let memUsedGB = 0; + let memTotalGB = 0; + let memUsedPercent = 0; + if (memResult.success) { + memTotalGB = memResult.data.total.toGB(); + memUsedGB = memResult.data.used.toGB(); + memUsedPercent = memResult.data.usagePercentage; + } + + // Get network stats from network.overview() + let netInputBytes = 0; + let netOutputBytes = 0; + const networkOverview = await osutils.network.overview(); + if (networkOverview.success) { + netInputBytes = networkOverview.data.totalRxBytes.toBytes(); + netOutputBytes = networkOverview.data.totalTxBytes.toBytes(); + } + + // Get Block I/O from disk.stats() + let blockReadBytes = 0; + let blockWriteBytes = 0; + const diskStats = await osutils.disk.stats(); + if (diskStats.success && diskStats.data.length > 0) { + // Filter out virtual devices (loop, ram, sr, etc.) - only include real disk devices + const excludePatterns = [/^loop/, /^ram/, /^sr\d+$/, /^fd\d+$/]; + for (const stat of diskStats.data) { + // Skip virtual devices + if ( + stat.device && + excludePatterns.some((pattern) => pattern.test(stat.device)) + ) { + continue; + } + // readBytes and writeBytes are DataSize objects with .toBytes() method + blockReadBytes += stat.readBytes.toBytes(); + blockWriteBytes += stat.writeBytes.toBytes(); + } + } + + // Format values similar to docker stats + const formatBytes = (bytes: number): string => { + if (bytes >= 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GiB`; + } + if (bytes >= 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(2)}MiB`; + } + if (bytes >= 1024) { + return `${(bytes / 1024).toFixed(2)}KiB`; + } + return `${bytes}B`; + }; + + // Format memory usage similar to docker stats format: "used / total" + const memUsedFormatted = `${memUsedGB.toFixed(2)}GiB`; + const memTotalFormatted = `${memTotalGB.toFixed(2)}GiB`; + const memUsageFormatted = `${memUsedFormatted} / ${memTotalFormatted}`; + + // Format network I/O + const netInputMb = netInputBytes / (1024 * 1024); + const netOutputMb = netOutputBytes / (1024 * 1024); + const netIOFormatted = `${netInputMb.toFixed(2)}MB / ${netOutputMb.toFixed(2)}MB`; + + // Format Block I/O + const blockIOFormatted = `${formatBytes(blockReadBytes)} / ${formatBytes(blockWriteBytes)}`; + + // Create a stat object compatible with recordAdvancedStats + return { + CPUPerc: `${cpuUsage.toFixed(2)}%`, + MemPerc: `${memUsedPercent.toFixed(2)}%`, + MemUsage: memUsageFormatted, + BlockIO: blockIOFormatted, + NetIO: netIOFormatted, + Container: "dokploy", + ID: "host-system", + Name: "dokploy", + }; +}; + export const getAdvancedStats = async (appName: string) => { return { cpu: await readStatsFile(appName, "cpu"), From 09a98a29e033d47f64648a439f784040e31d801c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 15 Nov 2025 00:59:36 -0600 Subject: [PATCH 30/79] fix: remove unnecessary console log from Docker stats monitoring --- apps/dokploy/server/wss/docker-stats.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/server/wss/docker-stats.ts b/apps/dokploy/server/wss/docker-stats.ts index 02c834789..bd740e976 100644 --- a/apps/dokploy/server/wss/docker-stats.ts +++ b/apps/dokploy/server/wss/docker-stats.ts @@ -56,7 +56,6 @@ export const setupDockerStatsMonitoringSocketServer = ( await recordAdvancedStats(stat, appName); const data = await getLastAdvancedStatsFile(appName); - console.log(data); ws.send( JSON.stringify({ From 05e3d241f17692925d1fc537c75c4957af64ac12 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 15 Nov 2025 17:43:51 -0600 Subject: [PATCH 31/79] feat: increase commit message truncation length and simplify truncation logic - Updated the maximum character length for commit message truncation from 150 to 200 characters. - Simplified the truncation logic by removing unnecessary checks and consolidating the function to focus solely on the new maximum length. - Enhanced the display logic for deployment titles to ensure better readability and user experience. --- .../deployments/show-deployments.tsx | 297 ++++++++---------- 1 file changed, 136 insertions(+), 161 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index a5a8c5fd6..8e7eb66ca 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -92,39 +92,21 @@ export const ShowDeployments = ({ new Set(), ); - // Maximum character length before truncating commit messages - const MAX_DESCRIPTION_LENGTH = 150; + const MAX_DESCRIPTION_LENGTH = 200; - // Helper function to truncate description intelligently - const truncateDescription = ( - description: string, - maxLength: number, - ): string => { - if (maxLength <= 0) { - return description; // Don't truncate if maxLength is 0 or negative - } - if (description.length <= maxLength) { + const truncateDescription = (description: string): string => { + if (description.length <= MAX_DESCRIPTION_LENGTH) { return description; } - // Try to truncate at a word boundary if possible - const truncated = description.slice(0, maxLength); + const truncated = description.slice(0, MAX_DESCRIPTION_LENGTH); const lastSpace = truncated.lastIndexOf(" "); - // If we find a space near the end (within last 20 chars), use it for cleaner truncation - if (lastSpace > maxLength - 20 && lastSpace > 0) { + // Truncate at word boundary if found near the end + if (lastSpace > MAX_DESCRIPTION_LENGTH - 20 && lastSpace > 0) { return `${truncated.slice(0, lastSpace)}...`; } return `${truncated}...`; }; - // Check if description should be truncated - const shouldTruncate = (description: string): boolean => { - // Only truncate if MAX_DESCRIPTION_LENGTH is greater than 0 - if (MAX_DESCRIPTION_LENGTH <= 0) { - return false; - } - return description.length > MAX_DESCRIPTION_LENGTH; - }; - // Toggle expand/collapse state for a specific deployment const toggleDescription = (deploymentId: string) => { setExpandedDescriptions((prev) => { @@ -274,165 +256,158 @@ export const ShowDeployments = ({
) : (
- {deployments?.map((deployment, index) => ( -
-
- - {index + 1}. {deployment.status} - - - {(() => { - // The commit message is in the title field, so we truncate that - const titleText = deployment.title.trim(); - const needsTruncation = shouldTruncate(titleText); - const isExpanded = expandedDescriptions.has( - deployment.deploymentId, - ); + {deployments?.map((deployment, index) => { + const titleText = deployment?.title?.trim() || ""; + const needsTruncation = titleText.length > MAX_DESCRIPTION_LENGTH; + const isExpanded = expandedDescriptions.has( + deployment.deploymentId, + ); - return ( -
- {/* Commit message (from title) - truncated */} - - {isExpanded || !needsTruncation - ? titleText - : truncateDescription( - titleText, - MAX_DESCRIPTION_LENGTH, - )} - - {needsTruncation && ( - - )} - {/* Hash (from description) - shown in compact form */} - {deployment.description && - deployment.description.trim() && ( - - {deployment.description} - - )} -
- ); - })()} -
-
-
- - {deployment.startedAt && deployment.finishedAt && ( - - - {formatDuration( - Math.floor( - (new Date(deployment.finishedAt).getTime() - - new Date(deployment.startedAt).getTime()) / - 1000, - ), - )} - - )} -
+ return ( +
+
+ + {index + 1}. {deployment.status} + + -
- {deployment.pid && deployment.status === "running" && ( - { - await killProcess({ - deploymentId: deployment.deploymentId, - }) - .then(() => { - toast.success("Process killed successfully"); - }) - .catch(() => { - toast.error("Error killing process"); - }); - }} - > - - - )} - + {isExpanded ? ( + <> + + Show less + + ) : ( + <> + + Show more + + )} + + )} + {/* Hash (from description) - shown in compact form */} + {deployment.description?.trim() && ( + + {deployment.description} + + )} +
+
+
+
+ + {deployment.startedAt && deployment.finishedAt && ( + + + {formatDuration( + Math.floor( + (new Date(deployment.finishedAt).getTime() - + new Date(deployment.startedAt).getTime()) / + 1000, + ), + )} + + )} +
- {deployment?.rollback && - deployment.status === "done" && - type === "application" && ( +
+ {deployment.pid && deployment.status === "running" && ( { - await rollback({ - rollbackId: deployment.rollback.rollbackId, + await killProcess({ + deploymentId: deployment.deploymentId, }) .then(() => { - toast.success( - "Rollback initiated successfully", - ); + toast.success("Process killed successfully"); }) .catch(() => { - toast.error("Error initiating rollback"); + toast.error("Error killing process"); }); }} > )} + + + {deployment?.rollback && + deployment.status === "done" && + type === "application" && ( + { + await rollback({ + rollbackId: deployment.rollback.rollbackId, + }) + .then(() => { + toast.success( + "Rollback initiated successfully", + ); + }) + .catch(() => { + toast.error("Error initiating rollback"); + }); + }} + > + + + )} +
-
- ))} + ); + })}
)} Date: Sat, 15 Nov 2025 17:46:14 -0600 Subject: [PATCH 32/79] refactor: simplify deployment description toggle logic - Removed the separate toggleDescription function and integrated its logic directly into the button's onClick handler for better readability. - Maintained functionality for expanding and collapsing deployment descriptions while streamlining the code structure. --- .../deployments/show-deployments.tsx | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 8e7eb66ca..1885ffc3a 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -100,26 +100,12 @@ export const ShowDeployments = ({ } const truncated = description.slice(0, MAX_DESCRIPTION_LENGTH); const lastSpace = truncated.lastIndexOf(" "); - // Truncate at word boundary if found near the end if (lastSpace > MAX_DESCRIPTION_LENGTH - 20 && lastSpace > 0) { return `${truncated.slice(0, lastSpace)}...`; } return `${truncated}...`; }; - // Toggle expand/collapse state for a specific deployment - const toggleDescription = (deploymentId: string) => { - setExpandedDescriptions((prev) => { - const next = new Set(prev); - if (next.has(deploymentId)) { - next.delete(deploymentId); - } else { - next.add(deploymentId); - } - return next; - }); - }; - // Check for stuck deployment (more than 9 minutes) - only for the most recent deployment const stuckDeployment = useMemo(() => { if (!isCloud || !deployments || deployments.length === 0) return null; @@ -286,9 +272,15 @@ export const ShowDeployments = ({ {needsTruncation && (
@@ -1400,11 +1466,6 @@ const EnvironmentPage = ( }} className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border" > - {service.serverId && ( -
- -
- )}
@@ -1471,7 +1532,15 @@ const EnvironmentPage = ( -
+
+ {service.serverName && ( +
+ + + {service.serverName} + +
+ )} Created diff --git a/packages/server/src/services/environment.ts b/packages/server/src/services/environment.ts index c35862714..fb1952818 100644 --- a/packages/server/src/services/environment.ts +++ b/packages/server/src/services/environment.ts @@ -37,16 +37,38 @@ export const findEnvironmentById = async (environmentId: string) => { applications: { with: { deployments: true, + server: true, + }, + }, + mariadb: { + with: { + server: true, + }, + }, + mongo: { + with: { + server: true, + }, + }, + mysql: { + with: { + server: true, + }, + }, + postgres: { + with: { + server: true, + }, + }, + redis: { + with: { + server: true, }, }, - mariadb: true, - mongo: true, - mysql: true, - postgres: true, - redis: true, compose: { with: { deployments: true, + server: true, }, }, project: true, From 3618be65fc5c83e1bb1b216659f4ee4f2e43cf13 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 15 Nov 2025 23:54:53 -0600 Subject: [PATCH 34/79] feat: add server icon display in environment service dashboard - Introduced a server icon next to services in the environment dashboard for better visual identification of server associations. - Enhanced user experience by providing immediate visual cues regarding the server linked to each service. --- .../project/[projectId]/environment/[environmentId].tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 17fe64ac9..a2e54ad51 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -1466,6 +1466,11 @@ const EnvironmentPage = ( }} className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border" > + {service.serverId && ( +
+ +
+ )}
From 3a17c9b9e826702a9178a0acee134626cd03ff16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= Date: Sun, 16 Nov 2025 15:57:34 +0100 Subject: [PATCH 35/79] fix: ensure Compose Traefik domain labels are written to local daemons --- packages/server/src/utils/builders/compose.ts | 4 ++-- packages/server/src/utils/docker/domain.ts | 15 ++++++--------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 7792ed11c..6ac5bf130 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -2,7 +2,7 @@ import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; -import { writeDomainsToComposeRemote } from "../docker/domain"; +import { writeDomainsToCompose } from "../docker/domain"; import { encodeBase64, getEnviromentVariablesObject, @@ -22,7 +22,7 @@ export const getBuildComposeCommand = async (compose: ComposeNested) => { const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const exportEnvCommand = getExportEnvCommand(compose); - const newCompose = await writeDomainsToComposeRemote(compose, domains); + const newCompose = await writeDomainsToCompose(compose, domains); const logContent = ` App Name: ${appName} Build Compose 🐳 diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index a176a4560..2272f364e 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -102,7 +102,7 @@ export const readComposeFile = async (compose: Compose) => { return null; }; -export const writeDomainsToComposeRemote = async ( +export const writeDomainsToCompose = async ( compose: Compose, domains: Domain[], ) => { @@ -120,19 +120,16 @@ echo "❌ Error: Compose file not found"; exit 1; `; } - if (compose.serverId) { - const composeString = stringify(composeConverted, { lineWidth: 1000 }); - const encodedContent = encodeBase64(composeString); - return `echo "${encodedContent}" | base64 -d > "${path}";`; - } + + const composeString = stringify(composeConverted, { lineWidth: 1000 }); + const encodedContent = encodeBase64(composeString); + return `echo "${encodedContent}" | base64 -d > "${path}";`; } catch (error) { // @ts-ignore - return `echo "❌ Has occured an error: ${error?.message || error}"; + return `echo "❌ Has occurred an error: ${error?.message || error}"; exit 1; `; } - - return ""; }; export const addDomainToCompose = async ( compose: Compose, From f7ddc715c7a0b710bda8e64fa23771bc412e2dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=84=D1=8B=D1=80=D0=B0=D1=82=20=D1=91=D0=B7=D0=B4=D1=8D?= =?UTF-8?q?=D0=BD?= <31664778+fir4tozden@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:34:39 +0300 Subject: [PATCH 36/79] chore: naming of redis volume has been made understandable --- packages/server/src/setup/redis-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/setup/redis-setup.ts b/packages/server/src/setup/redis-setup.ts index 7366546da..894b3427d 100644 --- a/packages/server/src/setup/redis-setup.ts +++ b/packages/server/src/setup/redis-setup.ts @@ -14,7 +14,7 @@ export const initializeRedis = async () => { Mounts: [ { Type: "volume", - Source: "redis-data-volume", + Source: "dokploy-redis", Target: "/data", }, ], From b2264a91487f9047152fce3b7faef7b3b20a8371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=84=D1=8B=D1=80=D0=B0=D1=82=20=D1=91=D0=B7=D0=B4=D1=8D?= =?UTF-8?q?=D0=BD?= <31664778+fir4tozden@users.noreply.github.com> Date: Sun, 16 Nov 2025 20:34:55 +0300 Subject: [PATCH 37/79] chore: naming of postgres volume has been made understandable --- packages/server/src/setup/postgres-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/setup/postgres-setup.ts b/packages/server/src/setup/postgres-setup.ts index cf162f1ed..377a84952 100644 --- a/packages/server/src/setup/postgres-setup.ts +++ b/packages/server/src/setup/postgres-setup.ts @@ -17,7 +17,7 @@ export const initializePostgres = async () => { Mounts: [ { Type: "volume", - Source: "dokploy-postgres-database", + Source: "dokploy-postgres", Target: "/var/lib/postgresql/data", }, ], From c4c193019574aab3ec0fccb31c43aa01ad799cbe Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 16 Nov 2025 15:43:46 -0600 Subject: [PATCH 38/79] fix: update database restore commands to properly quote user credentials - Modified the restore command functions for PostgreSQL, MariaDB, MySQL, and MongoDB to ensure that database user credentials are enclosed in single quotes. This change enhances command execution reliability and prevents potential issues with special characters in usernames and passwords. --- packages/server/src/utils/restore/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/utils/restore/utils.ts b/packages/server/src/utils/restore/utils.ts index c46077238..23052e642 100644 --- a/packages/server/src/utils/restore/utils.ts +++ b/packages/server/src/utils/restore/utils.ts @@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = ( database: string, databaseUser: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`; + return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`; }; export const getMariadbRestoreCommand = ( @@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = ( databaseUser: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`; + return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`; }; export const getMysqlRestoreCommand = ( database: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`; + return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`; }; export const getMongoRestoreCommand = ( @@ -30,7 +30,7 @@ export const getMongoRestoreCommand = ( databaseUser: string, databasePassword: string, ) => { - return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`; + return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`; }; export const getComposeSearchCommand = ( From 1581defc3993641e549180f06ccad121f3b3204a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sun, 16 Nov 2025 21:32:23 -0600 Subject: [PATCH 39/79] feat: conditionally render TimeBadge based on cloud status - Updated the ShowProjects and side layout components to only display the TimeBadge when not in cloud mode. - Modified the TimeBadge component to remove the refetch interval for server time when in cloud mode, returning null instead. - Enhanced the server API to return null for server time in cloud environments, improving performance and avoiding unnecessary queries. --- apps/dokploy/components/dashboard/projects/show.tsx | 13 ++++++++----- apps/dokploy/components/layouts/side.tsx | 2 +- apps/dokploy/components/ui/time-badge.tsx | 4 +--- apps/dokploy/server/api/routers/server.ts | 3 +++ 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 92f7ed5cb..5369a544e 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; +import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { AlertDialog, @@ -44,7 +45,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { Select, SelectContent, @@ -52,13 +52,14 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { TimeBadge } from "@/components/ui/time-badge"; import { api } from "@/utils/api"; import { HandleProject } from "./handle-project"; import { ProjectEnvironment } from "./project-environment"; -import { TimeBadge } from "@/components/ui/time-badge"; export const ShowProjects = () => { const utils = api.useUtils(); + const { data: isCloud } = api.settings.isCloud.useQuery(); const { data, isLoading } = api.project.all.useQuery(); const { data: auth } = api.user.get.useQuery(); const { mutateAsync } = api.project.remove.useMutation(); @@ -136,9 +137,11 @@ export const ShowProjects = () => { -
- -
+ {!isCloud && ( +
+ +
+ )}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 01df80f18..7473fe586 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -1126,7 +1126,7 @@ export default function Page({ children }: Props) {
- + {!isCloud && }
)} diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx index b409bf703..ea7f1f84e 100644 --- a/apps/dokploy/components/ui/time-badge.tsx +++ b/apps/dokploy/components/ui/time-badge.tsx @@ -4,9 +4,7 @@ import { useEffect, useState } from "react"; import { api } from "@/utils/api"; export function TimeBadge() { - const { data: serverTime } = api.server.getServerTime.useQuery(undefined, { - refetchInterval: 60000, // Refetch every 60 seconds - }); + const { data: serverTime } = api.server.getServerTime.useQuery(undefined); const [time, setTime] = useState(null); useEffect(() => { diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 4eb75bdf0..8a01228f8 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -384,6 +384,9 @@ export const serverRouter = createTRPCRouter({ return ip; }), getServerTime: protectedProcedure.query(() => { + if (IS_CLOUD) { + return null; + } return { time: new Date(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, From ba5e7e202662d52b5d92471cfdfafa8e0ab1d3c1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 18 Nov 2025 00:26:38 -0600 Subject: [PATCH 40/79] fix: improve error handling in getUpdateData function - Added error logging to the getUpdateData function to capture and display errors when retrieving the current service image digest, enhancing debugging capabilities. --- packages/server/src/services/settings.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 301573cb4..996aec352 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -59,7 +59,8 @@ export const getUpdateData = async (): Promise => { let currentDigest: string; try { currentDigest = await getServiceImageDigest(); - } catch { + } catch (error) { + console.error(error); // Docker service might not exist locally // You can run the # Installation command for docker service create mentioned in the below docs to test it locally: // https://docs.dokploy.com/docs/core/manual-installation From 46d1809f8424543cabf63d6397a2d2effd87f079 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 18 Nov 2025 00:27:04 -0600 Subject: [PATCH 41/79] chore: bump version to v0.25.7 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index c9addf8de..1b33df019 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.25.6", + "version": "v0.25.7", "private": true, "license": "Apache-2.0", "type": "module", From 6ba35057accbd619e0e8f82c825463addba2849e Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 18 Nov 2025 10:09:02 -0600 Subject: [PATCH 42/79] fix: update getServiceImageDigest to retrieve image digest more reliably - Refactored the getServiceImageDigest function to use a more robust command for fetching the Docker service image digest, improving accuracy. - Added console logging for the current digest to aid in debugging and monitoring. --- packages/server/src/services/settings.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 996aec352..793834059 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -42,10 +42,14 @@ export const pullLatestRelease = async () => { /** Returns Dokploy docker service image digest */ export const getServiceImageDigest = async () => { const { stdout } = await execAsync( - "docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'", + `docker image ls --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}' | \ + grep "$(docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}')" | \ + awk '{print $2}' | \ + awk -F':' '{print $2}'`, ); const currentDigest = stdout.trim().split("@")[1]; + console.log("currentDigest: ", currentDigest); if (!currentDigest) { throw new Error("Could not get current service image digest"); From 605de9780528e7b0167426c19a8b3b8c71f07c47 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:21:44 -0600 Subject: [PATCH 43/79] Correct description text in show-volume-backups.tsx Fix formatting in volume backups description. --- .../application/volume-backups/show-volume-backups.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx index 092538150..2e4dac472 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx @@ -86,7 +86,7 @@ export const ShowVolumeBackups = ({ Schedule volume backups to run automatically at specified - intervals. + intervals
From 8ca8839d7ed20d0b03da807dd3170a51789b8cf5 Mon Sep 17 00:00:00 2001 From: Bima42 Date: Tue, 18 Nov 2025 19:40:00 +0100 Subject: [PATCH 44/79] fix: update mount path on editing pg image --- apps/dokploy/server/api/routers/postgres.ts | 33 ++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index a05072ab7..9eb7f3af5 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -6,6 +6,7 @@ import { deployPostgres, findBackupsByDbId, findEnvironmentById, + findMountsByApplicationId, findPostgresById, findProjectById, IS_CLOUD, @@ -16,6 +17,7 @@ import { startServiceRemote, stopService, stopServiceRemote, + updateMount, updatePostgresById, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; @@ -37,6 +39,16 @@ import { postgres as postgresTable, } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; + +function getMountPath(dockerImage: string): string { + const versionMatch = dockerImage.match(/postgres:(\d+)/); + const version = versionMatch?.[1] + ? Number.parseInt(versionMatch[1], 10) + : 18; + + return `/var/lib/postgresql/${version}/data`; +} + export const postgresRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreatePostgres) @@ -79,11 +91,13 @@ export const postgresRouter = createTRPCRouter({ ); } + const mountPath = getMountPath(input.dockerImage); + await createMount({ serviceId: newPostgres.postgresId, serviceType: "postgres", volumeName: `${newPostgres.appName}-data`, - mountPath: "/var/lib/postgresql/data", + mountPath: mountPath, type: "volume", }); @@ -363,6 +377,23 @@ export const postgresRouter = createTRPCRouter({ message: "You are not authorized to update this Postgres", }); } + + if (rest.dockerImage) { + const mountPath = getMountPath(rest.dockerImage); + const mounts = await findMountsByApplicationId(postgresId, "postgres"); + if (!mounts || mounts.length === 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mount not found for this Postgres", + }); + } + for (const mount of mounts) { + await updateMount(mount.mountId, { + mountPath: mountPath, + }); + } + } + const service = await updatePostgresById(postgresId, { ...rest, }); From d08fdeb939101a94512f2f7d572ce37a9aee2ba2 Mon Sep 17 00:00:00 2001 From: Bima42 Date: Tue, 18 Nov 2025 19:47:29 +0100 Subject: [PATCH 45/79] fix: only upgrade those that use default pg path --- apps/dokploy/server/api/routers/postgres.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 9eb7f3af5..67faed6c6 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -42,9 +42,7 @@ import { cancelJobs } from "@/server/utils/backup"; function getMountPath(dockerImage: string): string { const versionMatch = dockerImage.match(/postgres:(\d+)/); - const version = versionMatch?.[1] - ? Number.parseInt(versionMatch[1], 10) - : 18; + const version = versionMatch?.[1] ? Number.parseInt(versionMatch[1], 10) : 18; return `/var/lib/postgresql/${version}/data`; } @@ -388,9 +386,11 @@ export const postgresRouter = createTRPCRouter({ }); } for (const mount of mounts) { - await updateMount(mount.mountId, { - mountPath: mountPath, - }); + if (mount.mountPath.startsWith("/var/lib/postgresql")) { + await updateMount(mount.mountId, { + mountPath: mountPath, + }); + } } } From 398300f729e7961608b39c47fed6d14c3dfa8e16 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Tue, 18 Nov 2025 17:02:13 -0500 Subject: [PATCH 46/79] chore: change view logs to deployments on preview deployments --- .../preview-deployments/show-preview-deployments.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index d93bbd1c8..9c2e48931 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -182,7 +182,16 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { id={deployment.previewDeploymentId} type="previewDeployment" serverId={data?.serverId || ""} - /> + > + + Date: Tue, 18 Nov 2025 22:44:42 -0600 Subject: [PATCH 47/79] fix: simplify getServiceImageDigest command for improved reliability - Refactored the getServiceImageDigest function to streamline the command used for retrieving the Docker service image digest, enhancing reliability. - Removed unnecessary console logging for the current digest. --- packages/server/src/services/settings.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 793834059..996aec352 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -42,14 +42,10 @@ export const pullLatestRelease = async () => { /** Returns Dokploy docker service image digest */ export const getServiceImageDigest = async () => { const { stdout } = await execAsync( - `docker image ls --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}' | \ - grep "$(docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}')" | \ - awk '{print $2}' | \ - awk -F':' '{print $2}'`, + "docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'", ); const currentDigest = stdout.trim().split("@")[1]; - console.log("currentDigest: ", currentDigest); if (!currentDigest) { throw new Error("Could not get current service image digest"); From 4884ee33522f7062dd256a54cb094a6742c9e848 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 00:22:29 -0600 Subject: [PATCH 48/79] feat: add KillBuild component and API mutation for terminating Docker builds - Introduced a new KillBuild component that allows users to terminate ongoing Docker builds for both applications and compose setups. - Implemented corresponding API mutations in the application and compose routers to handle build termination requests. - Enhanced queue setup with a killDockerBuild function to execute the termination commands on the server. --- .../application/deployments/kill-build.tsx | 65 +++++++++++++++++++ .../deployments/show-deployments.tsx | 4 ++ .../dokploy/server/api/routers/application.ts | 22 ++++++- apps/dokploy/server/api/routers/compose.ts | 21 +++++- apps/dokploy/server/queues/queueSetup.ts | 31 +++++++++ 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 apps/dokploy/components/dashboard/application/deployments/kill-build.tsx diff --git a/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx new file mode 100644 index 000000000..784534dd6 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/deployments/kill-build.tsx @@ -0,0 +1,65 @@ +import { Scissors } from "lucide-react"; +import { toast } from "sonner"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; + +interface Props { + id: string; + type: "application" | "compose"; +} + +export const KillBuild = ({ id, type }: Props) => { + const { mutateAsync, isLoading } = + type === "application" + ? api.application.killBuild.useMutation() + : api.compose.killBuild.useMutation(); + + return ( + + + + + + + Are you sure to kill the build? + + This will kill the build process + + + + Cancel + { + await mutateAsync({ + applicationId: id || "", + composeId: id || "", + }) + .then(() => { + toast.success("Build killed successfully"); + }) + .catch((err) => { + toast.error(err.message); + }); + }} + > + Confirm + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 1885ffc3a..7f3bc82b4 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -25,6 +25,7 @@ import { import { api, type RouterOutputs } from "@/utils/api"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; import { CancelQueues } from "./cancel-queues"; +import { KillBuild } from "./kill-build"; import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; @@ -143,6 +144,9 @@ export const ShowDeployments = ({
+ {(type === "application" || type === "compose") && ( + + )} {(type === "application" || type === "compose") && ( )} diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 006d024c4..c713fd7eb 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -58,7 +58,11 @@ import { applications, } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; -import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup"; +import { + cleanQueuesByApplication, + killDockerBuild, + myQueue, +} from "@/server/queues/queueSetup"; import { cancelDeployment, deploy } from "@/server/utils/deploy"; import { uploadFileSchema } from "@/utils/schema"; @@ -725,7 +729,21 @@ export const applicationRouter = createTRPCRouter({ } await cleanQueuesByApplication(input.applicationId); }), - + killBuild: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if ( + application.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to kill this build", + }); + } + await killDockerBuild("application", application.serverId); + }), readTraefikConfig: protectedProcedure .input(apiFindOneApplication) .query(async ({ input, ctx }) => { diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 026b6e8ad..e233dc6ca 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -59,7 +59,11 @@ import { compose as composeTable, } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; -import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup"; +import { + cleanQueuesByCompose, + killDockerBuild, + myQueue, +} from "@/server/queues/queueSetup"; import { cancelDeployment, deploy } from "@/server/utils/deploy"; import { generatePassword } from "@/templates/utils"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; @@ -248,6 +252,21 @@ export const composeRouter = createTRPCRouter({ await cleanQueuesByCompose(input.composeId); return { success: true, message: "Queues cleaned successfully" }; }), + killBuild: protectedProcedure + .input(apiFindCompose) + .mutation(async ({ input, ctx }) => { + const compose = await findComposeById(input.composeId); + if ( + compose.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to kill this build", + }); + } + await killDockerBuild("compose", compose.serverId); + }), loadServices: protectedProcedure .input(apiFetchServices) diff --git a/apps/dokploy/server/queues/queueSetup.ts b/apps/dokploy/server/queues/queueSetup.ts index 1577273c8..351f5d1c0 100644 --- a/apps/dokploy/server/queues/queueSetup.ts +++ b/apps/dokploy/server/queues/queueSetup.ts @@ -1,3 +1,7 @@ +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; import { Queue } from "bullmq"; import { redisConfig } from "./redis-connection"; @@ -41,4 +45,31 @@ export const cleanQueuesByCompose = async (composeId: string) => { } }; +export const killDockerBuild = async ( + type: "application" | "compose", + serverId: string | null, +) => { + try { + if (type === "application") { + const command = `pkill -2 -f "docker build"`; + + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } else if (type === "compose") { + const command = `pkill -2 -f "docker compose"`; + + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } + } catch (error) { + console.error(error); + } +}; + export { myQueue }; From 96dff0c1bb14bc903616537c467280af72e91596 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 02:34:05 -0600 Subject: [PATCH 49/79] chore: bump version to v0.25.8 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 1b33df019..070a0d6a4 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.25.7", + "version": "v0.25.8", "private": true, "license": "Apache-2.0", "type": "module", From 052fc5ffe1cbdfc28121a2f7a312f7eb65011b90 Mon Sep 17 00:00:00 2001 From: MacBook Pro Date: Wed, 19 Nov 2025 19:14:48 +0700 Subject: [PATCH 50/79] fix: server time is incorrect --- apps/dokploy/components/ui/time-badge.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx index ea7f1f84e..e72a94f9c 100644 --- a/apps/dokploy/components/ui/time-badge.tsx +++ b/apps/dokploy/components/ui/time-badge.tsx @@ -44,15 +44,17 @@ export function TimeBadge() { .padStart(2, "0")}`; }; + const formattedTime = new Intl.DateTimeFormat("en-US", {timeZone: serverTime.timezone, timeStyle: "medium", hour12:false}).format(time) + return ( -
- Server Time: - - {time.toLocaleTimeString()} - - - ({serverTime.timezone} | {getUtcOffset(serverTime.timezone)}) - -
+
+
+ Server Time: + {formattedTime} +
+ + {serverTime.timezone} | {getUtcOffset(serverTime.timezone)} + +
); } From daff2adb0237f4b73fec3fecba4a44152b64301e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:35:39 +0000 Subject: [PATCH 51/79] [autofix.ci] apply automated fixes --- apps/dokploy/components/ui/time-badge.tsx | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/ui/time-badge.tsx b/apps/dokploy/components/ui/time-badge.tsx index e72a94f9c..4cf778f25 100644 --- a/apps/dokploy/components/ui/time-badge.tsx +++ b/apps/dokploy/components/ui/time-badge.tsx @@ -44,17 +44,21 @@ export function TimeBadge() { .padStart(2, "0")}`; }; - const formattedTime = new Intl.DateTimeFormat("en-US", {timeZone: serverTime.timezone, timeStyle: "medium", hour12:false}).format(time) + const formattedTime = new Intl.DateTimeFormat("en-US", { + timeZone: serverTime.timezone, + timeStyle: "medium", + hour12: false, + }).format(time); return (
-
- Server Time: - {formattedTime} -
- - {serverTime.timezone} | {getUtcOffset(serverTime.timezone)} - -
+
+ Server Time: + {formattedTime} +
+ + {serverTime.timezone} | {getUtcOffset(serverTime.timezone)} + +
); } From 8a93116ce04a476a0eac1c48e6742d28e3fe1f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=84=D1=8B=D1=80=D0=B0=D1=82=20=D1=91=D0=B7=D0=B4=D1=8D?= =?UTF-8?q?=D0=BD?= <31664778+fir4tozden@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:48:29 +0300 Subject: [PATCH 52/79] fix: update docker cleanup commands to include --all options --- packages/server/src/services/settings.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 301573cb4..3c198d70d 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -218,11 +218,11 @@ echo "$json_output" }; export const cleanupFullDocker = async (serverId?: string | null) => { - const cleanupImages = "docker image prune --force"; - const cleanupVolumes = "docker volume prune --force"; + const cleanupImages = "docker image prune --all --force"; + const cleanupVolumes = "docker volume prune --all --force"; const cleanupContainers = "docker container prune --force"; - const cleanupSystem = "docker system prune --force --volumes"; - const cleanupBuilder = "docker builder prune --force"; + const cleanupSystem = "docker system prune --all --force --volumes"; + const cleanupBuilder = "docker builder prune --all --force"; try { if (serverId) { From 425b8ec3c2785ef998a1af8b00f08168d055cd8e Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 09:58:16 -0600 Subject: [PATCH 53/79] fix: update Docker command execution to use a clean environment - Modified Docker command invocations in compose service functions to use `env -i PATH="$PATH"` for improved environment isolation. - Ensured consistent handling of Docker commands across `removeCompose`, `startCompose`, and `stopCompose` functions in `compose.ts`. - Updated command execution in the builders to maintain environment integrity during Docker operations. --- packages/server/src/services/compose.ts | 15 +++++++++------ packages/server/src/utils/builders/compose.ts | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 519a0c404..3a2afb64d 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -375,7 +375,7 @@ export const removeCompose = async ( } else { const command = ` docker network disconnect ${compose.appName} dokploy-traefik; - cd ${projectPath} && docker compose -p ${compose.appName} down ${ + cd ${projectPath} && env -i PATH="$PATH" docker compose -p ${compose.appName} down ${ deleteVolumes ? "--volumes" : "" } && rm -rf ${projectPath}`; @@ -402,7 +402,7 @@ export const startCompose = async (composeId: string) => { const projectPath = join(COMPOSE_PATH, compose.appName, "code"); const path = compose.sourceType === "raw" ? "docker-compose.yml" : compose.composePath; - const baseCommand = `docker compose -p ${compose.appName} -f ${path} up -d`; + const baseCommand = `env -i PATH="$PATH" docker compose -p ${compose.appName} -f ${path} up -d`; if (compose.composeType === "docker-compose") { if (compose.serverId) { await execAsyncRemote( @@ -437,14 +437,17 @@ export const stopCompose = async (composeId: string) => { if (compose.serverId) { await execAsyncRemote( compose.serverId, - `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${ + `cd ${join(COMPOSE_PATH, compose.appName)} && env -i PATH="$PATH" docker compose -p ${ compose.appName } stop`, ); } else { - await execAsync(`docker compose -p ${compose.appName} stop`, { - cwd: join(COMPOSE_PATH, compose.appName), - }); + await execAsync( + `env -i PATH="$PATH" docker compose -p ${compose.appName} stop`, + { + cwd: join(COMPOSE_PATH, compose.appName), + }, + ); } } diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 6ac5bf130..e52beef57 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -54,7 +54,7 @@ Compose Type: ${composeType} ✅`; ${exportEnvCommand} ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""} - docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } + env -i PATH="$PATH" docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } ${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""} echo "Docker Compose Deployed: ✅"; From 153390ff269c066752cb1fa3c3b43fa8c28d1ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=84=D1=8B=D1=80=D0=B0=D1=82=20=D1=91=D0=B7=D0=B4=D1=8D?= =?UTF-8?q?=D0=BD?= <31664778+fir4tozden@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:59:19 +0300 Subject: [PATCH 54/79] Update settings.ts --- packages/server/src/services/settings.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index a480e298c..996aec352 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -219,11 +219,11 @@ echo "$json_output" }; export const cleanupFullDocker = async (serverId?: string | null) => { - const cleanupImages = "docker image prune --all --force"; - const cleanupVolumes = "docker volume prune --all --force"; + const cleanupImages = "docker image prune --force"; + const cleanupVolumes = "docker volume prune --force"; const cleanupContainers = "docker container prune --force"; - const cleanupSystem = "docker system prune --all --force --volumes"; - const cleanupBuilder = "docker builder prune --all --force"; + const cleanupSystem = "docker system prune --force --volumes"; + const cleanupBuilder = "docker builder prune --force"; try { if (serverId) { From 42a4cc7fff693d8ba0f3da1516c8cadb9d123d33 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 10:14:20 -0600 Subject: [PATCH 55/79] chore: bump version to v0.25.9 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 070a0d6a4..b519f3ea7 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.25.8", + "version": "v0.25.9", "private": true, "license": "Apache-2.0", "type": "module", From af2b053caacdd55753898ca6d14101c8e272cf87 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 21:17:09 -0600 Subject: [PATCH 56/79] feat: enhance environment variable handling for shell commands - Added `prepareEnvironmentVariablesForShell` function to properly escape environment variables for shell usage. - Updated various builders (Docker, Heroku, Nixpacks, Paketo, Railpack) to utilize the new function for improved handling of special characters in environment variables. - Introduced tests to validate the handling of environment variables with various special characters, ensuring robustness in shell command execution. - Added `shell-quote` dependency to manage quoting of shell arguments effectively. --- apps/dokploy/__test__/env/environment.test.ts | 311 +++++++++++++++++- packages/server/package.json | 2 + packages/server/src/utils/builders/compose.ts | 3 +- .../server/src/utils/builders/docker-file.ts | 9 +- packages/server/src/utils/builders/heroku.ts | 6 +- .../server/src/utils/builders/nixpacks.ts | 6 +- packages/server/src/utils/builders/paketo.ts | 6 +- .../server/src/utils/builders/railpack.ts | 17 +- packages/server/src/utils/docker/utils.ts | 16 + pnpm-lock.yaml | 11 + 10 files changed, 368 insertions(+), 19 deletions(-) diff --git a/apps/dokploy/__test__/env/environment.test.ts b/apps/dokploy/__test__/env/environment.test.ts index 95d46dcc0..24ef18b00 100644 --- a/apps/dokploy/__test__/env/environment.test.ts +++ b/apps/dokploy/__test__/env/environment.test.ts @@ -1,4 +1,7 @@ -import { prepareEnvironmentVariables } from "@dokploy/server/index"; +import { + prepareEnvironmentVariables, + prepareEnvironmentVariablesForShell, +} from "@dokploy/server/index"; import { describe, expect, it } from "vitest"; const projectEnv = ` @@ -332,4 +335,310 @@ IS_DEV=\${{environment.DEVELOPMENT}} "IS_DEV=0", ]); }); + + it("handles environment variables with single quotes in values", () => { + const envWithSingleQuotes = ` +ENV_VARIABLE='ENVITONME'NT' +ANOTHER_VAR='value with 'quotes' inside' +SIMPLE_VAR=no-quotes +`; + + const serviceWithSingleQuotes = ` +TEST_VAR=\${{environment.ENV_VARIABLE}} +ANOTHER_TEST=\${{environment.ANOTHER_VAR}} +SIMPLE=\${{environment.SIMPLE_VAR}} +`; + + const resolved = prepareEnvironmentVariables( + serviceWithSingleQuotes, + "", + envWithSingleQuotes, + ); + + expect(resolved).toEqual([ + "TEST_VAR=ENVITONME'NT", + "ANOTHER_TEST=value with 'quotes' inside", + "SIMPLE=no-quotes", + ]); + }); +}); + +describe("prepareEnvironmentVariablesForShell (shell escaping)", () => { + it("escapes single quotes in environment variable values", () => { + const serviceEnv = ` +ENV_VARIABLE='ENVITONME'NT' +ANOTHER_VAR='value with 'quotes' inside' +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // shell-quote should wrap these in double quotes + expect(resolved).toEqual([ + `"ENV_VARIABLE=ENVITONME'NT"`, + `"ANOTHER_VAR=value with 'quotes' inside"`, + ]); + }); + + it("escapes double quotes in environment variable values", () => { + const serviceEnv = ` +MESSAGE="Hello "World"" +QUOTED_PATH="/path/to/"file"" +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // shell-quote wraps in single quotes when there are double quotes inside + expect(resolved).toEqual([ + `'MESSAGE=Hello "World"'`, + `'QUOTED_PATH=/path/to/"file"'`, + ]); + }); + + it("escapes dollar signs in environment variable values", () => { + const serviceEnv = ` +PRICE=$100 +VARIABLE=$HOME/path +TEMPLATE=Hello $USER +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // Dollar signs should be escaped to prevent variable expansion + for (const env of resolved) { + expect(env).toContain("$"); + } + }); + + it("escapes backticks in environment variable values", () => { + const serviceEnv = ` +COMMAND=\`echo "test"\` +NESTED=value with \`backticks\` inside +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // Backticks are escaped/removed by dotenv parsing, but values should be safely quoted + expect(resolved.length).toBe(2); + expect(resolved[0]).toContain("COMMAND"); + expect(resolved[1]).toContain("NESTED"); + }); + + it("handles environment variables with spaces", () => { + const serviceEnv = ` +FULL_NAME="John Doe" +MESSAGE='Hello World' +SENTENCE=This is a test +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // shell-quote uses single quotes for strings with spaces + expect(resolved).toEqual([ + `'FULL_NAME=John Doe'`, + `'MESSAGE=Hello World'`, + `'SENTENCE=This is a test'`, + ]); + }); + + it("handles environment variables with backslashes", () => { + const serviceEnv = ` +WINDOWS_PATH=C:\\Users\\Documents +ESCAPED=value\\with\\backslashes +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // Backslashes should be properly escaped + expect(resolved.length).toBe(2); + for (const env of resolved) { + expect(env).toContain("\\"); + } + }); + + it("handles simple environment variables without special characters", () => { + const serviceEnv = ` +NODE_ENV=production +PORT=3000 +DEBUG=true +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // shell-quote escapes the = sign in some cases + expect(resolved).toEqual([ + "NODE_ENV\\=production", + "PORT\\=3000", + "DEBUG\\=true", + ]); + }); + + it("handles environment variables with mixed special characters", () => { + const serviceEnv = ` +COMPLEX='value with "double" and 'single' quotes' +BASH_COMMAND=echo "$HOME" && echo 'test' +WEIRD=\`echo "$VAR"\` with 'quotes' and "more" +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // All should be escaped, none should throw errors + expect(resolved.length).toBe(3); + // Verify each can be safely used in shell + for (const env of resolved) { + expect(typeof env).toBe("string"); + expect(env.length).toBeGreaterThan(0); + } + }); + + it("handles environment variables with newlines", () => { + const serviceEnv = ` +MULTILINE="line1 +line2 +line3" +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(1); + expect(resolved[0]).toContain("MULTILINE"); + }); + + it("handles empty environment variable values", () => { + const serviceEnv = ` +EMPTY= +EMPTY_QUOTED="" +EMPTY_SINGLE='' +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + // shell-quote escapes the = sign for empty values + expect(resolved).toEqual([ + "EMPTY\\=", + "EMPTY_QUOTED\\=", + "EMPTY_SINGLE\\=", + ]); + }); + + it("handles environment variables with equals signs in values", () => { + const serviceEnv = ` +EQUATION=a=b+c +CONNECTION_STRING=user=admin;password=test +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(2); + expect(resolved[0]).toContain("EQUATION"); + expect(resolved[1]).toContain("CONNECTION_STRING"); + }); + + it("resolves and escapes environment variables together", () => { + const projectEnv = ` +BASE_URL=https://example.com +API_KEY='secret-key-with-quotes' +`; + + const environmentEnv = ` +ENV_NAME=production +DB_PASS='pa$$word' +`; + + const serviceEnv = ` +FULL_URL=\${{project.BASE_URL}}/api +AUTH_KEY=\${{project.API_KEY}} +ENVIRONMENT=\${{environment.ENV_NAME}} +DB_PASSWORD=\${{environment.DB_PASS}} +CUSTOM='value with 'quotes' inside' +`; + + const resolved = prepareEnvironmentVariablesForShell( + serviceEnv, + projectEnv, + environmentEnv, + ); + + expect(resolved.length).toBe(5); + // All resolved values should be properly escaped + for (const env of resolved) { + expect(typeof env).toBe("string"); + } + }); + + it("handles environment variables with semicolons and ampersands", () => { + const serviceEnv = ` +COMMAND=echo "test" && echo "test2" +MULTIPLE=cmd1; cmd2; cmd3 +URL_WITH_PARAMS=https://example.com?a=1&b=2&c=3 +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(3); + // These should be safely escaped to prevent command injection + for (const env of resolved) { + expect(typeof env).toBe("string"); + expect(env.length).toBeGreaterThan(0); + } + }); + + it("handles environment variables with pipes and redirects", () => { + const serviceEnv = ` +PIPE_COMMAND=cat file | grep test +REDIRECT=echo "test" > output.txt +BOTH=cat input.txt | grep pattern > output.txt +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(3); + // Pipes and redirects should be safely quoted + expect(resolved[0]).toContain("PIPE_COMMAND"); + expect(resolved[1]).toContain("REDIRECT"); + expect(resolved[2]).toContain("BOTH"); + // At least one should contain a pipe + const hasPipe = resolved.some((env) => env.includes("|")); + expect(hasPipe).toBe(true); + }); + + it("handles environment variables with parentheses and brackets", () => { + const serviceEnv = ` +MATH=(a+b)*c +ARRAY=[1,2,3] +JSON={"key":"value"} +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(3); + expect(resolved[0]).toContain("("); + expect(resolved[1]).toContain("["); + expect(resolved[2]).toContain("{"); + }); + + it("handles very long environment variable values", () => { + const longValue = "a".repeat(10000); + const serviceEnv = `LONG_VAR=${longValue}`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(1); + expect(resolved[0]).toContain("LONG_VAR"); + expect(resolved[0]?.length).toBeGreaterThan(10000); + }); + + it("handles special unicode characters in environment variables", () => { + const serviceEnv = ` +EMOJI=Hello 🌍 World 🚀 +CHINESE=你好世界 +SPECIAL=café résumé naïve +`; + + const resolved = prepareEnvironmentVariablesForShell(serviceEnv, "", ""); + + expect(resolved.length).toBe(3); + expect(resolved[0]).toContain("🌍"); + expect(resolved[1]).toContain("你好"); + expect(resolved[2]).toContain("café"); + }); }); diff --git a/packages/server/package.json b/packages/server/package.json index 077ee3d5d..6a9b84f77 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -75,6 +75,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "rotating-file-stream": "3.2.3", + "shell-quote": "^1.8.1", "slugify": "^1.6.6", "ssh2": "1.15.0", "toml": "3.0.0", @@ -93,6 +94,7 @@ "@types/qrcode": "^1.5.5", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "@types/shell-quote": "^1.7.5", "@types/ssh2": "1.15.1", "@types/ws": "8.5.10", "drizzle-kit": "^0.30.6", diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index e52beef57..d196cef04 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -2,6 +2,7 @@ import { dirname, join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { InferResultType } from "@dokploy/server/types/with"; import boxen from "boxen"; +import { quote } from "shell-quote"; import { writeDomainsToCompose } from "../docker/domain"; import { encodeBase64, @@ -137,7 +138,7 @@ const getExportEnvCommand = (compose: ComposeNested) => { compose.environment.project.env, ); const exports = Object.entries(envVars) - .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) + .map(([key, value]) => `export ${key}=${quote([value])}`) .join("\n"); return exports ? `\n# Export environment variables\n${exports}\n` : ""; diff --git a/packages/server/src/utils/builders/docker-file.ts b/packages/server/src/utils/builders/docker-file.ts index a0acf5e6c..8ca99ccf2 100644 --- a/packages/server/src/utils/builders/docker-file.ts +++ b/packages/server/src/utils/builders/docker-file.ts @@ -1,7 +1,8 @@ import { getEnviromentVariablesObject, - prepareEnvironmentVariables, + prepareEnvironmentVariablesForShell, } from "@dokploy/server/utils/docker/utils"; +import { quote } from "shell-quote"; import { getBuildAppDirectory, getDockerContextPath, @@ -40,14 +41,14 @@ export const getDockerCommand = (application: ApplicationNested) => { commandArgs.push("--no-cache"); } - const args = prepareEnvironmentVariables( + const args = prepareEnvironmentVariablesForShell( buildArgs, application.environment.project.env, application.environment.env, ); for (const arg of args) { - commandArgs.push("--build-arg", `'${arg}'`); + commandArgs.push("--build-arg", arg); } const secrets = getEnviromentVariablesObject( @@ -57,7 +58,7 @@ export const getDockerCommand = (application: ApplicationNested) => { ); const joinedSecrets = Object.entries(secrets) - .map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`) + .map(([key, value]) => `${key}=${quote([value])}`) .join(" "); for (const key in secrets) { diff --git a/packages/server/src/utils/builders/heroku.ts b/packages/server/src/utils/builders/heroku.ts index e1ab4dff4..8b38c694d 100644 --- a/packages/server/src/utils/builders/heroku.ts +++ b/packages/server/src/utils/builders/heroku.ts @@ -1,4 +1,4 @@ -import { prepareEnvironmentVariables } from "../docker/utils"; +import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -6,7 +6,7 @@ export const getHerokuCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -26,7 +26,7 @@ export const getHerokuCommand = (application: ApplicationNested) => { } for (const env of envVariables) { - args.push("--env", `'${env}'`); + args.push("--env", env); } const command = `pack ${args.join(" ")}`; diff --git a/packages/server/src/utils/builders/nixpacks.ts b/packages/server/src/utils/builders/nixpacks.ts index 37f1953a4..b7134ea65 100644 --- a/packages/server/src/utils/builders/nixpacks.ts +++ b/packages/server/src/utils/builders/nixpacks.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { getStaticCommand } from "@dokploy/server/utils/builders/static"; import { nanoid } from "nanoid"; -import { prepareEnvironmentVariables } from "../docker/utils"; +import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -10,7 +10,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => { const buildAppDirectory = getBuildAppDirectory(application); const buildContainerId = `${appName}-${nanoid(10)}`; - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -23,7 +23,7 @@ export const getNixpacksCommand = (application: ApplicationNested) => { } for (const env of envVariables) { - args.push("--env", `'${env}'`); + args.push("--env", env); } if (publishDirectory) { diff --git a/packages/server/src/utils/builders/paketo.ts b/packages/server/src/utils/builders/paketo.ts index eb9767e7f..bb4f8c8a4 100644 --- a/packages/server/src/utils/builders/paketo.ts +++ b/packages/server/src/utils/builders/paketo.ts @@ -1,4 +1,4 @@ -import { prepareEnvironmentVariables } from "../docker/utils"; +import { prepareEnvironmentVariablesForShell } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -6,7 +6,7 @@ export const getPaketoCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -26,7 +26,7 @@ export const getPaketoCommand = (application: ApplicationNested) => { } for (const env of envVariables) { - args.push("--env", `'${env}'`); + args.push("--env", env); } const command = `pack ${args.join(" ")}`; diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 305ff20e8..0d23b4cff 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -1,8 +1,10 @@ import { createHash } from "node:crypto"; import { nanoid } from "nanoid"; +import { quote } from "shell-quote"; import { parseEnvironmentKeyValuePair, prepareEnvironmentVariables, + prepareEnvironmentVariablesForShell, } from "../docker/utils"; import { getBuildAppDirectory } from "../filesystem/directory"; import type { ApplicationNested } from "."; @@ -18,7 +20,7 @@ const calculateSecretsHash = (envVariables: string[]): string => { export const getRailpackCommand = (application: ApplicationNested) => { const { env, appName, cleanCache } = application; const buildAppDirectory = getBuildAppDirectory(application); - const envVariables = prepareEnvironmentVariables( + const envVariables = prepareEnvironmentVariablesForShell( env, application.environment.project.env, application.environment.env, @@ -35,7 +37,7 @@ export const getRailpackCommand = (application: ApplicationNested) => { ]; for (const env of envVariables) { - prepareArgs.push("--env", `'${env}'`); + prepareArgs.push("--env", env); } // Calculate secrets hash for layer invalidation @@ -63,12 +65,19 @@ export const getRailpackCommand = (application: ApplicationNested) => { ]; // Add secrets properly formatted + // Use prepareEnvironmentVariables (without ForShell) to get raw values for parsing + const rawEnvVariables = prepareEnvironmentVariables( + env, + application.environment.project.env, + application.environment.env, + ); const exportEnvs = []; - for (const pair of envVariables) { + for (const pair of rawEnvVariables) { const [key, value] = parseEnvironmentKeyValuePair(pair); if (key && value) { buildArgs.push("--secret", `id=${key},env=${key}`); - exportEnvs.push(`export ${key}='${value}'`); + // Use shell-quote to properly escape the export statement + exportEnvs.push(`export ${key}=${quote([value])}`); } } diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 6d00aa0df..4258cfbbe 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -5,6 +5,7 @@ import { docker, paths } from "@dokploy/server/constants"; import type { Compose } from "@dokploy/server/services/compose"; import type { ContainerInfo, ResourceRequirements } from "dockerode"; import { parse } from "dotenv"; +import { quote } from "shell-quote"; import type { ApplicationNested } from "../builders"; import type { MariadbNested } from "../databases/mariadb"; import type { MongoNested } from "../databases/mongo"; @@ -310,6 +311,21 @@ export const prepareEnvironmentVariables = ( return resolvedVars; }; +export const prepareEnvironmentVariablesForShell = ( + serviceEnv: string | null, + projectEnv?: string | null, + environmentEnv?: string | null, +): string[] => { + const envVars = prepareEnvironmentVariables( + serviceEnv, + projectEnv, + environmentEnv, + ); + // Using shell-quote library to properly escape shell arguments + // This is the standard way to handle special characters in shell commands + return envVars.map((env) => quote([env])); +}; + export const parseEnvironmentKeyValuePair = ( pair: string, ): [string, string] => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ba76d1b73..4b2df6957 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -726,6 +726,9 @@ importers: rotating-file-stream: specifier: 3.2.3 version: 3.2.3 + shell-quote: + specifier: ^1.8.1 + version: 1.8.2 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -778,6 +781,9 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@types/ssh2': specifier: 1.15.1 version: 1.15.1 @@ -4033,6 +4039,9 @@ packages: '@types/readable-stream@4.0.20': resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==} + '@types/shell-quote@1.7.5': + resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} + '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} @@ -11383,6 +11392,8 @@ snapshots: dependencies: '@types/node': 20.17.51 + '@types/shell-quote@1.7.5': {} + '@types/shimmer@1.2.0': {} '@types/ssh2@1.15.1': From fee802a57ba9dd02111ef8ce70fe988a5c3f577c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 21:18:13 -0600 Subject: [PATCH 57/79] refactor: remove outdated comment in railpack command builder - Removed a comment regarding the use of shell-quote for escaping export statements, as the functionality is now handled by the `prepareEnvironmentVariablesForShell` function introduced in a previous commit. --- packages/server/src/utils/builders/railpack.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 0d23b4cff..03d490cdf 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -76,7 +76,6 @@ export const getRailpackCommand = (application: ApplicationNested) => { const [key, value] = parseEnvironmentKeyValuePair(pair); if (key && value) { buildArgs.push("--secret", `id=${key},env=${key}`); - // Use shell-quote to properly escape the export statement exportEnvs.push(`export ${key}=${quote([value])}`); } } From 6413fa54e6733fe47c2aa8a2ccd3bb5dd2069538 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 19 Nov 2025 22:55:53 -0600 Subject: [PATCH 58/79] chore: add shell-quote dependency and its type definitions - Added `shell-quote` to dependencies for improved shell argument handling. - Included `@types/shell-quote` in devDependencies for TypeScript support. --- apps/dokploy/package.json | 2 ++ pnpm-lock.yaml | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index b519f3ea7..9f2229937 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -98,6 +98,7 @@ "bl": "6.0.11", "boxen": "^7.1.1", "bullmq": "5.4.2", + "shell-quote": "^1.8.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^0.2.1", @@ -157,6 +158,7 @@ "zod-form-data": "^2.0.7" }, "devDependencies": { + "@types/shell-quote": "^1.7.5", "@types/adm-zip": "^0.5.7", "@types/bcrypt": "5.0.2", "@types/js-cookie": "^3.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b2df6957..a03f77f4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -406,6 +406,9 @@ importers: rotating-file-stream: specifier: 3.2.3 version: 3.2.3 + shell-quote: + specifier: ^1.8.1 + version: 1.8.2 slugify: specifier: ^1.6.6 version: 1.6.6 @@ -488,6 +491,9 @@ importers: '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + '@types/shell-quote': + specifier: ^1.7.5 + version: 1.7.5 '@types/ssh2': specifier: 1.15.1 version: 1.15.1 From 7a0ff72f5146ea20140cef141a009da861aa9462 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 20 Nov 2025 08:43:24 -0600 Subject: [PATCH 59/79] fix: improve Docker command execution by including environment variable exports - Updated the Docker command execution to include environment variable exports directly in the command, enhancing the handling of environment variables during deployment. - Simplified the export command structure for better readability and efficiency. Fix https://github.com/Dokploy/dokploy/pull/3066#issuecomment-3558022350 --- packages/server/src/utils/builders/compose.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index d196cef04..fe5417ea5 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -53,9 +53,8 @@ Compose Type: ${composeType} ✅`; cd "${projectPath}"; - ${exportEnvCommand} ${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create --attachable ${compose.appName}` : ""} - env -i PATH="$PATH" docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } + env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; } ${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""} echo "Docker Compose Deployed: ✅"; @@ -66,7 +65,6 @@ Compose Type: ${composeType} ✅`; `; return bashCommand; - // return await execAsyncRemote(compose.serverId, bashCommand); }; const sanitizeCommand = (command: string) => { @@ -138,8 +136,8 @@ const getExportEnvCommand = (compose: ComposeNested) => { compose.environment.project.env, ); const exports = Object.entries(envVars) - .map(([key, value]) => `export ${key}=${quote([value])}`) - .join("\n"); + .map(([key, value]) => `${key}=${quote([value])}`) + .join(" "); - return exports ? `\n# Export environment variables\n${exports}\n` : ""; + return exports ? `${exports}` : ""; }; From ad0e044740a9a89c20289ed3292be88831605fb1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 20 Nov 2025 08:48:33 -0600 Subject: [PATCH 60/79] chore: bump version to v0.25.10 in package.json --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 9f2229937..e52968b15 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.25.9", + "version": "v0.25.10", "private": true, "license": "Apache-2.0", "type": "module", From e88a9ce96f0a37512fe70761e9cf6a0ab90be58c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 21 Nov 2025 11:58:23 -0500 Subject: [PATCH 61/79] fix: update input handling in application components - Modified the onChange event for input fields in AddApplication, AddCompose, and AddDatabase components to ensure proper trimming of whitespace from the input value before slugification. --- apps/dokploy/components/dashboard/project/add-application.tsx | 4 ++-- apps/dokploy/components/dashboard/project/add-compose.tsx | 4 ++-- apps/dokploy/components/dashboard/project/add-database.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 079701eb8..b0db46681 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -150,8 +150,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => { placeholder="Frontend" {...field} onChange={(e) => { - const val = e.target.value?.trim() || ""; - const serviceName = slugify(val); + const val = e.target.value || ""; + const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index a187104ec..bb911373f 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -161,8 +161,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => { placeholder="Frontend" {...field} onChange={(e) => { - const val = e.target.value?.trim() || ""; - const serviceName = slugify(val); + const val = e.target.value || ""; + const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index c0600a2d9..3176b9589 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -395,8 +395,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => { placeholder="Name" {...field} onChange={(e) => { - const val = e.target.value?.trim() || ""; - const serviceName = slugify(val); + const val = e.target.value || ""; + const serviceName = slugify(val.trim()); form.setValue("appName", `${slug}-${serviceName}`); field.onChange(val); }} From b12e84c645007266ae9ac0a9745337c1b2993f9a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 21 Nov 2025 15:02:17 -0500 Subject: [PATCH 62/79] feat: add SQL script to standardize date formats in environment table - Introduced a new SQL script to update the `createdAt` field in the `environment` table, converting PostgreSQL timestamp formats to ISO 8601 format. - This change addresses issue #2992, ensuring consistency in date formats across environments. - Updated journal to include the new migration tag for tracking purposes. --- apps/dokploy/drizzle/0121_rainy_cargill.sql | 9 + apps/dokploy/drizzle/meta/0121_snapshot.json | 6722 ++++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + 3 files changed, 6738 insertions(+) create mode 100644 apps/dokploy/drizzle/0121_rainy_cargill.sql create mode 100644 apps/dokploy/drizzle/meta/0121_snapshot.json diff --git a/apps/dokploy/drizzle/0121_rainy_cargill.sql b/apps/dokploy/drizzle/0121_rainy_cargill.sql new file mode 100644 index 000000000..85cfa8ceb --- /dev/null +++ b/apps/dokploy/drizzle/0121_rainy_cargill.sql @@ -0,0 +1,9 @@ +-- Fix inconsistent date formats in environment.createdAt field +-- Convert PostgreSQL timestamp format to ISO 8601 format +-- This addresses issue #2992 where old environments have PostgreSQL timestamp format +-- while new ones have ISO 8601 format + +UPDATE "environment" +SET "createdAt" = to_char("createdAt"::timestamptz, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') +WHERE "createdAt" NOT LIKE '%T%'; + diff --git a/apps/dokploy/drizzle/meta/0121_snapshot.json b/apps/dokploy/drizzle/meta/0121_snapshot.json new file mode 100644 index 000000000..52516ec29 --- /dev/null +++ b/apps/dokploy/drizzle/meta/0121_snapshot.json @@ -0,0 +1,6722 @@ +{ + "id": "6d1361fc-3a46-4016-b6db-42351c20393c", + "prevId": "bbe005b3-d5e0-4e93-8305-ba3b9a2e3f3d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "apikey_user_id_user_id_fk": { + "name": "apikey_user_id_user_id_fk", + "tableFrom": "apikey", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "columnsFrom": [ + "inviter_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteEnvironments": { + "name": "canDeleteEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateEnvironments": { + "name": "canCreateEnvironments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accessedEnvironments": { + "name": "accessedEnvironments", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + } + }, + "indexes": {}, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "columnsFrom": [ + "organization_id" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "organization_owner_id_user_id_fk": { + "name": "organization_owner_id_user_id_fk", + "tableFrom": "organization", + "columnsFrom": [ + "owner_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai": { + "name": "ai", + "schema": "", + "columns": { + "aiId": { + "name": "aiId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiUrl": { + "name": "apiUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isEnabled": { + "name": "isEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ai_organizationId_organization_id_fk": { + "name": "ai_organizationId_organization_id_fk", + "tableFrom": "ai", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildSecrets": { + "name": "previewBuildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLabels": { + "name": "previewLabels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewCustomCertResolver": { + "name": "previewCustomCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewRequireCollaboratorPermissions": { + "name": "previewRequireCollaboratorPermissions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rollbackActive": { + "name": "rollbackActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildSecrets": { + "name": "buildSecrets", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "cleanCache": { + "name": "cleanCache", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBuildPath": { + "name": "giteaBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "railpackVersion": { + "name": "railpackVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0.2.2'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isStaticSpa": { + "name": "isStaticSpa", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "tableTo": "ssh-key", + "columnsTo": [ + "sshKeyId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "columnsFrom": [ + "registryId" + ], + "tableTo": "registry", + "columnsTo": [ + "registryId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_environmentId_environment_environmentId_fk": { + "name": "application_environmentId_environment_environmentId_fk", + "tableFrom": "application", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "columnsFrom": [ + "githubId" + ], + "tableTo": "github", + "columnsTo": [ + "githubId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "columnsFrom": [ + "gitlabId" + ], + "tableTo": "gitlab", + "columnsTo": [ + "gitlabId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_giteaId_gitea_giteaId_fk": { + "name": "application_giteaId_gitea_giteaId_fk", + "tableFrom": "application", + "columnsFrom": [ + "giteaId" + ], + "tableTo": "gitea", + "columnsTo": [ + "giteaId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "columnsFrom": [ + "bitbucketId" + ], + "tableTo": "bitbucket", + "columnsTo": [ + "bitbucketId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "backupType": { + "name": "backupType", + "type": "backupType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'database'" + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "destinationId" + ], + "tableTo": "destination", + "columnsTo": [ + "destinationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_composeId_compose_composeId_fk": { + "name": "backup_composeId_compose_composeId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "postgresId" + ], + "tableTo": "postgres", + "columnsTo": [ + "postgresId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "mariadbId" + ], + "tableTo": "mariadb", + "columnsTo": [ + "mariadbId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "mysqlId" + ], + "tableTo": "mysql", + "columnsTo": [ + "mysqlId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "columnsFrom": [ + "mongoId" + ], + "tableTo": "mongo", + "columnsTo": [ + "mongoId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_userId_user_id_fk": { + "name": "backup_userId_user_id_fk", + "tableFrom": "backup", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "backup_appName_unique": { + "name": "backup_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketEmail": { + "name": "bitbucketEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apiToken": { + "name": "apiToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_organizationId_organization_id_fk": { + "name": "certificate_organizationId_organization_id_fk", + "tableFrom": "certificate", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "columns": [ + "certificatePath" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaRepository": { + "name": "giteaRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaOwner": { + "name": "giteaOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaBranch": { + "name": "giteaBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "enableSubmodules": { + "name": "enableSubmodules", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeployment": { + "name": "isolatedDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isolatedDeploymentsVolume": { + "name": "isolatedDeploymentsVolume", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggerType": { + "name": "triggerType", + "type": "triggerType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'push'" + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "watchPaths": { + "name": "watchPaths", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "tableTo": "ssh-key", + "columnsTo": [ + "sshKeyId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_environmentId_environment_environmentId_fk": { + "name": "compose_environmentId_environment_environmentId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "githubId" + ], + "tableTo": "github", + "columnsTo": [ + "githubId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "gitlabId" + ], + "tableTo": "gitlab", + "columnsTo": [ + "gitlabId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "bitbucketId" + ], + "tableTo": "bitbucket", + "columnsTo": [ + "bitbucketId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_giteaId_gitea_giteaId_fk": { + "name": "compose_giteaId_gitea_giteaId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "giteaId" + ], + "tableTo": "gitea", + "columnsTo": [ + "giteaId" + ], + "onUpdate": "no action", + "onDelete": "set null" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pid": { + "name": "pid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startedAt": { + "name": "startedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finishedAt": { + "name": "finishedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "previewDeploymentId" + ], + "tableTo": "preview_deployments", + "columnsTo": [ + "previewDeploymentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_scheduleId_schedule_scheduleId_fk": { + "name": "deployment_scheduleId_schedule_scheduleId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "scheduleId" + ], + "tableTo": "schedule", + "columnsTo": [ + "scheduleId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_backupId_backup_backupId_fk": { + "name": "deployment_backupId_backup_backupId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "backupId" + ], + "tableTo": "backup", + "columnsTo": [ + "backupId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "rollbackId" + ], + "tableTo": "rollback", + "columnsTo": [ + "rollbackId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": { + "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk", + "tableFrom": "deployment", + "columnsFrom": [ + "volumeBackupId" + ], + "tableTo": "volume_backup", + "columnsTo": [ + "volumeBackupId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "destination_organizationId_organization_id_fk": { + "name": "destination_organizationId_organization_id_fk", + "tableFrom": "destination", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customCertResolver": { + "name": "customCertResolver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "internalPath": { + "name": "internalPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "stripPath": { + "name": "stripPath", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "columnsFrom": [ + "previewDeploymentId" + ], + "tableTo": "preview_deployments", + "columnsTo": [ + "previewDeploymentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "environment_projectId_project_projectId_fk": { + "name": "environment_projectId_project_projectId_fk", + "tableFrom": "environment", + "columnsFrom": [ + "projectId" + ], + "tableTo": "project", + "columnsTo": [ + "projectId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_organizationId_organization_id_fk": { + "name": "git_provider_organizationId_organization_id_fk", + "tableFrom": "git_provider", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "git_provider_userId_user_id_fk": { + "name": "git_provider_userId_user_id_fk", + "tableFrom": "git_provider", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitea": { + "name": "gitea", + "schema": "", + "columns": { + "giteaId": { + "name": "giteaId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "giteaUrl": { + "name": "giteaUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitea.com'" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'repo,repo:status,read:user,read:org'" + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "gitea_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitea_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitea", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "columnsFrom": [ + "gitProviderId" + ], + "tableTo": "git_provider", + "columnsTo": [ + "gitProviderId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_environmentId_environment_environmentId_fk": { + "name": "mariadb_environmentId_environment_environmentId_fk", + "tableFrom": "mariadb", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replicaSets": { + "name": "replicaSets", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_environmentId_environment_environmentId_fk": { + "name": "mongo_environmentId_environment_environmentId_fk", + "tableFrom": "mongo", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "postgresId" + ], + "tableTo": "postgres", + "columnsTo": [ + "postgresId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "mariadbId" + ], + "tableTo": "mariadb", + "columnsTo": [ + "mariadbId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "mongoId" + ], + "tableTo": "mongo", + "columnsTo": [ + "mongoId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "mysqlId" + ], + "tableTo": "mysql", + "columnsTo": [ + "mysqlId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "redisId" + ], + "tableTo": "redis", + "columnsTo": [ + "redisId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_environmentId_environment_environmentId_fk": { + "name": "mysql_environmentId_environment_environmentId_fk", + "tableFrom": "mysql", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gotify": { + "name": "gotify", + "schema": "", + "columns": { + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appToken": { + "name": "appToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.lark": { + "name": "lark", + "schema": "", + "columns": { + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "serverThreshold": { + "name": "serverThreshold", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gotifyId": { + "name": "gotifyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "larkId": { + "name": "larkId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "slackId" + ], + "tableTo": "slack", + "columnsTo": [ + "slackId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "telegramId" + ], + "tableTo": "telegram", + "columnsTo": [ + "telegramId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "discordId" + ], + "tableTo": "discord", + "columnsTo": [ + "discordId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "emailId" + ], + "tableTo": "email", + "columnsTo": [ + "emailId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_gotifyId_gotify_gotifyId_fk": { + "name": "notification_gotifyId_gotify_gotifyId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "gotifyId" + ], + "tableTo": "gotify", + "columnsTo": [ + "gotifyId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_ntfyId_ntfy_ntfyId_fk": { + "name": "notification_ntfyId_ntfy_ntfyId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "ntfyId" + ], + "tableTo": "ntfy", + "columnsTo": [ + "ntfyId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_larkId_lark_larkId_fk": { + "name": "notification_larkId_lark_larkId_fk", + "tableFrom": "notification", + "columnsFrom": [ + "larkId" + ], + "tableTo": "lark", + "columnsTo": [ + "larkId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "notification_organizationId_organization_id_fk": { + "name": "notification_organizationId_organization_id_fk", + "tableFrom": "notification", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ntfy": { + "name": "ntfy", + "schema": "", + "columns": { + "ntfyId": { + "name": "ntfyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverUrl": { + "name": "serverUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "messageThreadId": { + "name": "messageThreadId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "publishMode": { + "name": "publishMode", + "type": "publishModeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'host'" + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_environmentId_environment_environmentId_fk": { + "name": "postgres_environmentId_environment_environmentId_fk", + "tableFrom": "postgres", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "columnsFrom": [ + "domainId" + ], + "tableTo": "domain", + "columnsTo": [ + "domainId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organizationId_organization_id_fk": { + "name": "project_organizationId_organization_id_fk", + "tableFrom": "project", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "stopGracePeriodSwarm": { + "name": "stopGracePeriodSwarm", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "endpointSpecSwarm": { + "name": "endpointSpecSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "environmentId": { + "name": "environmentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_environmentId_environment_environmentId_fk": { + "name": "redis_environmentId_environment_environmentId_fk", + "tableFrom": "redis", + "columnsFrom": [ + "environmentId" + ], + "tableTo": "environment", + "columnsTo": [ + "environmentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "columns": [ + "appName" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_organizationId_organization_id_fk": { + "name": "registry_organizationId_organization_id_fk", + "tableFrom": "registry", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollback": { + "name": "rollback", + "schema": "", + "columns": { + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fullContext": { + "name": "fullContext", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", + "tableFrom": "rollback", + "columnsFrom": [ + "deploymentId" + ], + "tableTo": "deployment", + "columnsTo": [ + "deploymentId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule": { + "name": "schedule", + "schema": "", + "columns": { + "scheduleId": { + "name": "scheduleId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shellType": { + "name": "shellType", + "type": "shellType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'bash'" + }, + "scheduleType": { + "name": "scheduleType", + "type": "scheduleType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script": { + "name": "script", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "schedule_applicationId_application_applicationId_fk": { + "name": "schedule_applicationId_application_applicationId_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schedule_composeId_compose_composeId_fk": { + "name": "schedule_composeId_compose_composeId_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schedule_serverId_server_serverId_fk": { + "name": "schedule_serverId_server_serverId_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "serverId" + ], + "tableTo": "server", + "columnsTo": [ + "serverId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "schedule_userId_user_id_fk": { + "name": "schedule_userId_user_id_fk", + "tableFrom": "schedule", + "columnsFrom": [ + "userId" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "columns": [ + "username", + "applicationId" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "server_organizationId_organization_id_fk": { + "name": "server_organizationId_organization_id_fk", + "tableFrom": "server", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "columnsFrom": [ + "sshKeyId" + ], + "tableTo": "ssh-key", + "columnsTo": [ + "sshKeyId" + ], + "onUpdate": "no action", + "onDelete": "set null" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session_temp": { + "name": "session_temp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_temp_user_id_user_id_fk": { + "name": "session_temp_user_id_user_id_fk", + "tableFrom": "session_temp", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_temp_token_unique": { + "name": "session_temp_token_unique", + "columns": [ + "token" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organizationId": { + "name": "organizationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_organizationId_organization_id_fk": { + "name": "ssh-key_organizationId_organization_id_fk", + "tableFrom": "ssh-key", + "columnsFrom": [ + "organizationId" + ], + "tableTo": "organization", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "logCleanupCron": { + "name": "logCleanupCron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 0 * * *'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "enablePaidFeatures": { + "name": "enablePaidFeatures", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowImpersonation": { + "name": "allowImpersonation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "metricsConfig": { + "name": "metricsConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb" + }, + "cleanupCacheApplications": { + "name": "cleanupCacheApplications", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnPreviews": { + "name": "cleanupCacheOnPreviews", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cleanupCacheOnCompose": { + "name": "cleanupCacheOnCompose", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.volume_backup": { + "name": "volume_backup", + "schema": "", + "columns": { + "volumeBackupId": { + "name": "volumeBackupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "turnOff": { + "name": "turnOff", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cronExpression": { + "name": "cronExpression", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keepLatestCount": { + "name": "keepLatestCount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "volume_backup_applicationId_application_applicationId_fk": { + "name": "volume_backup_applicationId_application_applicationId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "applicationId" + ], + "tableTo": "application", + "columnsTo": [ + "applicationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_postgresId_postgres_postgresId_fk": { + "name": "volume_backup_postgresId_postgres_postgresId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "postgresId" + ], + "tableTo": "postgres", + "columnsTo": [ + "postgresId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_mariadbId_mariadb_mariadbId_fk": { + "name": "volume_backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "mariadbId" + ], + "tableTo": "mariadb", + "columnsTo": [ + "mariadbId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_mongoId_mongo_mongoId_fk": { + "name": "volume_backup_mongoId_mongo_mongoId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "mongoId" + ], + "tableTo": "mongo", + "columnsTo": [ + "mongoId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_mysqlId_mysql_mysqlId_fk": { + "name": "volume_backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "mysqlId" + ], + "tableTo": "mysql", + "columnsTo": [ + "mysqlId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_redisId_redis_redisId_fk": { + "name": "volume_backup_redisId_redis_redisId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "redisId" + ], + "tableTo": "redis", + "columnsTo": [ + "redisId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_composeId_compose_composeId_fk": { + "name": "volume_backup_composeId_compose_composeId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "composeId" + ], + "tableTo": "compose", + "columnsTo": [ + "composeId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "volume_backup_destinationId_destination_destinationId_fk": { + "name": "volume_backup_destinationId_destination_destinationId_fk", + "tableFrom": "volume_backup", + "columnsFrom": [ + "destinationId" + ], + "tableTo": "destination", + "columnsTo": [ + "destinationId" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + "railpack" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "drop" + ] + }, + "public.backupType": { + "name": "backupType", + "schema": "public", + "values": [ + "database", + "compose" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo", + "web-server" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "gitea", + "raw" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error", + "cancelled" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket", + "gitea" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email", + "gotify", + "ntfy", + "lark" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.publishModeType": { + "name": "publishModeType", + "schema": "public", + "values": [ + "ingress", + "host" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.scheduleType": { + "name": "scheduleType", + "schema": "public", + "values": [ + "application", + "compose", + "server", + "dokploy-server" + ] + }, + "public.shellType": { + "name": "shellType", + "schema": "public", + "values": [ + "bash", + "sh" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none", + "custom" + ] + }, + "public.triggerType": { + "name": "triggerType", + "schema": "public", + "values": [ + "push", + "tag" + ] + } + }, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index c51c28816..323562fd1 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -848,6 +848,13 @@ "when": 1762632540024, "tag": "0120_lame_captain_midlands", "breakpoints": true + }, + { + "idx": 121, + "version": "7", + "when": 1763755037033, + "tag": "0121_rainy_cargill", + "breakpoints": true } ] } \ No newline at end of file From 4840abe3a4f54ecc29939097286ce010d3c87ffd Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 22 Nov 2025 09:55:35 -0500 Subject: [PATCH 63/79] Specify Docker version in installation script --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 11310b18e..ae8c997f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules # Install docker -RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash +RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version 28.5.2 && rm get-docker.sh && curl https://rclone.org/install.sh | bash # Install Nixpacks and tsx # | VERBOSE=1 VERSION=1.21.0 bash From e136934cbc22c669291e5af3cc52a2cffd89e8a7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:02:52 -0500 Subject: [PATCH 64/79] Fix newline at end of .env.example file --- apps/dokploy/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/.env.example b/apps/dokploy/.env.example index ba57ec7be..8f801196e 100644 --- a/apps/dokploy/.env.example +++ b/apps/dokploy/.env.example @@ -1,3 +1,3 @@ DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy" PORT=3000 -NODE_ENV=development \ No newline at end of file +NODE_ENV=development From 73d87c06e16a75c14fd466a79ae89792050d81c7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 00:11:43 -0500 Subject: [PATCH 65/79] feat: enhance error handling in deployment processes - Introduced a new ExecError class to standardize error handling during command execution. - Updated deployApplication and deployCompose functions to log detailed error messages, excluding ExecError instances. - Improved execAsync and execAsyncRemote functions to throw ExecError with additional context for better debugging. - Added base64 encoding for error messages to ensure sensitive information is handled appropriately. --- packages/server/src/services/application.ts | 28 ++++- packages/server/src/services/compose.ts | 32 ++++++ packages/server/src/services/settings.ts | 5 +- .../server/src/utils/process/ExecError.ts | 55 ++++++++++ .../server/src/utils/process/execAsync.ts | 103 +++++++++++++++--- packages/server/src/utils/providers/github.ts | 1 - 6 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 packages/server/src/utils/process/ExecError.ts diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 31f015c0c..6ad90a93b 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -13,6 +13,7 @@ import { import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { + ExecError, execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; @@ -28,6 +29,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { createTraefikConfig } from "@dokploy/server/utils/traefik/application"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeployment, @@ -228,7 +230,16 @@ export const deployApplication = async ({ environmentName: application.environment.name, }); } catch (error) { - const command = `echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};`; + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; if (application.serverId) { await execAsyncRemote(application.serverId, command); } else { @@ -317,6 +328,21 @@ export const rebuildApplication = async ({ environmentName: application.environment.name, }); } catch (error) { + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (application.serverId) { + await execAsyncRemote(application.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); throw error; diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 1d4a7e5c9..89a12a156 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -18,6 +18,7 @@ import type { ComposeSpecification } from "@dokploy/server/utils/docker/types"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { + ExecError, execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; @@ -32,6 +33,7 @@ import { cloneGitlabRepository } from "@dokploy/server/utils/providers/gitlab"; import { getCreateComposeFileCommand } from "@dokploy/server/utils/providers/raw"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { encodeBase64 } from "../utils/docker/utils"; import { getDokployUrl } from "./admin"; import { createDeploymentCompose, @@ -270,6 +272,21 @@ export const deployCompose = async ({ environmentName: compose.environment.name, }); } catch (error) { + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateCompose(composeId, { composeStatus: "error", @@ -342,6 +359,21 @@ export const rebuildCompose = async ({ composeStatus: "done", }); } catch (error) { + let command = ""; + + // Only log details for non-ExecError errors + if (!(error instanceof ExecError)) { + const message = error instanceof Error ? error.message : String(error); + const encodedMessage = encodeBase64(message); + command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`; + } + + command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } await updateDeploymentStatus(deployment.deploymentId, "error"); await updateCompose(composeId, { composeStatus: "error", diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 996aec352..23d11b09b 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -60,10 +60,7 @@ export const getUpdateData = async (): Promise => { try { currentDigest = await getServiceImageDigest(); } catch (error) { - console.error(error); - // Docker service might not exist locally - // You can run the # Installation command for docker service create mentioned in the below docs to test it locally: - // https://docs.dokploy.com/docs/core/manual-installation + // TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version. return DEFAULT_UPDATE_DATA; } diff --git a/packages/server/src/utils/process/ExecError.ts b/packages/server/src/utils/process/ExecError.ts new file mode 100644 index 000000000..773968b5c --- /dev/null +++ b/packages/server/src/utils/process/ExecError.ts @@ -0,0 +1,55 @@ +export interface ExecErrorDetails { + command: string; + stdout?: string; + stderr?: string; + exitCode?: number; + originalError?: Error; + serverId?: string | null; +} + +export class ExecError extends Error { + public readonly command: string; + public readonly stdout?: string; + public readonly stderr?: string; + public readonly exitCode?: number; + public readonly originalError?: Error; + public readonly serverId?: string | null; + + constructor(message: string, details: ExecErrorDetails) { + super(message); + this.name = "ExecError"; + this.command = details.command; + this.stdout = details.stdout; + this.stderr = details.stderr; + this.exitCode = details.exitCode; + this.originalError = details.originalError; + this.serverId = details.serverId; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ExecError); + } + } + + /** + * Get a formatted error message with all details + */ + getDetailedMessage(): string { + const parts = [ + `Command: ${this.command}`, + this.exitCode !== undefined ? `Exit Code: ${this.exitCode}` : null, + this.serverId ? `Server ID: ${this.serverId}` : "Location: Local", + this.stderr ? `Stderr: ${this.stderr}` : null, + this.stdout ? `Stdout: ${this.stdout}` : null, + ].filter(Boolean); + + return `${this.message}\n${parts.join("\n")}`; + } + + /** + * Check if this error is from a remote execution + */ + isRemote(): boolean { + return !!this.serverId; + } +} diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index 13b06c6c4..0838e099f 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -2,8 +2,43 @@ import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; +import { ExecError } from "./ExecError"; -export const execAsync = util.promisify(exec); +// Re-export ExecError for easier imports +export { ExecError } from "./ExecError"; + +const execAsyncBase = util.promisify(exec); + +export const execAsync = async ( + command: string, + options?: { cwd?: string; env?: NodeJS.ProcessEnv }, +): Promise<{ stdout: string; stderr: string }> => { + try { + const result = await execAsyncBase(command, options); + return { + stdout: result.stdout.toString(), + stderr: result.stderr.toString(), + }; + } catch (error) { + if (error instanceof Error) { + // @ts-ignore - exec error has these properties + const exitCode = error.code; + // @ts-ignore + const stdout = error.stdout?.toString() || ""; + // @ts-ignore + const stderr = error.stderr?.toString() || ""; + + throw new ExecError(`Command execution failed: ${error.message}`, { + command, + stdout, + stderr, + exitCode, + originalError: error, + }); + } + throw error; + } +}; interface ExecOptions { cwd?: string; @@ -21,7 +56,16 @@ export const execAsyncStream = ( const childProcess = exec(command, options, (error) => { if (error) { - reject(error); + reject( + new ExecError(`Command execution failed: ${error.message}`, { + command, + stdout: stdoutComplete, + stderr: stderrComplete, + // @ts-ignore + exitCode: error.code, + originalError: error, + }), + ); return; } resolve({ stdout: stdoutComplete, stderr: stderrComplete }); @@ -45,7 +89,14 @@ export const execAsyncStream = ( childProcess.on("error", (error) => { console.log(error); - reject(error); + reject( + new ExecError(`Command execution error: ${error.message}`, { + command, + stdout: stdoutComplete, + stderr: stderrComplete, + originalError: error, + }), + ); }); }); }; @@ -108,7 +159,14 @@ export const execAsyncRemote = async ( conn.exec(command, (err, stream) => { if (err) { onData?.(err.message); - throw err; + reject( + new ExecError(`Remote command execution failed: ${err.message}`, { + command, + serverId, + originalError: err, + }), + ); + return; } stream .on("close", (code: number, _signal: string) => { @@ -116,7 +174,18 @@ export const execAsyncRemote = async ( if (code === 0) { resolve({ stdout, stderr }); } else { - reject(new Error(`Error occurred ❌: ${stderr}`)); + reject( + new ExecError( + `Remote command failed with exit code ${code}`, + { + command, + stdout, + stderr, + exitCode: code, + serverId, + }, + ), + ); } }) .on("data", (data: string) => { @@ -132,17 +201,25 @@ export const execAsyncRemote = async ( .on("error", (err) => { conn.end(); if (err.level === "client-authentication") { - onData?.( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ); + const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`; + onData?.(errorMsg); reject( - new Error( - `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, - ), + new ExecError(errorMsg, { + command, + serverId, + originalError: err, + }), ); } else { - onData?.(`SSH connection error: ${err.message}`); - reject(new Error(`SSH connection error: ${err.message}`)); + const errorMsg = `SSH connection error: ${err.message}`; + onData?.(errorMsg); + reject( + new ExecError(errorMsg, { + command, + serverId, + originalError: err, + }), + ); } }) .connect({ diff --git a/packages/server/src/utils/providers/github.ts b/packages/server/src/utils/providers/github.ts index c0f1b651a..5b7763df7 100644 --- a/packages/server/src/utils/providers/github.ts +++ b/packages/server/src/utils/providers/github.ts @@ -159,7 +159,6 @@ export const cloneGithubRepository = async ({ const octokit = authGithub(githubProvider); const token = await getGithubToken(octokit); const repoclone = `github.com/${owner}/${repository}.git`; - // await recreateDirectory(outputPath); command += `rm -rf ${outputPath};`; command += `mkdir -p ${outputPath};`; const cloneUrl = `https://oauth2:${token}@${repoclone}`; From dbe1733dcb5f144cc8080415ad17893ea22fb48d Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 00:20:39 -0500 Subject: [PATCH 66/79] refactor: update execAsync options type for improved flexibility - Enhanced the options parameter in execAsync to accept ObjectEncodingOptions and ExecOptions, allowing for more versatile command execution configurations. --- packages/server/src/utils/process/execAsync.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index 0838e099f..c3421446d 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -1,4 +1,5 @@ import { exec, execFile } from "node:child_process"; +import type { ObjectEncodingOptions } from "node:fs"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; @@ -11,7 +12,7 @@ const execAsyncBase = util.promisify(exec); export const execAsync = async ( command: string, - options?: { cwd?: string; env?: NodeJS.ProcessEnv }, + options?: ObjectEncodingOptions & ExecOptions, ): Promise<{ stdout: string; stderr: string }> => { try { const result = await execAsyncBase(command, options); From 8aff1e761421995ec9d86662404ac753e3944d35 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 00:50:51 -0500 Subject: [PATCH 67/79] refactor: simplify execAsync options type for better clarity - Updated the options parameter in execAsync to a more streamlined type, focusing on cwd, env, and shell properties for improved clarity and usability. --- packages/server/src/utils/process/execAsync.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index c3421446d..cd0249000 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -1,5 +1,4 @@ import { exec, execFile } from "node:child_process"; -import type { ObjectEncodingOptions } from "node:fs"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; @@ -12,7 +11,7 @@ const execAsyncBase = util.promisify(exec); export const execAsync = async ( command: string, - options?: ObjectEncodingOptions & ExecOptions, + options?: { cwd?: string; env?: NodeJS.ProcessEnv; shell?: string }, ): Promise<{ stdout: string; stderr: string }> => { try { const result = await execAsyncBase(command, options); From 965f05c7c86156d62deec13ecacf007d1da9b182 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 01:12:39 -0500 Subject: [PATCH 68/79] refactor: improve cleanup operation handling in postgres router - Changed cleanup operations to use async functions for better error handling. - Replaced Promise.allSettled with a for loop to individually await each operation, allowing for more granular error management. --- apps/dokploy/server/api/routers/postgres.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index a05072ab7..96485d637 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -282,12 +282,16 @@ export const postgresRouter = createTRPCRouter({ const backups = await findBackupsByDbId(input.postgresId, "postgres"); const cleanupOperations = [ - removeService(postgres.appName, postgres.serverId), - cancelJobs(backups), - removePostgresById(input.postgresId), + async () => await removeService(postgres?.appName, postgres.serverId), + async () => await cancelJobs(backups), + async () => await removePostgresById(input.postgresId), ]; - await Promise.allSettled(cleanupOperations); + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (_) {} + } return postgres; }), From 23b235303ccdb72cb505270f55864570763b272c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 01:20:40 -0500 Subject: [PATCH 69/79] refactor: move getMountPath function to services and update logic - Moved the getMountPath function from the postgres router to the postgres service for better organization. - Updated the logic to return the correct mount path based on the PostgreSQL version, ensuring compatibility with versions below 18. --- apps/dokploy/server/api/routers/postgres.ts | 8 +------- packages/server/src/services/postgres.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 67faed6c6..8503384e2 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -9,6 +9,7 @@ import { findMountsByApplicationId, findPostgresById, findProjectById, + getMountPath, IS_CLOUD, rebuildDatabase, removePostgresById, @@ -40,13 +41,6 @@ import { } from "@/server/db/schema"; import { cancelJobs } from "@/server/utils/backup"; -function getMountPath(dockerImage: string): string { - const versionMatch = dockerImage.match(/postgres:(\d+)/); - const version = versionMatch?.[1] ? Number.parseInt(versionMatch[1], 10) : 18; - - return `/var/lib/postgresql/${version}/data`; -} - export const postgresRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreatePostgres) diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts index 0d900443e..d7016038a 100644 --- a/packages/server/src/services/postgres.ts +++ b/packages/server/src/services/postgres.ts @@ -13,6 +13,18 @@ import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; import { validUniqueServerAppName } from "./project"; +export function getMountPath(dockerImage: string): string { + const versionMatch = dockerImage.match(/postgres:(\d+)/); + + if (versionMatch?.[1]) { + const version = Number.parseInt(versionMatch[1], 10); + if (version >= 18) { + return `/var/lib/postgresql/${version}/data`; + } + } + return "/var/lib/postgresql/data"; +} + export type Postgres = typeof postgres.$inferSelect; export const createPostgres = async (input: typeof apiCreatePostgres._type) => { From 736a7320d4ffa2a7ba27967be0bf744d5abe2bd1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 01:21:56 -0500 Subject: [PATCH 70/79] refactor: remove unused mount-related logic from postgres router - Removed the findMountsByApplicationId and updateMount functions from the postgres router as they are no longer needed after the recent refactor of the getMountPath function. - Cleaned up the code to streamline the update process for PostgreSQL instances. --- apps/dokploy/server/api/routers/postgres.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 8503384e2..6cb88385f 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -6,7 +6,6 @@ import { deployPostgres, findBackupsByDbId, findEnvironmentById, - findMountsByApplicationId, findPostgresById, findProjectById, getMountPath, @@ -18,7 +17,6 @@ import { startServiceRemote, stopService, stopServiceRemote, - updateMount, updatePostgresById, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; @@ -370,24 +368,6 @@ export const postgresRouter = createTRPCRouter({ }); } - if (rest.dockerImage) { - const mountPath = getMountPath(rest.dockerImage); - const mounts = await findMountsByApplicationId(postgresId, "postgres"); - if (!mounts || mounts.length === 0) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Mount not found for this Postgres", - }); - } - for (const mount of mounts) { - if (mount.mountPath.startsWith("/var/lib/postgresql")) { - await updateMount(mount.mountId, { - mountPath: mountPath, - }); - } - } - } - const service = await updatePostgresById(postgresId, { ...rest, }); From cd8c67bb9bb32a88c9a375d159235126c51fe560 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:00:14 -0500 Subject: [PATCH 71/79] feat: add additional rclone configuration options for S3 integration - Introduced new rclone flags: --retries, --low-level-retries, --timeout, and --contimeout to enhance S3 operations. - These options aim to improve reliability and performance during file transfers. --- apps/dokploy/server/api/routers/destination.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index dc5892c85..aec5f1251 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -53,6 +53,10 @@ export const destinationRouter = createTRPCRouter({ `--s3-endpoint=${endpoint}`, "--s3-no-check-bucket", "--s3-force-path-style", + "--retries 1", + "--low-level-retries 1", + "--timeout 10s", + "--contimeout 5s", ]; if (provider) { rcloneFlags.unshift(`--s3-provider=${provider}`); From 7da1be877b5f621e286995b30220fac8c857347c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:04:15 -0500 Subject: [PATCH 72/79] fix: update rclone S3 flags to use quotes for improved parsing - Added quotes around S3 configuration options in rclone flags to ensure proper handling of special characters and spaces. - This change enhances the reliability of the S3 integration by preventing potential parsing issues. --- apps/dokploy/server/api/routers/destination.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/server/api/routers/destination.ts b/apps/dokploy/server/api/routers/destination.ts index aec5f1251..d7cbf53d5 100644 --- a/apps/dokploy/server/api/routers/destination.ts +++ b/apps/dokploy/server/api/routers/destination.ts @@ -47,10 +47,10 @@ export const destinationRouter = createTRPCRouter({ input; try { const rcloneFlags = [ - `--s3-access-key-id=${accessKey}`, - `--s3-secret-access-key=${secretAccessKey}`, - `--s3-region=${region}`, - `--s3-endpoint=${endpoint}`, + `--s3-access-key-id="${accessKey}"`, + `--s3-secret-access-key="${secretAccessKey}"`, + `--s3-region="${region}"`, + `--s3-endpoint="${endpoint}"`, "--s3-no-check-bucket", "--s3-force-path-style", "--retries 1", @@ -59,7 +59,7 @@ export const destinationRouter = createTRPCRouter({ "--contimeout 5s", ]; if (provider) { - rcloneFlags.unshift(`--s3-provider=${provider}`); + rcloneFlags.unshift(`--s3-provider="${provider}"`); } const rcloneDestination = `:s3:${bucket}`; const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`; From 891260fe4110a33039e9bcd92b50ca8f010d8949 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:09:06 -0500 Subject: [PATCH 73/79] fix: correct GitLab API URL by removing 'owned' parameter from project fetch request --- packages/server/src/utils/providers/gitlab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index a3106b8c0..3343c9bb6 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -290,7 +290,7 @@ export const validateGitlabProvider = async (gitlabProvider: Gitlab) => { while (true) { const response = await fetch( - `${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&owned=true&page=${page}&per_page=${perPage}`, + `${gitlabProvider.gitlabUrl}/api/v4/projects?membership=true&page=${page}&per_page=${perPage}`, { headers: { Authorization: `Bearer ${gitlabProvider.accessToken}`, From 783324628f0b9d5d0dc88a5c26002fee5a758ec9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:23:12 -0500 Subject: [PATCH 74/79] refactor: enhance user management actions in dashboard - Updated the user action dropdown to conditionally display options based on user role, ensuring that only non-owner users can access certain actions. - Improved the delete and unlink user functionalities with better error handling and success notifications. - Streamlined the code for clarity and maintainability. --- .../dashboard/settings/users/show-users.tsx | 190 +++++++++--------- 1 file changed, 92 insertions(+), 98 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index 51d8704a3..c78cc8a03 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -111,35 +111,75 @@ export const ShowUsers = () => { - - - - - - - Actions - + {member.role !== "owner" && ( + + + + + + + Actions + - {member.role !== "owner" && ( - )} - {member.role !== "owner" && ( - <> - {!isCloud && ( - { + {!isCloud && ( + { + await mutateAsync({ + userId: member.user.id, + }) + .then(() => { + toast.success( + "User deleted successfully", + ); + refetch(); + }) + .catch(() => { + toast.error( + "Error deleting destination", + ); + }); + }} + > + e.preventDefault()} + > + Delete User + + + )} + + { + if (!isCloud) { + const orgCount = + await utils.user.checkUserOrganizations.fetch( + { + userId: member.user.id, + }, + ); + + console.log(orgCount); + + if (orgCount === 1) { await mutateAsync({ userId: member.user.id, }) @@ -151,86 +191,40 @@ export const ShowUsers = () => { }) .catch(() => { toast.error( - "Error deleting destination", + "Error deleting user", ); }); - }} - > - - e.preventDefault() - } - > - Delete User - - - )} - - { - if (!isCloud) { - const orgCount = - await utils.user.checkUserOrganizations.fetch( - { - userId: member.user.id, - }, - ); - - console.log(orgCount); - - if (orgCount === 1) { - await mutateAsync({ - userId: member.user.id, - }) - .then(() => { - toast.success( - "User deleted successfully", - ); - refetch(); - }) - .catch(() => { - toast.error( - "Error deleting user", - ); - }); - return; - } + return; } + } - const { error } = - await authClient.organization.removeMember( - { - memberIdOrEmail: member.id, - }, - ); + const { error } = + await authClient.organization.removeMember( + { + memberIdOrEmail: member.id, + }, + ); - if (!error) { - toast.success( - "User unlinked successfully", - ); - refetch(); - } else { - toast.error( - "Error unlinking user", - ); - } - }} + if (!error) { + toast.success( + "User unlinked successfully", + ); + refetch(); + } else { + toast.error("Error unlinking user"); + } + }} + > + e.preventDefault()} > - e.preventDefault()} - > - Unlink User - - - - )} - - + Unlink User + + + + + )} ); From 45b75fdfdee273a5c2f58e5ab6a54f9c63a44ea3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:24:52 -0500 Subject: [PATCH 75/79] refactor: remove TableCaption from user display in dashboard settings --- apps/dokploy/components/dashboard/settings/users/show-users.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index c78cc8a03..06c94416b 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -21,7 +21,6 @@ import { import { Table, TableBody, - TableCaption, TableCell, TableHead, TableHeader, @@ -68,7 +67,6 @@ export const ShowUsers = () => { ) : (
- See all users Email From bacadccaa93be444ff607204e4df1754ec902e98 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:39:01 -0500 Subject: [PATCH 76/79] refactor: improve error handling in notification components - Enhanced error messages in HandleNotifications and notificationRouter to provide more specific feedback. - Updated email and Discord notification functions to throw detailed errors on failure. - Ensured consistent error handling across notification utilities for better debugging. --- .../notifications/handle-notifications.tsx | 6 +- .../server/api/routers/notification.ts | 6 +- .../src/utils/notifications/build-error.ts | 524 ++++++++-------- .../src/utils/notifications/build-success.ts | 547 ++++++++--------- .../utils/notifications/database-backup.ts | 571 +++++++++--------- .../src/utils/notifications/docker-cleanup.ts | 329 +++++----- .../utils/notifications/dokploy-restart.ts | 69 +-- .../server/src/utils/notifications/utils.ts | 43 +- 8 files changed, 1061 insertions(+), 1034 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index e5fee3a9d..337cdfd2b 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -1261,8 +1261,10 @@ export const HandleNotifications = ({ notificationId }: Props) => { }); } toast.success("Connection Success"); - } catch { - toast.error("Error testing the provider"); + } catch (error) { + toast.error( + `Error testing the provider ${error instanceof Error ? error.message : "Unknown error"}`, + ); } }} > diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index 56748dc98..c483b813e 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -111,7 +111,7 @@ export const notificationRouter = createTRPCRouter({ } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error testing the notification", + message: `${error instanceof Error ? error.message : "Unknown error"}`, cause: error, }); } @@ -228,7 +228,7 @@ export const notificationRouter = createTRPCRouter({ } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error testing the notification", + message: `${error instanceof Error ? error.message : "Unknown error"}`, cause: error, }); } @@ -285,7 +285,7 @@ export const notificationRouter = createTRPCRouter({ } catch (error) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Error testing the notification", + message: `${error instanceof Error ? error.message : "Unknown error"}`, cause: error, }); } diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 67c568b72..3ee983031 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, - sendLarkNotification, sendGotifyNotification, + sendLarkNotification, sendNtfyNotification, sendSlackNotification, sendTelegramNotification, @@ -52,279 +52,287 @@ export const sendBuildErrorNotifications = async ({ for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, lark } = notification; - if (email) { - const template = await renderAsync( - BuildFailedEmail({ - projectName, - applicationName, - applicationType, - errorMessage: errorMessage, - buildLink, - date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification(email, "Build failed for dokploy", template); - } + try { + if (email) { + const template = await renderAsync( + BuildFailedEmail({ + projectName, + applicationName, + applicationType, + errorMessage: errorMessage, + buildLink, + date: date.toLocaleString(), + }), + ).catch(); + await sendEmailNotification( + email, + "Build failed for dokploy", + template, + ); + } - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - const limitCharacter = 800; - const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); - await sendDiscordNotification(discord, { - title: decorate(">", "`⚠️` Build Failed"), - color: 0xed4245, - fields: [ - { - name: decorate("`🛠️`", "Project"), - value: projectName, - inline: true, + const limitCharacter = 800; + const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); + await sendDiscordNotification(discord, { + title: decorate(">", "`⚠️` Build Failed"), + color: 0xed4245, + fields: [ + { + name: decorate("`🛠️`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`⚙️`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`❔`", "Type"), + value: applicationType, + inline: true, + }, + { + name: decorate("`📅`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`❓`", "Type"), + value: "Failed", + inline: true, + }, + { + name: decorate("`⚠️`", "Error Message"), + value: `\`\`\`${truncatedErrorMessage}\`\`\``, + }, + { + name: decorate("`🧷`", "Build Link"), + value: `[Click here to access build link](${buildLink})`, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Build Notification", }, - { - name: decorate("`⚙️`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`❔`", "Type"), - value: applicationType, - inline: true, - }, - { - name: decorate("`📅`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: "Failed", - inline: true, - }, - { - name: decorate("`⚠️`", "Error Message"), - value: `\`\`\`${truncatedErrorMessage}\`\`\``, - }, - { - name: decorate("`🧷`", "Build Link"), - value: `[Click here to access build link](${buildLink})`, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Build Notification", - }, - }); - } + }); + } - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("⚠️", "Build Failed"), - `${decorate("🛠️", `Project: ${projectName}`)}` + - `${decorate("⚙️", `Application: ${applicationName}`)}` + - `${decorate("❔", `Type: ${applicationType}`)}` + - `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + - `${decorate("⚠️", `Error:\n${errorMessage}`)}` + - `${decorate("🔗", `Build details:\n${buildLink}`)}`, - ); - } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("⚠️", "Build Failed"), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("❔", `Type: ${applicationType}`)}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${decorate("⚠️", `Error:\n${errorMessage}`)}` + + `${decorate("🔗", `Build details:\n${buildLink}`)}`, + ); + } - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Build Failed", - "warning", - `view, Build details, ${buildLink}, clear=true;`, - `🛠️Project: ${projectName}\n` + - `⚙️Application: ${applicationName}\n` + - `❔Type: ${applicationType}\n` + - `🕒Date: ${date.toLocaleString()}\n` + - `⚠️Error:\n${errorMessage}`, - ); - } + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Build Failed", + "warning", + `view, Build details, ${buildLink}, clear=true;`, + `🛠️Project: ${projectName}\n` + + `⚙️Application: ${applicationName}\n` + + `❔Type: ${applicationType}\n` + + `🕒Date: ${date.toLocaleString()}\n` + + `⚠️Error:\n${errorMessage}`, + ); + } - if (telegram) { - const inlineButton = [ - [ - { - text: "Deployment Logs", - url: buildLink, - }, - ], - ]; + if (telegram) { + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ]; - await sendTelegramNotification( - telegram, - `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, - inlineButton, - ); - } + await sendTelegramNotification( + telegram, + `⚠️ Build Failed\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}\n\nError:\n
${errorMessage}
`, + inlineButton, + ); + } - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#FF0000", - pretext: ":warning: *Build Failed*", - fields: [ - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Type", - value: applicationType, - short: true, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Error", - value: `\`\`\`${errorMessage}\`\`\``, - short: false, - }, - ], - actions: [ - { - type: "button", - text: "View Build Details", - url: buildLink, - }, - ], - }, - ], - }); - } + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#FF0000", + pretext: ":warning: *Build Failed*", + fields: [ + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Type", + value: applicationType, + short: true, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Error", + value: `\`\`\`${errorMessage}\`\`\``, + short: false, + }, + ], + actions: [ + { + type: "button", + text: "View Build Details", + url: buildLink, + }, + ], + }, + ], + }); + } - if (lark) { - const limitCharacter = 800; - const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", + if (lark) { + const limitCharacter = 800; + const truncatedErrorMessage = errorMessage.substring(0, limitCharacter); + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, }, }, }, - }, - header: { - title: { - tag: "plain_text", - content: "⚠️ Build Failed", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "red", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Project:**\n${projectName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Type:**\n${applicationType}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Application:**\n${applicationName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], + header: { + title: { + tag: "plain_text", + content: "⚠️ Build Failed", }, - { - tag: "button", - text: { - tag: "plain_text", - content: "View Build Details", + subtitle: { + tag: "plain_text", + content: "", + }, + template: "red", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Project:**\n${projectName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Type:**\n${applicationType}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Application:**\n${applicationName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], }, - type: "danger", - width: "default", - size: "medium", - behaviors: [ - { - type: "open_url", - default_url: buildLink, - pc_url: "", - ios_url: "", - android_url: "", + { + tag: "button", + text: { + tag: "plain_text", + content: "View Build Details", }, - ], - margin: "0px 0px 0px 0px", - }, - ], + type: "danger", + width: "default", + size: "medium", + behaviors: [ + { + type: "open_url", + default_url: buildLink, + pc_url: "", + ios_url: "", + android_url: "", + }, + ], + margin: "0px 0px 0px 0px", + }, + ], + }, }, - }, - }); + }); + } + } catch (error) { + console.log(error); } } }; diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 5b5d6f518..232eb76c2 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -8,8 +8,8 @@ import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, - sendLarkNotification, sendGotifyNotification, + sendLarkNotification, sendNtfyNotification, sendSlackNotification, sendTelegramNotification, @@ -55,288 +55,295 @@ export const sendBuildSuccessNotifications = async ({ for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, lark } = notification; - - if (email) { - const template = await renderAsync( - BuildSuccessEmail({ - projectName, - applicationName, - applicationType, - buildLink, - date: date.toLocaleString(), - environmentName, - }), - ).catch(); - await sendEmailNotification(email, "Build success for dokploy", template); - } - - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: decorate(">", "`✅` Build Successes"), - color: 0x57f287, - fields: [ - { - name: decorate("`🛠️`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`⚙️`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`🌍`", "Environment"), - value: environmentName, - inline: true, - }, - { - name: decorate("`❔`", "Type"), - value: applicationType, - inline: true, - }, - { - name: decorate("`📅`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: "Successful", - inline: true, - }, - { - name: decorate("`🧷`", "Build Link"), - value: `[Click here to access build link](${buildLink})`, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Build Notification", - }, - }); - } - - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("✅", "Build Success"), - `${decorate("🛠️", `Project: ${projectName}`)}` + - `${decorate("⚙️", `Application: ${applicationName}`)}` + - `${decorate("🌍", `Environment: ${environmentName}`)}` + - `${decorate("❔", `Type: ${applicationType}`)}` + - `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + - `${decorate("🔗", `Build details:\n${buildLink}`)}`, - ); - } - - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Build Success", - "white_check_mark", - `view, Build details, ${buildLink}, clear=true;`, - `🛠Project: ${projectName}\n` + - `⚙️Application: ${applicationName}\n` + - `🌍Environment: ${environmentName}\n` + - `❔Type: ${applicationType}\n` + - `🕒Date: ${date.toLocaleString()}`, - ); - } - - if (telegram) { - const chunkArray = (array: T[], chunkSize: number): T[][] => - Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => - array.slice(i * chunkSize, i * chunkSize + chunkSize), + try { + if (email) { + const template = await renderAsync( + BuildSuccessEmail({ + projectName, + applicationName, + applicationType, + buildLink, + date: date.toLocaleString(), + environmentName, + }), + ).catch(); + await sendEmailNotification( + email, + "Build success for dokploy", + template, ); + } - const inlineButton = [ - [ - { - text: "Deployment Logs", - url: buildLink, + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + + await sendDiscordNotification(discord, { + title: decorate(">", "`✅` Build Successes"), + color: 0x57f287, + fields: [ + { + name: decorate("`🛠️`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`⚙️`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`🌍`", "Environment"), + value: environmentName, + inline: true, + }, + { + name: decorate("`❔`", "Type"), + value: applicationType, + inline: true, + }, + { + name: decorate("`📅`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`❓`", "Type"), + value: "Successful", + inline: true, + }, + { + name: decorate("`🧷`", "Build Link"), + value: `[Click here to access build link](${buildLink})`, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Build Notification", }, - ], - ...chunkArray(domains, 2).map((chunk) => - chunk.map((data) => ({ - text: data.host, - url: `${data.https ? "https" : "http"}://${data.host}`, - })), - ), - ]; + }); + } - await sendTelegramNotification( - telegram, - `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, - inlineButton, - ); - } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("✅", "Build Success"), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("🌍", `Environment: ${environmentName}`)}` + + `${decorate("❔", `Type: ${applicationType}`)}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${decorate("🔗", `Build details:\n${buildLink}`)}`, + ); + } - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Build Success*", - fields: [ - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Environment", - value: environmentName, - short: true, - }, - { - title: "Type", - value: applicationType, - short: true, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - actions: [ - { - type: "button", - text: "View Build Details", - url: buildLink, - }, - ], - }, - ], - }); - } + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Build Success", + "white_check_mark", + `view, Build details, ${buildLink}, clear=true;`, + `🛠Project: ${projectName}\n` + + `⚙️Application: ${applicationName}\n` + + `🌍Environment: ${environmentName}\n` + + `❔Type: ${applicationType}\n` + + `🕒Date: ${date.toLocaleString()}`, + ); + } - if (lark) { - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", + if (telegram) { + const chunkArray = (array: T[], chunkSize: number): T[][] => + Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => + array.slice(i * chunkSize, i * chunkSize + chunkSize), + ); + + const inlineButton = [ + [ + { + text: "Deployment Logs", + url: buildLink, + }, + ], + ...chunkArray(domains, 2).map((chunk) => + chunk.map((data) => ({ + text: data.host, + url: `${data.https ? "https" : "http"}://${data.host}`, + })), + ), + ]; + + await sendTelegramNotification( + telegram, + `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + inlineButton, + ); + } + + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Build Success*", + fields: [ + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Environment", + value: environmentName, + short: true, + }, + { + title: "Type", + value: applicationType, + short: true, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + actions: [ + { + type: "button", + text: "View Build Details", + url: buildLink, + }, + ], + }, + ], + }); + } + + if (lark) { + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, }, }, }, - }, - header: { - title: { - tag: "plain_text", - content: "✅ Build Success", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "green", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Project:**\n${projectName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Environment:**\n${environmentName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Type:**\n${applicationType}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Application:**\n${applicationName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], + header: { + title: { + tag: "plain_text", + content: "✅ Build Success", }, - { - tag: "button", - text: { - tag: "plain_text", - content: "View Build Details", + subtitle: { + tag: "plain_text", + content: "", + }, + template: "green", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Project:**\n${projectName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Environment:**\n${environmentName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Type:**\n${applicationType}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Application:**\n${applicationName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], }, - type: "primary", - width: "default", - size: "medium", - behaviors: [ - { - type: "open_url", - default_url: buildLink, - pc_url: "", - ios_url: "", - android_url: "", + { + tag: "button", + text: { + tag: "plain_text", + content: "View Build Details", }, - ], - margin: "0px 0px 0px 0px", - }, - ], + type: "primary", + width: "default", + size: "medium", + behaviors: [ + { + type: "open_url", + default_url: buildLink, + pc_url: "", + ios_url: "", + android_url: "", + }, + ], + margin: "0px 0px 0px 0px", + }, + ], + }, }, - }, - }); + }); + } + } catch (error) { + console.log(error); } } }; diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index c5cb68dbc..02141e3ae 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, - sendLarkNotification, sendGotifyNotification, + sendLarkNotification, sendNtfyNotification, sendSlackNotification, sendTelegramNotification, @@ -52,309 +52,312 @@ export const sendDatabaseBackupNotifications = async ({ for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, lark } = notification; + try { + if (email) { + const template = await renderAsync( + DatabaseBackupEmail({ + projectName, + applicationName, + databaseType, + type, + errorMessage, + date: date.toLocaleString(), + }), + ).catch(); + await sendEmailNotification( + email, + "Database backup for dokploy", + template, + ); + } - if (email) { - const template = await renderAsync( - DatabaseBackupEmail({ - projectName, - applicationName, - databaseType, - type, - errorMessage, - date: date.toLocaleString(), - }), - ).catch(); - await sendEmailNotification( - email, - "Database backup for dokploy", - template, - ); - } + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(discord, { + title: + type === "success" + ? decorate(">", "`✅` Database Backup Successful") + : decorate(">", "`❌` Database Backup Failed"), + color: type === "success" ? 0x57f287 : 0xed4245, + fields: [ + { + name: decorate("`🛠️`", "Project"), + value: projectName, + inline: true, + }, + { + name: decorate("`⚙️`", "Application"), + value: applicationName, + inline: true, + }, + { + name: decorate("`❔`", "Database"), + value: databaseType, + inline: true, + }, + { + name: decorate("`📂`", "Database Name"), + value: databaseName, + inline: true, + }, + { + name: decorate("`📅`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`❓`", "Type"), + value: type + .replace("error", "Failed") + .replace("success", "Successful"), + inline: true, + }, + ...(type === "error" && errorMessage + ? [ + { + name: decorate("`⚠️`", "Error Message"), + value: `\`\`\`${errorMessage}\`\`\``, + }, + ] + : []), + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Database Backup Notification", + }, + }); + } - await sendDiscordNotification(discord, { - title: - type === "success" - ? decorate(">", "`✅` Database Backup Successful") - : decorate(">", "`❌` Database Backup Failed"), - color: type === "success" ? 0x57f287 : 0xed4245, - fields: [ - { - name: decorate("`🛠️`", "Project"), - value: projectName, - inline: true, - }, - { - name: decorate("`⚙️`", "Application"), - value: applicationName, - inline: true, - }, - { - name: decorate("`❔`", "Database"), - value: databaseType, - inline: true, - }, - { - name: decorate("`📂`", "Database Name"), - value: databaseName, - inline: true, - }, - { - name: decorate("`📅`", "Date"), - value: ``, - inline: true, - }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: type - .replace("error", "Failed") - .replace("success", "Successful"), - inline: true, - }, - ...(type === "error" && errorMessage - ? [ - { - name: decorate("`⚠️`", "Error Message"), - value: `\`\`\`${errorMessage}\`\`\``, - }, - ] - : []), - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Database Backup Notification", - }, - }); - } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate( + type === "success" ? "✅" : "❌", + `Database Backup ${type === "success" ? "Successful" : "Failed"}`, + ), + `${decorate("🛠️", `Project: ${projectName}`)}` + + `${decorate("⚙️", `Application: ${applicationName}`)}` + + `${decorate("❔", `Type: ${databaseType}`)}` + + `${decorate("📂", `Database Name: ${databaseName}`)}` + + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`, + ); + } - await sendGotifyNotification( - gotify, - decorate( - type === "success" ? "✅" : "❌", + if (ntfy) { + await sendNtfyNotification( + ntfy, `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - ), - `${decorate("🛠️", `Project: ${projectName}`)}` + - `${decorate("⚙️", `Application: ${applicationName}`)}` + - `${decorate("❔", `Type: ${databaseType}`)}` + - `${decorate("📂", `Database Name: ${databaseName}`)}` + - `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + - `${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`, - ); - } + `${type === "success" ? "white_check_mark" : "x"}`, + "", + `🛠Project: ${projectName}\n` + + `⚙️Application: ${applicationName}\n` + + `❔Type: ${databaseType}\n` + + `📂Database Name: ${databaseName}` + + `🕒Date: ${date.toLocaleString()}\n` + + `${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`, + ); + } - if (ntfy) { - await sendNtfyNotification( - ntfy, - `Database Backup ${type === "success" ? "Successful" : "Failed"}`, - `${type === "success" ? "white_check_mark" : "x"}`, - "", - `🛠Project: ${projectName}\n` + - `⚙️Application: ${applicationName}\n` + - `❔Type: ${databaseType}\n` + - `📂Database Name: ${databaseName}` + - `🕒Date: ${date.toLocaleString()}\n` + - `${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`, - ); - } + if (telegram) { + const isError = type === "error" && errorMessage; - if (telegram) { - const isError = type === "error" && errorMessage; + const statusEmoji = type === "success" ? "✅" : "❌"; + const typeStatus = type === "success" ? "Successful" : "Failed"; + const errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; - const statusEmoji = type === "success" ? "✅" : "❌"; - const typeStatus = type === "success" ? "Successful" : "Failed"; - const errorMsg = isError - ? `\n\nError:\n
${errorMessage}
` - : ""; + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; + await sendTelegramNotification(telegram, messageText); + } - await sendTelegramNotification(telegram, messageText); - } + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: type === "success" ? "#00FF00" : "#FF0000", + pretext: + type === "success" + ? ":white_check_mark: *Database Backup Successful*" + : ":x: *Database Backup Failed*", + fields: [ + ...(type === "error" && errorMessage + ? [ + { + title: "Error Message", + value: errorMessage, + short: false, + }, + ] + : []), + { + title: "Project", + value: projectName, + short: true, + }, + { + title: "Application", + value: applicationName, + short: true, + }, + { + title: "Type", + value: databaseType, + short: true, + }, + { + title: "Database Name", + value: databaseName, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + { + title: "Type", + value: type, + }, + { + title: "Status", + value: type === "success" ? "Successful" : "Failed", + }, + ], + }, + ], + }); + } - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: type === "success" ? "#00FF00" : "#FF0000", - pretext: - type === "success" - ? ":white_check_mark: *Database Backup Successful*" - : ":x: *Database Backup Failed*", - fields: [ - ...(type === "error" && errorMessage - ? [ - { - title: "Error Message", - value: errorMessage, - short: false, - }, - ] - : []), - { - title: "Project", - value: projectName, - short: true, - }, - { - title: "Application", - value: applicationName, - short: true, - }, - { - title: "Type", - value: databaseType, - short: true, - }, - { - title: "Database Name", - value: databaseName, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - { - title: "Type", - value: type, - }, - { - title: "Status", - value: type === "success" ? "Successful" : "Failed", - }, - ], - }, - ], - }); - } + if (lark) { + const limitCharacter = 800; + const truncatedErrorMessage = + errorMessage && errorMessage.length > limitCharacter + ? errorMessage.substring(0, limitCharacter) + : errorMessage; - if (lark) { - const limitCharacter = 800; - const truncatedErrorMessage = - errorMessage && errorMessage.length > limitCharacter - ? errorMessage.substring(0, limitCharacter) - : errorMessage; - - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, }, }, }, - }, - header: { - title: { - tag: "plain_text", - content: - type === "success" - ? "✅ Database Backup Successful" - : "❌ Database Backup Failed", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: type === "success" ? "green" : "red", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Project:**\n${projectName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Database Type:**\n${databaseType}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Application:**\n${applicationName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Database Name:**\n${databaseName}`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], + header: { + title: { + tag: "plain_text", + content: + type === "success" + ? "✅ Database Backup Successful" + : "❌ Database Backup Failed", }, - ...(type === "error" && truncatedErrorMessage - ? [ + subtitle: { + tag: "plain_text", + content: "", + }, + template: type === "success" ? "green" : "red", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ { - tag: "markdown", - content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, - text_align: "left", - text_size: "normal_v2", + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Project:**\n${projectName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Database Type:**\n${databaseType}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, }, - ] - : []), - ], + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Application:**\n${applicationName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Database Name:**\n${databaseName}`, + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + ...(type === "error" && truncatedErrorMessage + ? [ + { + tag: "markdown", + content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``, + text_align: "left", + text_size: "normal_v2", + }, + ] + : []), + ], + }, }, - }, - }); + }); + } + } catch (error) { + console.log(error); } } }; diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 062da9d49..f7947c8a0 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -7,8 +7,8 @@ import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, - sendLarkNotification, sendGotifyNotification, + sendLarkNotification, sendNtfyNotification, sendSlackNotification, sendTelegramNotification, @@ -39,182 +39,185 @@ export const sendDockerCleanupNotifications = async ( for (const notification of notificationList) { const { email, discord, telegram, slack, gotify, ntfy, lark } = notification; + try { + if (email) { + const template = await renderAsync( + DockerCleanupEmail({ message, date: date.toLocaleString() }), + ).catch(); - if (email) { - const template = await renderAsync( - DockerCleanupEmail({ message, date: date.toLocaleString() }), - ).catch(); + await sendEmailNotification( + email, + "Docker cleanup for dokploy", + template, + ); + } - await sendEmailNotification( - email, - "Docker cleanup for dokploy", - template, - ); - } + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); - - await sendDiscordNotification(discord, { - title: decorate(">", "`✅` Docker Cleanup"), - color: 0x57f287, - fields: [ - { - name: decorate("`📅`", "Date"), - value: ``, - inline: true, + await sendDiscordNotification(discord, { + title: decorate(">", "`✅` Docker Cleanup"), + color: 0x57f287, + fields: [ + { + name: decorate("`📅`", "Date"), + value: ``, + inline: true, + }, + { + name: decorate("`⌚`", "Time"), + value: ``, + inline: true, + }, + { + name: decorate("`❓`", "Type"), + value: "Successful", + inline: true, + }, + { + name: decorate("`📜`", "Message"), + value: `\`\`\`${message}\`\`\``, + }, + ], + timestamp: date.toISOString(), + footer: { + text: "Dokploy Docker Cleanup Notification", }, - { - name: decorate("`⌚`", "Time"), - value: ``, - inline: true, - }, - { - name: decorate("`❓`", "Type"), - value: "Successful", - inline: true, - }, - { - name: decorate("`📜`", "Message"), - value: `\`\`\`${message}\`\`\``, - }, - ], - timestamp: date.toISOString(), - footer: { - text: "Dokploy Docker Cleanup Notification", - }, - }); - } + }); + } - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - await sendGotifyNotification( - gotify, - decorate("✅", "Docker Cleanup"), - `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + - `${decorate("📜", `Message:\n${message}`)}`, - ); - } + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; + await sendGotifyNotification( + gotify, + decorate("✅", "Docker Cleanup"), + `${decorate("🕒", `Date: ${date.toLocaleString()}`)}` + + `${decorate("📜", `Message:\n${message}`)}`, + ); + } - if (ntfy) { - await sendNtfyNotification( - ntfy, - "Docker Cleanup", - "white_check_mark", - "", - `🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`, - ); - } + if (ntfy) { + await sendNtfyNotification( + ntfy, + "Docker Cleanup", + "white_check_mark", + "", + `🕒Date: ${date.toLocaleString()}\n` + `📜Message:\n${message}`, + ); + } - if (telegram) { - await sendTelegramNotification( - telegram, - `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, - ); - } + if (telegram) { + await sendTelegramNotification( + telegram, + `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + ); + } - if (slack) { - const { channel } = slack; - await sendSlackNotification(slack, { - channel: channel, - attachments: [ - { - color: "#00FF00", - pretext: ":white_check_mark: *Docker Cleanup*", - fields: [ - { - title: "Message", - value: message, - }, - { - title: "Time", - value: date.toLocaleString(), - short: true, - }, - ], - }, - ], - }); - } + if (slack) { + const { channel } = slack; + await sendSlackNotification(slack, { + channel: channel, + attachments: [ + { + color: "#00FF00", + pretext: ":white_check_mark: *Docker Cleanup*", + fields: [ + { + title: "Message", + value: message, + }, + { + title: "Time", + value: date.toLocaleString(), + short: true, + }, + ], + }, + ], + }); + } - if (lark) { - await sendLarkNotification(lark, { - msg_type: "interactive", - card: { - schema: "2.0", - config: { - update_multi: true, - style: { - text_size: { - normal_v2: { - default: "normal", - pc: "normal", - mobile: "heading", + if (lark) { + await sendLarkNotification(lark, { + msg_type: "interactive", + card: { + schema: "2.0", + config: { + update_multi: true, + style: { + text_size: { + normal_v2: { + default: "normal", + pc: "normal", + mobile: "heading", + }, }, }, }, - }, - header: { - title: { - tag: "plain_text", - content: "✅ Docker Cleanup", - }, - subtitle: { - tag: "plain_text", - content: "", - }, - template: "green", - padding: "12px 12px 12px 12px", - }, - body: { - direction: "vertical", - padding: "12px 12px 12px 12px", - elements: [ - { - tag: "column_set", - columns: [ - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Status:**\nSuccessful`, - text_align: "left", - text_size: "normal_v2", - }, - { - tag: "markdown", - content: `**Cleanup Details:**\n${message}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - { - tag: "column", - width: "weighted", - elements: [ - { - tag: "markdown", - content: `**Date:**\n${format(date, "PP pp")}`, - text_align: "left", - text_size: "normal_v2", - }, - ], - vertical_align: "top", - weight: 1, - }, - ], + header: { + title: { + tag: "plain_text", + content: "✅ Docker Cleanup", }, - ], + subtitle: { + tag: "plain_text", + content: "", + }, + template: "green", + padding: "12px 12px 12px 12px", + }, + body: { + direction: "vertical", + padding: "12px 12px 12px 12px", + elements: [ + { + tag: "column_set", + columns: [ + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: "**Status:**\nSuccessful", + text_align: "left", + text_size: "normal_v2", + }, + { + tag: "markdown", + content: `**Cleanup Details:**\n${message}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + { + tag: "column", + width: "weighted", + elements: [ + { + tag: "markdown", + content: `**Date:**\n${format(date, "PP pp")}`, + text_align: "left", + text_size: "normal_v2", + }, + ], + vertical_align: "top", + weight: 1, + }, + ], + }, + ], + }, }, - }, - }); + }); + } + } catch (error) { + console.log(error); } } }; diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index 2582c92d1..093e14010 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -7,8 +7,8 @@ import { eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, - sendLarkNotification, sendGotifyNotification, + sendLarkNotification, sendNtfyNotification, sendSlackNotification, sendTelegramNotification, @@ -34,18 +34,23 @@ export const sendDokployRestartNotifications = async () => { const { email, discord, telegram, slack, gotify, ntfy, lark } = notification; - if (email) { - const template = await renderAsync( - DokployRestartEmail({ date: date.toLocaleString() }), - ).catch(); - await sendEmailNotification(email, "Dokploy Server Restarted", template); - } + try { + if (email) { + const template = await renderAsync( + DokployRestartEmail({ date: date.toLocaleString() }), + ).catch(); - if (discord) { - const decorate = (decoration: string, text: string) => - `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendEmailNotification( + email, + "Dokploy Server Restarted", + template, + ); + } + + if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); - try { await sendDiscordNotification(discord, { title: decorate(">", "`✅` Dokploy Server Restarted"), color: 0x57f287, @@ -71,27 +76,19 @@ export const sendDokployRestartNotifications = async () => { text: "Dokploy Restart Notification", }, }); - } catch (error) { - console.log(error); } - } - if (gotify) { - const decorate = (decoration: string, text: string) => - `${gotify.decoration ? decoration : ""} ${text}\n`; - try { + if (gotify) { + const decorate = (decoration: string, text: string) => + `${gotify.decoration ? decoration : ""} ${text}\n`; await sendGotifyNotification( gotify, decorate("✅", "Dokploy Server Restarted"), `${decorate("🕒", `Date: ${date.toLocaleString()}`)}`, ); - } catch (error) { - console.log(error); } - } - if (ntfy) { - try { + if (ntfy) { await sendNtfyNotification( ntfy, "Dokploy Server Restarted", @@ -99,25 +96,17 @@ export const sendDokployRestartNotifications = async () => { "", `🕒Date: ${date.toLocaleString()}`, ); - } catch (error) { - console.log(error); } - } - if (telegram) { - try { + if (telegram) { await sendTelegramNotification( telegram, `✅ Dokploy Server Restarted\n\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); - } catch (error) { - console.log(error); } - } - if (slack) { - const { channel } = slack; - try { + if (slack) { + const { channel } = slack; await sendSlackNotification(slack, { channel: channel, attachments: [ @@ -134,13 +123,9 @@ export const sendDokployRestartNotifications = async () => { }, ], }); - } catch (error) { - console.log(error); } - } - if (lark) { - try { + if (lark) { await sendLarkNotification(lark, { msg_type: "interactive", card: { @@ -182,7 +167,7 @@ export const sendDokployRestartNotifications = async () => { elements: [ { tag: "markdown", - content: `**Status:**\nSuccessful`, + content: "**Status:**\nSuccessful", text_align: "left", text_size: "normal_v2", }, @@ -210,9 +195,9 @@ export const sendDokployRestartNotifications = async () => { }, }, }); - } catch (error) { - console.log(error); } + } catch (error) { + console.log(error); } } }; diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index a56a70918..539376ac5 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -1,8 +1,8 @@ import type { discord, email, - lark, gotify, + lark, ntfy, slack, telegram, @@ -38,6 +38,9 @@ export const sendEmailNotification = async ( }); } catch (err) { console.log(err); + throw new Error( + `Failed to send email notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); } }; @@ -45,15 +48,23 @@ export const sendDiscordNotification = async ( connection: typeof discord.$inferInsert, embed: any, ) => { - // try { - await fetch(connection.webhookUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ embeds: [embed] }), - }); - // } catch (err) { - // console.log(err); - // } + try { + const response = await fetch(connection.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ embeds: [embed] }), + }); + if (!response.ok) { + throw new Error( + `Failed to send discord notification ${response.statusText}`, + ); + } + } catch (err) { + console.log("error", err); + throw new Error( + `Failed to send discord notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); + } }; export const sendTelegramNotification = async ( @@ -90,13 +101,21 @@ export const sendSlackNotification = async ( message: any, ) => { try { - await fetch(connection.webhookUrl, { + const response = await fetch(connection.webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(message), }); + if (!response.ok) { + throw new Error( + `Failed to send slack notification ${response.statusText}`, + ); + } } catch (err) { - console.log(err); + console.log("error", err); + throw new Error( + `Failed to send slack notification ${err instanceof Error ? err.message : "Unknown error"}`, + ); } }; From ff27f0828bfcfba67d25c11da94e24ce677682db Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 02:59:32 -0500 Subject: [PATCH 77/79] feat: add Railpack installation command to builder script - Introduced a command to set the RAILPACK_VERSION environment variable and execute the Railpack installation script. - This enhancement ensures that the correct version of Railpack is used during the build process. --- packages/server/src/utils/builders/railpack.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 03d490cdf..62fa9f975 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -85,6 +85,9 @@ export const getRailpackCommand = (application: ApplicationNested) => { const bashCommand = ` # Ensure we have a builder with containerd + +export RAILPACK_VERSION=${application.railpackVersion} +bash -c "$(curl -fsSL https://railpack.com/install.sh)" docker buildx create --use --name builder-containerd --driver docker-container || true docker buildx use builder-containerd From f61fb3aba0fc39ad30e53c4b40c8a6c68fd1fff8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 03:13:26 -0500 Subject: [PATCH 78/79] chore: update dokploy version to v0.25.11 --- apps/dokploy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index e52968b15..1301fe91b 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.25.10", + "version": "v0.25.11", "private": true, "license": "Apache-2.0", "type": "module", From b9d05b00a93fef72d1972a0b14da1f4322be47f7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Wed, 26 Nov 2025 11:17:28 -0600 Subject: [PATCH 79/79] fix: correct query parameter name in GitLab authorization URL --- .../components/dashboard/settings/git/show-git-providers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx index 5f99a2e97..5e4c12696 100644 --- a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx +++ b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx @@ -48,7 +48,7 @@ export const ShowGitProviders = () => { ) => { const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`; const scope = "api read_user read_repository"; - const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`; + const authUrl = `${gitlabUrl}/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scopes=${encodeURIComponent(scope)}`; return authUrl; };