diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index c966748a..fac90cc7 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -14,6 +14,9 @@ import { import { beforeEach, expect, test, vi } from "vitest"; const baseAdmin: Admin = { + cleanupCacheApplications: false, + cleanupCacheOnCompose: false, + cleanupCacheOnPreviews: false, createdAt: "", authId: "", adminId: "string", diff --git a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx index 9e493529..21fe28d4 100644 --- a/apps/dokploy/components/dashboard/database/backups/show-backups.tsx +++ b/apps/dokploy/components/dashboard/database/backups/show-backups.tsx @@ -75,7 +75,7 @@ export const ShowBackups = ({ id, type }: Props) => { {data?.length === 0 ? (
- + To create a backup it is required to set at least 1 provider. Please, go to{" "} -
+
CPU Usage diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index f7a6a546..90d17364 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -133,7 +133,9 @@ export const AddTemplate = ({ projectId }: Props) => { diff --git a/apps/dokploy/pages/dashboard/settings/index.tsx b/apps/dokploy/pages/dashboard/settings/index.tsx new file mode 100644 index 00000000..bf76607b --- /dev/null +++ b/apps/dokploy/pages/dashboard/settings/index.tsx @@ -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; + +const Page = () => { + const { data, refetch } = api.admin.one.useQuery(); + const { mutateAsync, isLoading, isError, error } = + api.admin.update.useMutation(); + const form = useForm({ + 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 ( +
+ +
+ + + + Settings + + Manage your Dokploy settings + {isError && {error?.message}} + + +
+ + ( + +
+ Clean Cache on Applications + + Clean the cache after every application deployment + +
+ + + +
+ )} + /> + ( + +
+ Clean Cache on Previews + + Clean the cache after every preview deployment + +
+ + + +
+ )} + /> + ( + +
+ Clean Cache on Compose + + Clean the cache after every compose deployment + +
+ + + +
+ )} + /> + + + + + +
+
+
+
+ ); +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return {page}; +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }>, +) { + const { req, res } = ctx; + const { user, session } = await validateRequest(ctx.req, 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(), + }, + }; +} diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index f40a0a83..3a8a60b2 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -8,11 +8,7 @@ import type { ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { - return ( - <> - - - ); + return ; }; export default Dashboard; diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 19acb9a3..9c2608cc 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -239,7 +239,10 @@ export const projectRouter = createTRPCRouter({ } }), }); -function buildServiceFilter(fieldName: AnyPgColumn, accessedServices: string[]) { +function buildServiceFilter( + fieldName: AnyPgColumn, + accessedServices: string[], +) { return accessedServices.length > 0 ? sql`${fieldName} IN (${sql.join( accessedServices.map((serviceId) => sql`${serviceId}`), diff --git a/apps/dokploy/styles/globals.css b/apps/dokploy/styles/globals.css index 3910d69a..7b7977b9 100644 --- a/apps/dokploy/styles/globals.css +++ b/apps/dokploy/styles/globals.css @@ -101,7 +101,7 @@ * { @apply border-border; } - + body { @apply bg-background text-foreground; } @@ -110,16 +110,16 @@ ::-webkit-scrollbar { width: 0.3125rem; } - + ::-webkit-scrollbar-track { background: transparent; } - + ::-webkit-scrollbar-thumb { background: hsl(var(--border)); border-radius: 0.3125rem; } - + * { scrollbar-width: thin; scrollbar-color: hsl(var(--border)) transparent; diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 6238a19c..9531eb7a 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1269,7 +1269,8 @@ export const templates: TemplateData[] = [ }, tags: ["cloud", "networking", "security", "tunnel"], load: () => import("./cloudflared/index").then((m) => m.generate), - },{ + }, + { id: "couchdb", name: "CouchDB", version: "latest", diff --git a/packages/server/src/db/schema/admin.ts b/packages/server/src/db/schema/admin.ts index 222fb16c..e9c73bcc 100644 --- a/packages/server/src/db/schema/admin.ts +++ b/packages/server/src/db/schema/admin.ts @@ -31,6 +31,15 @@ export const admins = pgTable("admin", { stripeCustomerId: text("stripeCustomerId"), stripeSubscriptionId: text("stripeSubscriptionId"), 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 }) => ({ diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 068e69b3..ccadebf7 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -40,7 +40,7 @@ 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 { findAdminById, getDokployUrl } from "./admin"; import { createDeployment, createDeploymentPreview, @@ -58,6 +58,7 @@ import { updatePreviewDeployment, } from "./preview-deployment"; import { validUniqueServerAppName } from "./project"; +import { cleanupFullDocker } from "./settings"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( @@ -213,7 +214,7 @@ export const deployApplication = async ({ applicationType: "application", buildLink, adminId: application.project.adminId, - domains: application.domains + domains: application.domains, }); } catch (error) { await updateDeploymentStatus(deployment.deploymentId, "error"); @@ -229,6 +230,12 @@ export const deployApplication = async ({ }); throw error; + } finally { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } } return true; @@ -270,6 +277,12 @@ export const rebuildApplication = async ({ await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); throw error; + } finally { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } } return true; @@ -333,7 +346,7 @@ export const deployRemoteApplication = async ({ applicationType: "application", buildLink, adminId: application.project.adminId, - domains: application.domains + domains: application.domains, }); } catch (error) { // @ts-ignore @@ -359,15 +372,13 @@ export const deployRemoteApplication = async ({ adminId: application.project.adminId, }); - console.log( - "Error on ", - application.buildType, - "/", - application.sourceType, - error, - ); - throw error; + } finally { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } } return true; @@ -475,6 +486,12 @@ export const deployPreviewApplication = async ({ previewStatus: "error", }); throw error; + } finally { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheOnPreviews) { + await cleanupFullDocker(application?.serverId); + } } return true; @@ -587,6 +604,12 @@ export const deployRemotePreviewApplication = async ({ previewStatus: "error", }); throw error; + } finally { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheOnPreviews) { + await cleanupFullDocker(application?.serverId); + } } return true; @@ -634,6 +657,12 @@ export const rebuildRemoteApplication = async ({ await updateDeploymentStatus(deployment.deploymentId, "error"); await updateApplicationStatus(applicationId, "error"); throw error; + } finally { + const admin = await findAdminById(application.project.adminId); + + if (admin.cleanupCacheApplications) { + await cleanupFullDocker(application?.serverId); + } } return true; diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 8561dd37..7f6a5954 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -3,7 +3,6 @@ import { paths } from "@dokploy/server/constants"; import { db } from "@dokploy/server/db"; import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; -import { generatePassword } from "@dokploy/server/templates/utils"; import { buildCompose, getBuildComposeCommand, @@ -45,9 +44,10 @@ import { import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { encodeBase64 } from "../utils/docker/utils"; -import { getDokployUrl } from "./admin"; +import { findAdminById, getDokployUrl } from "./admin"; import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; import { validUniqueServerAppName } from "./project"; +import { cleanupFullDocker } from "./settings"; export type Compose = typeof compose.$inferSelect; @@ -260,6 +260,11 @@ export const deployCompose = async ({ adminId: compose.project.adminId, }); 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", }); throw error; + } finally { + const admin = await findAdminById(compose.project.adminId); + if (admin.cleanupCacheOnCompose) { + await cleanupFullDocker(compose?.serverId); + } } return true; @@ -394,6 +404,11 @@ export const deployRemoteCompose = async ({ adminId: compose.project.adminId, }); 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", }); throw error; + } finally { + const admin = await findAdminById(compose.project.adminId); + if (admin.cleanupCacheOnCompose) { + await cleanupFullDocker(compose?.serverId); + } } return true; diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 37f7b2ee..d22780c9 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -5,6 +5,7 @@ import { execAsync, execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; +import { findAdminById } from "./admin"; // import packageInfo from "../../../package.json"; export interface IUpdateData { @@ -213,3 +214,35 @@ echo "$json_output" } 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); + } +}; diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index c8a9849c..d8d9862c 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -73,7 +73,8 @@ export const canPerformCreationService = async ( userId: string, projectId: string, ) => { - const { accessedProjects, canCreateServices } = await findUserByAuthId(userId); + const { accessedProjects, canCreateServices } = + await findUserByAuthId(userId); const haveAccessToProject = accessedProjects.includes(projectId); if (canCreateServices && haveAccessToProject) { @@ -101,7 +102,8 @@ export const canPeformDeleteService = async ( authId: string, serviceId: string, ) => { - const { accessedServices, canDeleteServices } = await findUserByAuthId(authId); + const { accessedServices, canDeleteServices } = + await findUserByAuthId(authId); const haveAccessToService = accessedServices.includes(serviceId); if (canDeleteServices && haveAccessToService) { diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 2ab2125c..95936652 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -2,8 +2,8 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import BuildFailedEmail from "@dokploy/server/emails/emails/build-failed"; import { renderAsync } from "@react-email/components"; -import { and, eq } from "drizzle-orm"; import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, @@ -139,11 +139,11 @@ export const sendBuildErrorNotifications = async ({ }, ], ]; - + 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 + inlineButton, ); } diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 19b17811..960f7a6a 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -1,10 +1,10 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; 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 { and, eq } from "drizzle-orm"; import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, @@ -28,7 +28,7 @@ export const sendBuildSuccessNotifications = async ({ applicationType, buildLink, adminId, - domains + domains, }: Props) => { const date = new Date(); const unixDate = ~~(Number(date) / 1000); @@ -128,9 +128,10 @@ export const sendBuildSuccessNotifications = async ({ 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) - ); - + Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, i) => + array.slice(i * chunkSize, i * chunkSize + chunkSize), + ); + const inlineButton = [ [ { @@ -142,14 +143,14 @@ export const sendBuildSuccessNotifications = async ({ chunk.map((data) => ({ text: data.host, url: `${data.https ? "https" : "http"}://${data.host}`, - })) + })), ), ]; - + await sendTelegramNotification( telegram, - `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, - inlineButton + `✅ Build Success\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, + inlineButton, ); } diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 1460964d..0b1d61f7 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -3,8 +3,8 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DatabaseBackupEmail from "@dokploy/server/emails/emails/database-backup"; import { renderAsync } from "@react-email/components"; -import { and, eq } from "drizzle-orm"; import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, @@ -144,13 +144,15 @@ export const sendDatabaseBackupNotifications = async ({ 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 errorMsg = isError + ? `\n\nError:\n
${errorMessage}
` + : ""; + const messageText = `${statusEmoji} Database Backup ${typeStatus}\n\nProject: ${projectName}\nApplication: ${applicationName}\nType: ${databaseType}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}${isError ? errorMsg : ""}`; - + await sendTelegramNotification(telegram, messageText); } diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 05624e49..b60e3b0a 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -2,8 +2,8 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DockerCleanupEmail from "@dokploy/server/emails/emails/docker-cleanup"; import { renderAsync } from "@react-email/components"; -import { and, eq } from "drizzle-orm"; import { format } from "date-fns"; +import { and, eq } from "drizzle-orm"; import { sendDiscordNotification, sendEmailNotification, @@ -96,7 +96,7 @@ export const sendDockerCleanupNotifications = async ( if (telegram) { await sendTelegramNotification( telegram, - `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}` + `✅ Docker Cleanup\n\nMessage: ${message}\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); } diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index 6debb7a7..5a156aff 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -2,6 +2,7 @@ import { db } from "@dokploy/server/db"; import { notifications } from "@dokploy/server/db/schema"; import DokployRestartEmail from "@dokploy/server/emails/emails/dokploy-restart"; import { renderAsync } from "@react-email/components"; +import { format } from "date-fns"; import { eq } from "drizzle-orm"; import { sendDiscordNotification, @@ -10,7 +11,6 @@ import { sendSlackNotification, sendTelegramNotification, } from "./utils"; -import { format } from "date-fns"; export const sendDokployRestartNotifications = async () => { const date = new Date(); @@ -80,7 +80,7 @@ export const sendDokployRestartNotifications = async () => { if (telegram) { await sendTelegramNotification( telegram, - `✅ Dokploy Server Restarted\n\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}` + `✅ Dokploy Server Restarted\n\nDate: ${format(date, "PP")}\nTime: ${format(date, "pp")}`, ); } diff --git a/packages/server/src/utils/notifications/utils.ts b/packages/server/src/utils/notifications/utils.ts index ede46034..4f8bb1a5 100644 --- a/packages/server/src/utils/notifications/utils.ts +++ b/packages/server/src/utils/notifications/utils.ts @@ -59,7 +59,7 @@ export const sendTelegramNotification = async ( inlineButton?: { text: string; url: string; - }[][] + }[][], ) => { try { const url = `https://api.telegram.org/bot${connection.botToken}/sendMessage`;