diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx
index fff5413e0..13dcb6bad 100644
--- a/apps/dokploy/components/layouts/onboarding-layout.tsx
+++ b/apps/dokploy/components/layouts/onboarding-layout.tsx
@@ -4,21 +4,35 @@ import { cn } from "@/lib/utils";
import { GithubIcon } from "../icons/data-tools-icons";
import { Logo } from "../shared/logo";
import { Button } from "../ui/button";
+import { api } from "@/utils/api";
interface Props {
children: React.ReactNode;
}
export const OnboardingLayout = ({ children }: Props) => {
+ const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
+ const appName = whitelabel?.whitelabelAppName ?? "Dokploy";
+ const logoUrl =
+ whitelabel?.whitelabelLogoUrl ?? whitelabel?.whitelabelLoginLogoUrl;
+
return (
+ {whitelabel?.whitelabelLoginBackgroundImageUrl && (
+
+ )}
-
- Dokploy
+
+ {appName}
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx
index 204bf9695..ee4372e24 100644
--- a/apps/dokploy/components/layouts/side.tsx
+++ b/apps/dokploy/components/layouts/side.tsx
@@ -21,8 +21,10 @@ import {
Key,
KeyRound,
Loader2,
+ LogIn,
type LucideIcon,
Package,
+ Palette,
PieChart,
Server,
ShieldCheck,
@@ -30,7 +32,6 @@ import {
Trash2,
User,
Users,
- LogIn,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
@@ -416,6 +417,15 @@ const MENU: Menu = {
isEnabled: ({ auth }) =>
!!(auth?.role === "owner" || auth?.role === "admin"),
},
+ {
+ isSingle: true,
+ title: "Whitelabeling",
+ url: "/dashboard/settings/whitelabelling",
+ icon: Palette,
+ // Enterprise only – page shows gate if no license
+ isEnabled: ({ auth }) =>
+ !!(auth?.role === "owner" || auth?.role === "admin"),
+ },
],
help: [
@@ -546,6 +556,7 @@ function SidebarLogo() {
refetch,
isLoading,
} = api.organization.all.useQuery();
+ const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
const { mutateAsync: deleteOrganization, isLoading: isRemoving } =
api.organization.delete.useMutation();
const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } =
@@ -611,7 +622,11 @@ function SidebarLogo() {
"transition-all",
state === "collapsed" ? "size-4" : "size-5",
)}
- logoUrl={activeOrganization?.logo || undefined}
+ logoUrl={
+ activeOrganization?.logo ||
+ whitelabel?.whitelabelLogoUrl ||
+ undefined
+ }
/>
- {activeOrganization?.name ?? "Select Organization"}
+ {activeOrganization?.name ??
+ whitelabel?.whitelabelAppName ??
+ "Select Organization"}
diff --git a/apps/dokploy/components/proprietary/whitelabelling/whitelabel-settings.tsx b/apps/dokploy/components/proprietary/whitelabelling/whitelabel-settings.tsx
new file mode 100644
index 000000000..692fb8825
--- /dev/null
+++ b/apps/dokploy/components/proprietary/whitelabelling/whitelabel-settings.tsx
@@ -0,0 +1,290 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Loader2, Palette } from "lucide-react";
+import { useEffect } from "react";
+import { useForm } from "react-hook-form";
+import { toast } from "sonner";
+import { z } from "zod";
+import { Button } from "@/components/ui/button";
+import { CardDescription, CardTitle } from "@/components/ui/card";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { api } from "@/utils/api";
+
+const whitelabelSchema = z.object({
+ whitelabelAppName: z.string().min(1).max(100),
+ whitelabelLogoUrl: z.union([z.string().url(), z.literal("")]).optional(),
+ whitelabelLoginLogoUrl: z.union([z.string().url(), z.literal("")]).optional(),
+ whitelabelFaviconUrl: z.union([z.string().url(), z.literal("")]).optional(),
+ whitelabelLoginTitle: z.string().max(200).optional(),
+ whitelabelLoginSubtitle: z.string().max(500).optional(),
+ whitelabelLoginBackgroundImageUrl: z
+ .union([z.string().url(), z.literal("")])
+ .optional(),
+});
+
+type WhitelabelFormValues = z.infer
;
+
+export function WhitelabelSettings() {
+ const { data: settings, isLoading } =
+ api.settings.getWebServerSettings.useQuery();
+ const { mutateAsync: updateWhitelabel, isLoading: isSaving } =
+ api.settings.updateWhitelabelSettings.useMutation();
+ const utils = api.useUtils();
+
+ const form = useForm({
+ resolver: zodResolver(whitelabelSchema),
+ defaultValues: {
+ whitelabelAppName: "Dokploy",
+ whitelabelLogoUrl: "",
+ whitelabelLoginLogoUrl: "",
+ whitelabelFaviconUrl: "",
+ whitelabelLoginTitle: "",
+ whitelabelLoginSubtitle: "",
+ whitelabelLoginBackgroundImageUrl: "",
+ },
+ });
+
+ useEffect(() => {
+ if (settings) {
+ form.reset({
+ whitelabelAppName: settings.whitelabelAppName ?? "Dokploy",
+ whitelabelLogoUrl: settings.whitelabelLogoUrl ?? "",
+ whitelabelLoginLogoUrl: settings.whitelabelLoginLogoUrl ?? "",
+ whitelabelFaviconUrl: settings.whitelabelFaviconUrl ?? "",
+ whitelabelLoginTitle: settings.whitelabelLoginTitle ?? "",
+ whitelabelLoginSubtitle: settings.whitelabelLoginSubtitle ?? "",
+ whitelabelLoginBackgroundImageUrl:
+ settings.whitelabelLoginBackgroundImageUrl ?? "",
+ });
+ }
+ }, [settings, form]);
+
+ const onSubmit = async (values: WhitelabelFormValues) => {
+ try {
+ await updateWhitelabel({
+ whitelabelAppName: values.whitelabelAppName || null,
+ whitelabelLogoUrl: values.whitelabelLogoUrl || undefined,
+ whitelabelLoginLogoUrl: values.whitelabelLoginLogoUrl || undefined,
+ whitelabelFaviconUrl: values.whitelabelFaviconUrl || undefined,
+ whitelabelLoginTitle: values.whitelabelLoginTitle || null,
+ whitelabelLoginSubtitle: values.whitelabelLoginSubtitle || null,
+ whitelabelLoginBackgroundImageUrl:
+ values.whitelabelLoginBackgroundImageUrl || undefined,
+ });
+ toast.success("Whitelabel settings saved");
+ utils.settings.getWebServerSettings.invalidate();
+ utils.settings.getWhitelabelSettings.invalidate();
+ } catch (e) {
+ toast.error("Failed to save whitelabel settings");
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ Loading whitelabel settings...
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Customize the application name, logos, and login page for your brand.
+ Leave URLs empty to use defaults.
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dokploy/pages/dashboard/settings/whitelabelling.tsx b/apps/dokploy/pages/dashboard/settings/whitelabelling.tsx
new file mode 100644
index 000000000..f8f3f9e10
--- /dev/null
+++ b/apps/dokploy/pages/dashboard/settings/whitelabelling.tsx
@@ -0,0 +1,84 @@
+import { validateRequest } from "@dokploy/server";
+import { createServerSideHelpers } from "@trpc/react-query/server";
+import type { GetServerSidePropsContext } from "next";
+import type { ReactElement } from "react";
+import superjson from "superjson";
+import { DashboardLayout } from "@/components/layouts/dashboard-layout";
+import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
+import { WhitelabelSettings } from "@/components/proprietary/whitelabelling/whitelabel-settings";
+import { Card } from "@/components/ui/card";
+import { appRouter } from "@/server/api/root";
+import { getLocale, serverSideTranslations } from "@/utils/i18n";
+
+const Page = () => {
+ return (
+
+ );
+};
+
+export default Page;
+
+Page.getLayout = (page: ReactElement) => {
+ return {page};
+};
+
+export async function getServerSideProps(ctx: GetServerSidePropsContext) {
+ const { req } = ctx;
+ const locale = await getLocale(req.cookies);
+ const { user, session } = await validateRequest(ctx.req);
+ if (!user) {
+ return {
+ redirect: {
+ permanent: true,
+ destination: "/",
+ },
+ };
+ }
+ if (user.role === "member") {
+ return {
+ redirect: {
+ permanent: true,
+ destination: "/dashboard/settings/profile",
+ },
+ };
+ }
+
+ const helpers = createServerSideHelpers({
+ router: appRouter,
+ ctx: {
+ req: req as any,
+ res: ctx.res as any,
+ db: null as any,
+ session: session as any,
+ user: user as any,
+ },
+ transformer: superjson,
+ });
+ await helpers.user.get.prefetch();
+
+ return {
+ props: {
+ trpcState: helpers.dehydrate(),
+ ...(await serverSideTranslations(locale, ["settings"])),
+ },
+ };
+}
diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx
index de4294581..cf99b60ce 100644
--- a/apps/dokploy/pages/index.tsx
+++ b/apps/dokploy/pages/index.tsx
@@ -59,6 +59,7 @@ interface Props {
export default function Home({ IS_CLOUD }: Props) {
const router = useRouter();
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
+ const { data: whitelabel } = api.settings.getWhitelabelSettings.useQuery();
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
const [isBackupCodeLoading, setIsBackupCodeLoading] = useState(false);
@@ -212,17 +213,27 @@ export default function Home({ IS_CLOUD }: Props) {
>
);
+ const loginLogoUrl =
+ whitelabel?.whitelabelLoginLogoUrl ?? whitelabel?.whitelabelLogoUrl;
+ const loginTitle = whitelabel?.whitelabelLoginTitle ?? "Sign in";
+ const loginSubtitle =
+ whitelabel?.whitelabelLoginSubtitle ??
+ "Enter your email and password to sign in";
+
return (
<>
-
- Sign in
+
+ {loginTitle}
- Enter your email and password to sign in
+ {loginSubtitle}
{error && (
diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts
index c9d21e515..ef2999dfd 100644
--- a/apps/dokploy/server/api/routers/settings.ts
+++ b/apps/dokploy/server/api/routers/settings.ts
@@ -63,6 +63,7 @@ import {
apiServerSchema,
apiTraefikConfig,
apiUpdateDockerCleanup,
+ apiUpdateWhitelabel,
projects,
server,
} from "@/server/db/schema";
@@ -72,6 +73,7 @@ import { appRouter } from "../root";
import {
adminProcedure,
createTRPCRouter,
+ enterpriseProcedure,
protectedProcedure,
publicProcedure,
} from "../trpc";
@@ -84,6 +86,57 @@ export const settingsRouter = createTRPCRouter({
const settings = await getWebServerSettings();
return settings;
}),
+ getWhitelabelSettings: publicProcedure.query(async () => {
+ if (IS_CLOUD) {
+ return null;
+ }
+ const settings = await getWebServerSettings();
+ if (!settings) return null;
+ return {
+ whitelabelAppName: settings.whitelabelAppName ?? "Dokploy",
+ whitelabelLogoUrl: settings.whitelabelLogoUrl ?? null,
+ whitelabelLoginLogoUrl: settings.whitelabelLoginLogoUrl ?? null,
+ whitelabelFaviconUrl: settings.whitelabelFaviconUrl ?? null,
+ whitelabelLoginTitle: settings.whitelabelLoginTitle ?? null,
+ whitelabelLoginSubtitle: settings.whitelabelLoginSubtitle ?? null,
+ whitelabelLoginBackgroundImageUrl:
+ settings.whitelabelLoginBackgroundImageUrl ?? null,
+ };
+ }),
+ updateWhitelabelSettings: enterpriseProcedure
+ .input(apiUpdateWhitelabel)
+ .mutation(async ({ input }) => {
+ if (IS_CLOUD) {
+ return null;
+ }
+ const updates: Record = {};
+ if (input.whitelabelAppName !== undefined)
+ updates.whitelabelAppName = input.whitelabelAppName;
+ if (input.whitelabelLogoUrl !== undefined)
+ updates.whitelabelLogoUrl =
+ input.whitelabelLogoUrl === "" ? null : input.whitelabelLogoUrl;
+ if (input.whitelabelLoginLogoUrl !== undefined)
+ updates.whitelabelLoginLogoUrl =
+ input.whitelabelLoginLogoUrl === ""
+ ? null
+ : input.whitelabelLoginLogoUrl;
+ if (input.whitelabelFaviconUrl !== undefined)
+ updates.whitelabelFaviconUrl =
+ input.whitelabelFaviconUrl === ""
+ ? null
+ : input.whitelabelFaviconUrl;
+ if (input.whitelabelLoginTitle !== undefined)
+ updates.whitelabelLoginTitle = input.whitelabelLoginTitle;
+ if (input.whitelabelLoginSubtitle !== undefined)
+ updates.whitelabelLoginSubtitle = input.whitelabelLoginSubtitle;
+ if (input.whitelabelLoginBackgroundImageUrl !== undefined)
+ updates.whitelabelLoginBackgroundImageUrl =
+ input.whitelabelLoginBackgroundImageUrl === ""
+ ? null
+ : input.whitelabelLoginBackgroundImageUrl;
+ const updated = await updateWebServerSettings(updates as any);
+ return updated;
+ }),
reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts
index fe5cc5ad1..3dca33e3b 100644
--- a/packages/server/src/db/schema/web-server-settings.ts
+++ b/packages/server/src/db/schema/web-server-settings.ts
@@ -76,6 +76,14 @@ export const webServerSettings = pgTable("webServerSettings", {
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
+ // Whitelabel (Enterprise)
+ whitelabelAppName: text("whitelabelAppName").default("Dokploy"),
+ whitelabelLogoUrl: text("whitelabelLogoUrl"),
+ whitelabelLoginLogoUrl: text("whitelabelLoginLogoUrl"),
+ whitelabelFaviconUrl: text("whitelabelFaviconUrl"),
+ whitelabelLoginTitle: text("whitelabelLoginTitle"),
+ whitelabelLoginSubtitle: text("whitelabelLoginSubtitle"),
+ whitelabelLoginBackgroundImageUrl: text("whitelabelLoginBackgroundImageUrl"),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
@@ -125,6 +133,18 @@ export const apiUpdateWebServerSettings = createSchema.partial().extend({
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
+ whitelabelAppName: z.string().optional().nullable(),
+ whitelabelLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
+ whitelabelLoginLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
+ whitelabelFaviconUrl: z.string().url().optional().nullable().or(z.literal("")),
+ whitelabelLoginTitle: z.string().optional().nullable(),
+ whitelabelLoginSubtitle: z.string().optional().nullable(),
+ whitelabelLoginBackgroundImageUrl: z
+ .string()
+ .url()
+ .optional()
+ .nullable()
+ .or(z.literal("")),
});
export const apiAssignDomain = z
@@ -154,6 +174,21 @@ export const apiUpdateDockerCleanup = z.object({
serverId: z.string().optional(),
});
+export const apiUpdateWhitelabel = z.object({
+ whitelabelAppName: z.string().min(1).max(100).optional().nullable(),
+ whitelabelLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
+ whitelabelLoginLogoUrl: z.string().url().optional().nullable().or(z.literal("")),
+ whitelabelFaviconUrl: z.string().url().optional().nullable().or(z.literal("")),
+ whitelabelLoginTitle: z.string().max(200).optional().nullable(),
+ whitelabelLoginSubtitle: z.string().max(500).optional().nullable(),
+ whitelabelLoginBackgroundImageUrl: z
+ .string()
+ .url()
+ .optional()
+ .nullable()
+ .or(z.literal("")),
+});
+
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({