Merge pull request #1134 from Dokploy/1031-excessive-unused-docker-cache-generated-by-dokploy-deployments

feat: add cleanup cache on deployments
This commit is contained in:
Mauricio Siu
2025-01-19 02:08:48 -06:00
committed by GitHub
40 changed files with 13428 additions and 108 deletions

View File

@@ -14,6 +14,9 @@ import {
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: Admin = { const baseAdmin: Admin = {
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
createdAt: "", createdAt: "",
authId: "", authId: "",
adminId: "string", adminId: "string",

View File

@@ -75,7 +75,7 @@ export const ShowBackups = ({ id, type }: Props) => {
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<DatabaseBackup className="size-8 text-muted-foreground" /> <DatabaseBackup className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground text-center">
To create a backup it is required to set at least 1 provider. To create a backup it is required to set at least 1 provider.
Please, go to{" "} Please, go to{" "}
<Link <Link

View File

@@ -200,7 +200,7 @@ export const DockerMonitoring = ({
</div> </div>
</header> </header>
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle> <CardTitle className="text-sm font-medium">CPU Usage</CardTitle>

View File

@@ -133,7 +133,9 @@ export const AddTemplate = ({ projectId }: Props) => {
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
className={cn("w-full sm:w-[200px] justify-between !bg-input")} className={cn(
"w-full sm:w-[200px] justify-between !bg-input",
)}
> >
{isLoadingTags {isLoadingTags
? "Loading...." ? "Loading...."

View File

@@ -77,8 +77,8 @@ export const ShowProjects = () => {
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl "> <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center"> <div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className=""> <CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" /> <FolderInput className="size-6 text-muted-foreground self-center" />
Projects Projects
@@ -87,7 +87,7 @@ export const ShowProjects = () => {
Create and manage your projects Create and manage your projects
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<div className=" px-4 "> <div className="">
<HandleProject /> <HandleProject />
</div> </div>
</div> </div>

View File

@@ -50,7 +50,7 @@ export const ShowCertificates = () => {
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center"> <div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<ShieldCheck className="size-8 self-center text-muted-foreground" /> <ShieldCheck className="size-8 self-center text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground text-center">
You don't have any certificates created You don't have any certificates created
</span> </span>
<AddCertificate /> <AddCertificate />

View File

@@ -47,7 +47,7 @@ export const ShowNotifications = () => {
{data?.length === 0 ? ( {data?.length === 0 ? (
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center"> <div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<Bell /> <Bell />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground text-center">
To send notifications it is required to set at least 1 To send notifications it is required to set at least 1
provider. provider.
</span> </span>

View File

@@ -60,7 +60,7 @@ export function NodeCard({ node, serverId }: Props) {
<div className="font-medium">{node.Hostname}</div> <div className="font-medium">{node.Hostname}</div>
<Badge variant="green">{node.ManagerStatus || "Worker"}</Badge> <Badge variant="green">{node.ManagerStatus || "Worker"}</Badge>
</div> </div>
<div className="flex flex-wrap items-center space-x-4"> <div className="flex flex-wrap items-center gap-4">
<Badge variant="green">TLS Status: {node.TLSStatus}</Badge> <Badge variant="green">TLS Status: {node.TLSStatus}</Badge>
<Badge variant="blue">Availability: {node.Availability}</Badge> <Badge variant="blue">Availability: {node.Availability}</Badge>
</div> </div>

View File

@@ -72,7 +72,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
return ( return (
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full"> <Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4"> <div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
<header className="flex items-center justify-between"> <header className="flex items-center flex-wrap gap-4 justify-between">
<div className="space-y-1"> <div className="space-y-1">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<WorkflowIcon className="size-6 text-muted-foreground self-center" /> <WorkflowIcon className="size-6 text-muted-foreground self-center" />

View File

@@ -5,9 +5,5 @@ interface Props {
} }
export const DashboardLayout = ({ children }: Props) => { export const DashboardLayout = ({ children }: Props) => {
return ( return <Page>{children}</Page>;
<Page>
<div>{children}</div>
</Page>
);
}; };

View File

@@ -11,7 +11,7 @@ interface Props {
export const OnboardingLayout = ({ children }: Props) => { export const OnboardingLayout = ({ children }: Props) => {
return ( return (
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full"> <div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
<div className="relative hidden h-full flex-col p-10 text-white dark:border-r lg:flex"> <div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
<div className="absolute inset-0 bg-muted" /> <div className="absolute inset-0 bg-muted" />
<Link <Link
href="https://dokploy.com" href="https://dokploy.com"
@@ -22,35 +22,16 @@ export const OnboardingLayout = ({ children }: Props) => {
</Link> </Link>
<div className="relative z-20 mt-auto"> <div className="relative z-20 mt-auto">
<blockquote className="space-y-2"> <blockquote className="space-y-2">
<p className="text-lg"> <p className="text-lg text-primary">
&ldquo;The Open Source alternative to Netlify, Vercel, &ldquo;The Open Source alternative to Netlify, Vercel,
Heroku.&rdquo; Heroku.&rdquo;
</p> </p>
{/* <footer className="text-sm">Sofia Davis</footer> */}
</blockquote> </blockquote>
</div> </div>
</div> </div>
<div className="w-full"> <div className="w-full">
<div className="flex w-full flex-col justify-center space-y-6 max-w-lg mx-auto"> <div className="flex w-full flex-col justify-center space-y-6 max-w-lg mx-auto">
{children} {children}
{/* <p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="underline underline-offset-4 hover:text-primary"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="underline underline-offset-4 hover:text-primary"
>
Privacy Policy
</Link>
.
</p> */}
</div> </div>
<div className="flex items-center gap-4 justify-center absolute bottom-4 right-4 text-muted-foreground"> <div className="flex items-center gap-4 justify-center absolute bottom-4 right-4 text-muted-foreground">
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">

View File

@@ -5,9 +5,5 @@ interface Props {
} }
export const ProjectLayout = ({ children }: Props) => { export const ProjectLayout = ({ children }: Props) => {
return ( return <Page>{children}</Page>;
<div>
<Page>{children}</Page>
</div>
);
}; };

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { useState, useEffect } from "react";
import { import {
Activity, Activity,
AudioWaveform, AudioWaveform,
@@ -46,6 +46,7 @@ import {
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { import {
Sidebar, Sidebar,
SIDEBAR_COOKIE_NAME,
SidebarContent, SidebarContent,
SidebarFooter, SidebarFooter,
SidebarGroup, SidebarGroup,
@@ -369,6 +370,19 @@ function SidebarLogo() {
} }
export default function Page({ children }: Props) { export default function Page({ children }: Props) {
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined,
);
useEffect(() => {
const cookieValue = document.cookie
.split("; ")
.find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`))
?.split("=")[1];
setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true");
}, []);
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const currentPath = router.pathname; const currentPath = router.pathname;
@@ -445,6 +459,13 @@ export default function Page({ children }: Props) {
return ( return (
<SidebarProvider <SidebarProvider
defaultOpen={defaultOpen}
open={defaultOpen}
onOpenChange={(open) => {
setDefaultOpen(open);
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
}}
style={ style={
{ {
"--sidebar-width": "19.5rem", "--sidebar-width": "19.5rem",
@@ -676,7 +697,7 @@ export default function Page({ children }: Props) {
<Separator orientation="vertical" className="mr-2 h-4" /> <Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>
<BreadcrumbItem className="hidden md:block"> <BreadcrumbItem className="block">
<BreadcrumbLink asChild> <BreadcrumbLink asChild>
<Link <Link
href={activeItem?.url || "/"} href={activeItem?.url || "/"}
@@ -686,7 +707,7 @@ export default function Page({ children }: Props) {
</Link> </Link>
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="block" />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>{activeItem?.title}</BreadcrumbPage> <BreadcrumbPage>{activeItem?.title}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>

View File

@@ -118,14 +118,16 @@ export const UserNav = () => {
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuItem {data?.rol === "admin" && (
className="cursor-pointer" <DropdownMenuItem
onClick={() => { className="cursor-pointer"
router.push("/dashboard/settings/server"); onClick={() => {
}} router.push("/dashboard/settings");
> }}
Settings >
</DropdownMenuItem> Settings
</DropdownMenuItem>
)}
</> </>
) : ( ) : (
<> <>

View File

@@ -28,7 +28,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
<BreadcrumbList> <BreadcrumbList>
{list.map((item, index) => ( {list.map((item, index) => (
<Fragment key={item.name}> <Fragment key={item.name}>
<BreadcrumbItem className="hidden md:block"> <BreadcrumbItem className="block">
<BreadcrumbLink href={item.href} asChild={!!item.href}> <BreadcrumbLink href={item.href} asChild={!!item.href}>
{item.href ? ( {item.href ? (
<Link href={item.href}>{item.name}</Link> <Link href={item.href}>{item.name}</Link>
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
)} )}
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" /> <BreadcrumbSeparator className="block" />
</Fragment> </Fragment>
))} ))}
</BreadcrumbList> </BreadcrumbList>

View File

@@ -17,7 +17,7 @@ import {
import { useIsMobile } from "@/hooks/use-mobile"; import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const SIDEBAR_COOKIE_NAME = "sidebar:state"; export const SIDEBAR_COOKIE_NAME = "sidebar:state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem"; const SIDEBAR_WIDTH_MOBILE = "18rem";
@@ -329,7 +329,7 @@ const SidebarInset = React.forwardRef<
<main <main
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background", "relative flex min-h-svh overflow-auto w-full flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className, className,
)} )}

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" ADD COLUMN "enableCleanupCache" boolean DEFAULT true NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "admin" RENAME COLUMN "enableCleanupCache" TO "cleanupCacheOnDeployments";

View File

@@ -0,0 +1,3 @@
ALTER TABLE "admin" RENAME COLUMN "cleanupCacheOnDeployments" TO "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "cleanupCacheOnCompose" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -400,6 +400,27 @@
"when": 1736789918294, "when": 1736789918294,
"tag": "0056_majestic_skaar", "tag": "0056_majestic_skaar",
"breakpoints": true "breakpoints": true
},
{
"idx": 57,
"version": "6",
"when": 1737266192013,
"tag": "0057_oval_ken_ellis",
"breakpoints": true
},
{
"idx": 58,
"version": "6",
"when": 1737266560950,
"tag": "0058_cultured_warpath",
"breakpoints": true
},
{
"idx": 59,
"version": "6",
"when": 1737268325181,
"tag": "0059_public_speed",
"breakpoints": true
} }
] ]
} }

View File

@@ -239,8 +239,8 @@ const Project = (
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl "> <Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md ">
<div className="flex justify-between gap-4 w-full items-center"> <div className="flex justify-between gap-4 w-full items-center flex-wrap p-6">
<CardHeader className=""> <CardHeader className="p-0">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<FolderInput className="size-6 text-muted-foreground self-center" /> <FolderInput className="size-6 text-muted-foreground self-center" />
{data?.name} {data?.name}
@@ -248,7 +248,7 @@ const Project = (
<CardDescription>{data?.description}</CardDescription> <CardDescription>{data?.description}</CardDescription>
</CardHeader> </CardHeader>
{(auth?.rol === "admin" || user?.canCreateServices) && ( {(auth?.rol === "admin" || user?.canCreateServices) && (
<div className="flex flex-row gap-4 flex-wrap px-4"> <div className="flex flex-row gap-4 flex-wrap">
<ProjectEnvironment projectId={projectId}> <ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button> <Button variant="outline">Project Environment</Button>
</ProjectEnvironment> </ProjectEnvironment>

View File

@@ -0,0 +1,220 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { DialogFooter } from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { Settings } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import React, { useEffect, type ReactElement } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import superjson from "superjson";
import { z } from "zod";
const settings = z.object({
cleanCacheOnApplications: z.boolean(),
cleanCacheOnCompose: z.boolean(),
cleanCacheOnPreviews: z.boolean(),
});
type SettingsType = z.infer<typeof settings>;
const Page = () => {
const { data, refetch } = api.admin.one.useQuery();
const { mutateAsync, isLoading, isError, error } =
api.admin.update.useMutation();
const form = useForm<SettingsType>({
defaultValues: {
cleanCacheOnApplications: false,
cleanCacheOnCompose: false,
cleanCacheOnPreviews: false,
},
resolver: zodResolver(settings),
});
useEffect(() => {
form.reset({
cleanCacheOnApplications: data?.cleanupCacheApplications || false,
cleanCacheOnCompose: data?.cleanupCacheOnCompose || false,
cleanCacheOnPreviews: data?.cleanupCacheOnPreviews || false,
});
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (values: SettingsType) => {
await mutateAsync({
cleanupCacheApplications: values.cleanCacheOnApplications,
cleanupCacheOnCompose: values.cleanCacheOnCompose,
cleanupCacheOnPreviews: values.cleanCacheOnPreviews,
})
.then(() => {
toast.success("Settings updated");
refetch();
})
.catch(() => {
toast.error("Something went wrong");
});
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<Settings className="size-6 text-muted-foreground self-center" />
Settings
</CardTitle>
<CardDescription>Manage your Dokploy settings</CardDescription>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
<Form {...form}>
<form
id="hook-form-add-security"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-2"
>
<FormField
control={form.control}
name="cleanCacheOnApplications"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Applications</FormLabel>
<FormDescription>
Clean the cache after every application deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnPreviews"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Previews</FormLabel>
<FormDescription>
Clean the cache after every preview deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="cleanCacheOnCompose"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Clean Cache on Compose</FormLabel>
<FormDescription>
Clean the cache after every compose deployment
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-add-security"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</CardContent>
</div>
</Card>
</div>
);
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Server">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
if (user.rol === "user") {
return {
redirect: {
permanent: true,
destination: "/dashboard/settings/profile",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -8,11 +8,7 @@ import type { ReactElement } from "react";
import superjson from "superjson"; import superjson from "superjson";
const Dashboard = () => { const Dashboard = () => {
return ( return <SwarmMonitorCard />;
<>
<SwarmMonitorCard />
</>
);
}; };
export default Dashboard; export default Dashboard;

View File

@@ -239,7 +239,10 @@ export const projectRouter = createTRPCRouter({
} }
}), }),
}); });
function buildServiceFilter(fieldName: AnyPgColumn, accessedServices: string[]) { function buildServiceFilter(
fieldName: AnyPgColumn,
accessedServices: string[],
) {
return accessedServices.length > 0 return accessedServices.length > 0
? sql`${fieldName} IN (${sql.join( ? sql`${fieldName} IN (${sql.join(
accessedServices.map((serviceId) => sql`${serviceId}`), accessedServices.map((serviceId) => sql`${serviceId}`),

View File

@@ -1269,7 +1269,8 @@ export const templates: TemplateData[] = [
}, },
tags: ["cloud", "networking", "security", "tunnel"], tags: ["cloud", "networking", "security", "tunnel"],
load: () => import("./cloudflared/index").then((m) => m.generate), load: () => import("./cloudflared/index").then((m) => m.generate),
},{ },
{
id: "couchdb", id: "couchdb",
name: "CouchDB", name: "CouchDB",
version: "latest", version: "latest",

View File

@@ -31,6 +31,15 @@ export const admins = pgTable("admin", {
stripeCustomerId: text("stripeCustomerId"), stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(true),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
}); });
export const adminsRelations = relations(admins, ({ one, many }) => ({ export const adminsRelations = relations(admins, ({ one, many }) => ({

View File

@@ -40,7 +40,7 @@ import { createTraefikConfig } from "@dokploy/server/utils/traefik/application";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils"; import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin"; import { findAdminById, getDokployUrl } from "./admin";
import { import {
createDeployment, createDeployment,
createDeploymentPreview, createDeploymentPreview,
@@ -58,6 +58,7 @@ import {
updatePreviewDeployment, updatePreviewDeployment,
} from "./preview-deployment"; } from "./preview-deployment";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { cleanupFullDocker } from "./settings";
export type Application = typeof applications.$inferSelect; export type Application = typeof applications.$inferSelect;
export const createApplication = async ( export const createApplication = async (
@@ -213,7 +214,7 @@ export const deployApplication = async ({
applicationType: "application", applicationType: "application",
buildLink, buildLink,
adminId: application.project.adminId, adminId: application.project.adminId,
domains: application.domains domains: application.domains,
}); });
} catch (error) { } catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
@@ -229,6 +230,12 @@ export const deployApplication = async ({
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
} }
return true; return true;
@@ -270,6 +277,12 @@ export const rebuildApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
throw error; throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
} }
return true; return true;
@@ -333,7 +346,7 @@ export const deployRemoteApplication = async ({
applicationType: "application", applicationType: "application",
buildLink, buildLink,
adminId: application.project.adminId, adminId: application.project.adminId,
domains: application.domains domains: application.domains,
}); });
} catch (error) { } catch (error) {
// @ts-ignore // @ts-ignore
@@ -359,15 +372,13 @@ export const deployRemoteApplication = async ({
adminId: application.project.adminId, adminId: application.project.adminId,
}); });
console.log(
"Error on ",
application.buildType,
"/",
application.sourceType,
error,
);
throw error; throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
} }
return true; return true;
@@ -475,6 +486,12 @@ export const deployPreviewApplication = async ({
previewStatus: "error", previewStatus: "error",
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheOnPreviews) {
await cleanupFullDocker(application?.serverId);
}
} }
return true; return true;
@@ -587,6 +604,12 @@ export const deployRemotePreviewApplication = async ({
previewStatus: "error", previewStatus: "error",
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheOnPreviews) {
await cleanupFullDocker(application?.serverId);
}
} }
return true; return true;
@@ -634,6 +657,12 @@ export const rebuildRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
throw error; throw error;
} finally {
const admin = await findAdminById(application.project.adminId);
if (admin.cleanupCacheApplications) {
await cleanupFullDocker(application?.serverId);
}
} }
return true; return true;

View File

@@ -3,7 +3,6 @@ import { paths } from "@dokploy/server/constants";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates/utils";
import { import {
buildCompose, buildCompose,
getBuildComposeCommand, getBuildComposeCommand,
@@ -45,9 +44,10 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { encodeBase64 } from "../utils/docker/utils"; import { encodeBase64 } from "../utils/docker/utils";
import { getDokployUrl } from "./admin"; import { findAdminById, getDokployUrl } from "./admin";
import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment";
import { validUniqueServerAppName } from "./project"; import { validUniqueServerAppName } from "./project";
import { cleanupFullDocker } from "./settings";
export type Compose = typeof compose.$inferSelect; export type Compose = typeof compose.$inferSelect;
@@ -260,6 +260,11 @@ export const deployCompose = async ({
adminId: compose.project.adminId, adminId: compose.project.adminId,
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
} }
}; };
@@ -296,6 +301,11 @@ export const rebuildCompose = async ({
composeStatus: "error", composeStatus: "error",
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
} }
return true; return true;
@@ -394,6 +404,11 @@ export const deployRemoteCompose = async ({
adminId: compose.project.adminId, adminId: compose.project.adminId,
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
} }
}; };
@@ -438,6 +453,11 @@ export const rebuildRemoteCompose = async ({
composeStatus: "error", composeStatus: "error",
}); });
throw error; throw error;
} finally {
const admin = await findAdminById(compose.project.adminId);
if (admin.cleanupCacheOnCompose) {
await cleanupFullDocker(compose?.serverId);
}
} }
return true; return true;

View File

@@ -5,6 +5,7 @@ import {
execAsync, execAsync,
execAsyncRemote, execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import { findAdminById } from "./admin";
// import packageInfo from "../../../package.json"; // import packageInfo from "../../../package.json";
export interface IUpdateData { export interface IUpdateData {
@@ -213,3 +214,35 @@ echo "$json_output"
} }
return result; return result;
}; };
export const cleanupFullDocker = async (serverId?: string | null) => {
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 --all --force --volumes";
const cleanupBuilder = "docker builder prune --all --force";
try {
if (serverId) {
await execAsyncRemote(
serverId,
`
${cleanupImages}
${cleanupVolumes}
${cleanupContainers}
${cleanupSystem}
${cleanupBuilder}
`,
);
}
await execAsync(`
${cleanupImages}
${cleanupVolumes}
${cleanupContainers}
${cleanupSystem}
${cleanupBuilder}
`);
} catch (error) {
console.log(error);
}
};

View File

@@ -73,7 +73,8 @@ export const canPerformCreationService = async (
userId: string, userId: string,
projectId: string, projectId: string,
) => { ) => {
const { accessedProjects, canCreateServices } = await findUserByAuthId(userId); const { accessedProjects, canCreateServices } =
await findUserByAuthId(userId);
const haveAccessToProject = accessedProjects.includes(projectId); const haveAccessToProject = accessedProjects.includes(projectId);
if (canCreateServices && haveAccessToProject) { if (canCreateServices && haveAccessToProject) {
@@ -101,7 +102,8 @@ export const canPeformDeleteService = async (
authId: string, authId: string,
serviceId: string, serviceId: string,
) => { ) => {
const { accessedServices, canDeleteServices } = await findUserByAuthId(authId); const { accessedServices, canDeleteServices } =
await findUserByAuthId(authId);
const haveAccessToService = accessedServices.includes(serviceId); const haveAccessToService = accessedServices.includes(serviceId);
if (canDeleteServices && haveAccessToService) { if (canDeleteServices && haveAccessToService) {

View File

@@ -2,8 +2,8 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema"; import { notifications } from "@dokploy/server/db/schema";
import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed"; import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed";
import { renderAsync } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
@@ -143,7 +143,7 @@ export const sendBuildErrorNotifications = async ({
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`, `<b>⚠️ Build Failed</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`,
inlineButton inlineButton,
); );
} }

View File

@@ -1,10 +1,10 @@
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema"; import { notifications } from "@dokploy/server/db/schema";
import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success"; import BuildSuccessEmail from "@dokploy/server/emails/emails/build-success";
import { Domain } from "@dokploy/server/services/domain"; import type { Domain } from "@dokploy/server/services/domain";
import { renderAsync } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
@@ -28,7 +28,7 @@ export const sendBuildSuccessNotifications = async ({
applicationType, applicationType,
buildLink, buildLink,
adminId, adminId,
domains domains,
}: Props) => { }: Props) => {
const date = new Date(); const date = new Date();
const unixDate = ~~(Number(date) / 1000); const unixDate = ~~(Number(date) / 1000);
@@ -128,8 +128,9 @@ export const sendBuildSuccessNotifications = async ({
if (telegram) { if (telegram) {
const chunkArray = <T>(array: T[], chunkSize: number): T[][] => const chunkArray = <T>(array: T[], chunkSize: number): T[][] =>
Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => array.slice(i * chunkSize, i * chunkSize + chunkSize) Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) =>
); array.slice(i * chunkSize, i * chunkSize + chunkSize),
);
const inlineButton = [ const inlineButton = [
[ [
@@ -142,14 +143,14 @@ export const sendBuildSuccessNotifications = async ({
chunk.map((data) => ({ chunk.map((data) => ({
text: data.host, text: data.host,
url: `${data.https ? "https" : "http"}://${data.host}`, url: `${data.https ? "https" : "http"}://${data.host}`,
})) })),
), ),
]; ];
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`, `<b>✅ Build Success</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${applicationType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
inlineButton inlineButton,
); );
} }

View File

@@ -3,8 +3,8 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema"; import { notifications } from "@dokploy/server/db/schema";
import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup";
import { renderAsync } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
@@ -147,7 +147,9 @@ export const sendDatabaseBackupNotifications = async ({
const statusEmoji = type === "success" ? "✅" : "❌"; const statusEmoji = type === "success" ? "✅" : "❌";
const typeStatus = type === "success" ? "Successful" : "Failed"; const typeStatus = type === "success" ? "Successful" : "Failed";
const errorMsg = isError ? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>` : ""; const errorMsg = isError
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
: "";
const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`; const messageText = `<b>${statusEmoji} Database Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Type:</b> ${databaseType}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;

View File

@@ -2,8 +2,8 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema"; import { notifications } from "@dokploy/server/db/schema";
import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup"; import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup";
import { renderAsync } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { and, eq } from "drizzle-orm";
import { format } from "date-fns"; import { format } from "date-fns";
import { and, eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
@@ -96,7 +96,7 @@ export const sendDockerCleanupNotifications = async (
if (telegram) { if (telegram) {
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}` `<b>✅ Docker Cleanup</b>\n\n<b>Message:</b> ${message}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
); );
} }

View File

@@ -2,6 +2,7 @@ import { db } from "@dokploy/server/db";
import { notifications } from "@dokploy/server/db/schema"; import { notifications } from "@dokploy/server/db/schema";
import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart"; import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart";
import { renderAsync } from "@react-email/components"; import { renderAsync } from "@react-email/components";
import { format } from "date-fns";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { import {
sendDiscordNotification, sendDiscordNotification,
@@ -10,7 +11,6 @@ import {
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
import { format } from "date-fns";
export const sendDokployRestartNotifications = async () => { export const sendDokployRestartNotifications = async () => {
const date = new Date(); const date = new Date();
@@ -80,7 +80,7 @@ export const sendDokployRestartNotifications = async () => {
if (telegram) { if (telegram) {
await sendTelegramNotification( await sendTelegramNotification(
telegram, telegram,
`<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}` `<b>✅ Dokploy Server Restarted</b>\n\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}`,
); );
} }

View File

@@ -59,7 +59,7 @@ export const sendTelegramNotification = async (
inlineButton?: { inlineButton?: {
text: string; text: string;
url: string; url: string;
}[][] }[][],
) => { ) => {
try { try {
const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`; const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;