-
- Dokploy
+
+
+ {whitelabeling?.appName || "Dokploy"}
+
Reset Password
diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts
index f123fde85..e80bf9e37 100644
--- a/apps/dokploy/server/api/root.ts
+++ b/apps/dokploy/server/api/root.ts
@@ -25,6 +25,7 @@ import { organizationRouter } from "./routers/organization";
import { patchRouter } from "./routers/patch";
import { licenseKeyRouter } from "./routers/proprietary/license-key";
import { ssoRouter } from "./routers/proprietary/sso";
+import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
import { portRouter } from "./routers/port";
import { postgresRouter } from "./routers/postgres";
import { previewDeploymentRouter } from "./routers/preview-deployment";
@@ -87,6 +88,7 @@ export const appRouter = createTRPCRouter({
organization: organizationRouter,
licenseKey: licenseKeyRouter,
sso: ssoRouter,
+ whitelabeling: whitelabelingRouter,
schedule: scheduleRouter,
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
diff --git a/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts b/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts
new file mode 100644
index 000000000..b504ed0f5
--- /dev/null
+++ b/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts
@@ -0,0 +1,106 @@
+import {
+ getWebServerSettings,
+ IS_CLOUD,
+ updateWebServerSettings,
+} from "@dokploy/server";
+import { TRPCError } from "@trpc/server";
+import { apiUpdateWhitelabeling } from "@/server/db/schema";
+import {
+ createTRPCRouter,
+ enterpriseProcedure,
+ protectedProcedure,
+ publicProcedure,
+} from "../../trpc";
+
+export const whitelabelingRouter = createTRPCRouter({
+ get: protectedProcedure.query(async () => {
+ if (IS_CLOUD) {
+ return null;
+ }
+ const settings = await getWebServerSettings();
+ return settings?.whitelabelingConfig ?? null;
+ }),
+
+ update: enterpriseProcedure
+ .input(apiUpdateWhitelabeling)
+ .mutation(async ({ input, ctx }) => {
+ if (IS_CLOUD) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Whitelabeling is not available in Cloud",
+ });
+ }
+
+ if (ctx.user.role !== "owner") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Only the owner can update whitelabeling settings",
+ });
+ }
+
+ await updateWebServerSettings({
+ whitelabelingConfig: input.whitelabelingConfig,
+ });
+
+ return { success: true };
+ }),
+
+ reset: enterpriseProcedure.mutation(async ({ ctx }) => {
+ if (IS_CLOUD) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Whitelabeling is not available in Cloud",
+ });
+ }
+
+ if (ctx.user.role !== "owner") {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Only the owner can reset whitelabeling settings",
+ });
+ }
+
+ await updateWebServerSettings({
+ whitelabelingConfig: {
+ appName: null,
+ appDescription: null,
+ logoUrl: null,
+ faviconUrl: null,
+ customCss: null,
+ loginLogoUrl: null,
+ supportUrl: null,
+ docsUrl: null,
+ errorPageTitle: null,
+ errorPageDescription: null,
+ metaTitle: null,
+ footerText: null,
+ },
+ });
+
+ return { success: true };
+ }),
+
+ // Public endpoint only for unauthenticated pages (login, register, error)
+ // Returns only the fields needed for public pages
+ getPublic: publicProcedure.query(async () => {
+ if (IS_CLOUD) {
+ return null;
+ }
+ const settings = await getWebServerSettings();
+ const config = settings?.whitelabelingConfig;
+ if (!config) return null;
+
+ return {
+ appName: config.appName,
+ appDescription: config.appDescription,
+ logoUrl: config.logoUrl,
+ loginLogoUrl: config.loginLogoUrl,
+ faviconUrl: config.faviconUrl,
+ customCss: config.customCss,
+ metaTitle: config.metaTitle,
+ errorPageTitle: config.errorPageTitle,
+ errorPageDescription: config.errorPageDescription,
+ footerText: config.footerText,
+ };
+ }),
+});
diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts
index f67b62925..067f57bbc 100644
--- a/apps/dokploy/server/api/routers/user.ts
+++ b/apps/dokploy/server/api/routers/user.ts
@@ -101,7 +101,10 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
- session: protectedProcedure.query(async ({ ctx }) => {
+ session: publicProcedure.query(async ({ ctx }) => {
+ if (!ctx.user || !ctx.session || !ctx.session.activeOrganizationId) {
+ return null;
+ }
return {
user: {
id: ctx.user.id,
diff --git a/apps/dokploy/utils/hooks/use-whitelabeling.ts b/apps/dokploy/utils/hooks/use-whitelabeling.ts
new file mode 100644
index 000000000..0970f04f9
--- /dev/null
+++ b/apps/dokploy/utils/hooks/use-whitelabeling.ts
@@ -0,0 +1,25 @@
+import { api } from "@/utils/api";
+
+/**
+ * Hook to access whitelabeling config for authenticated pages (dashboard, services, etc.).
+ * Requires the user to be logged in.
+ */
+export function useWhitelabeling() {
+ const { data, ...rest } = api.whitelabeling.get.useQuery(undefined, {
+ staleTime: 5 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ });
+ return { config: data ?? null, ...rest };
+}
+
+/**
+ * Hook to access the public whitelabeling config.
+ * Only for unauthenticated pages (login, register, error, invitation, password reset).
+ */
+export function useWhitelabelingPublic() {
+ const { data, ...rest } = api.whitelabeling.getPublic.useQuery(undefined, {
+ staleTime: 5 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ });
+ return { config: data ?? null, ...rest };
+}
diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts
index 3d28c4e15..903e02ae1 100644
--- a/packages/server/src/constants/index.ts
+++ b/packages/server/src/constants/index.ts
@@ -2,22 +2,23 @@ import path from "node:path";
import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true";
-export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION;
-export const DOCKER_HOST = process.env.DOCKER_HOST;
-export const DOCKER_PORT = process.env.DOCKER_PORT
- ? Number(process.env.DOCKER_PORT)
+export const DOKPLOY_DOCKER_API_VERSION =
+ process.env.DOKPLOY_DOCKER_API_VERSION;
+export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
+export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
+ ? Number(process.env.DOKPLOY_DOCKER_PORT)
: undefined;
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const docker = new Docker({
- ...(DOCKER_API_VERSION && {
- version: DOCKER_API_VERSION,
+ ...(DOKPLOY_DOCKER_API_VERSION && {
+ version: DOKPLOY_DOCKER_API_VERSION,
}),
- ...(DOCKER_HOST && {
- host: DOCKER_HOST,
+ ...(DOKPLOY_DOCKER_HOST && {
+ host: DOKPLOY_DOCKER_HOST,
}),
- ...(DOCKER_PORT && {
- port: DOCKER_PORT,
+ ...(DOKPLOY_DOCKER_PORT && {
+ port: DOKPLOY_DOCKER_PORT,
}),
});
diff --git a/packages/server/src/db/schema/web-server-settings.ts b/packages/server/src/db/schema/web-server-settings.ts
index fe5cc5ad1..f36f75660 100644
--- a/packages/server/src/db/schema/web-server-settings.ts
+++ b/packages/server/src/db/schema/web-server-settings.ts
@@ -66,6 +66,36 @@ export const webServerSettings = pgTable("webServerSettings", {
},
},
}),
+ // Whitelabeling Configuration (Enterprise / Proprietary)
+ whitelabelingConfig: jsonb("whitelabelingConfig")
+ .$type<{
+ appName: string | null;
+ appDescription: string | null;
+ logoUrl: string | null;
+ faviconUrl: string | null;
+ customCss: string | null;
+ loginLogoUrl: string | null;
+ supportUrl: string | null;
+ docsUrl: string | null;
+ errorPageTitle: string | null;
+ errorPageDescription: string | null;
+ metaTitle: string | null;
+ footerText: string | null;
+ }>()
+ .default({
+ appName: null,
+ appDescription: null,
+ logoUrl: null,
+ faviconUrl: null,
+ customCss: null,
+ loginLogoUrl: null,
+ supportUrl: null,
+ docsUrl: null,
+ errorPageTitle: null,
+ errorPageDescription: null,
+ metaTitle: null,
+ footerText: null,
+ }),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
@@ -154,6 +184,33 @@ export const apiUpdateDockerCleanup = z.object({
serverId: z.string().optional(),
});
+// Whitelabeling validation schemas
+const safeUrl = z
+ .string()
+ .refine((url) => /^https?:\/\//i.test(url), {
+ message: "Only http:// and https:// URLs are allowed",
+ })
+ .nullable();
+
+export const whitelabelingConfigSchema = z.object({
+ appName: z.string().nullable(),
+ appDescription: z.string().nullable(),
+ logoUrl: safeUrl,
+ faviconUrl: safeUrl,
+ customCss: z.string().nullable(),
+ loginLogoUrl: safeUrl,
+ supportUrl: safeUrl,
+ docsUrl: safeUrl,
+ errorPageTitle: z.string().nullable(),
+ errorPageDescription: z.string().nullable(),
+ metaTitle: z.string().nullable(),
+ footerText: z.string().nullable(),
+});
+
+export const apiUpdateWhitelabeling = z.object({
+ whitelabelingConfig: whitelabelingConfigSchema,
+});
+
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
diff --git a/packages/server/src/verification/send-verification-email.tsx b/packages/server/src/verification/send-verification-email.tsx
index c673c0f77..d38c2cdfc 100644
--- a/packages/server/src/verification/send-verification-email.tsx
+++ b/packages/server/src/verification/send-verification-email.tsx
@@ -1,7 +1,4 @@
-import {
- sendDiscordNotification,
- sendEmailNotification,
-} from "../utils/notifications/utils";
+import { sendEmailNotification } from "../utils/notifications/utils";
export const sendEmail = async ({
email,
subject,
@@ -26,26 +23,3 @@ export const sendEmail = async ({
return true;
};
-
-export const sendDiscordNotificationWelcome = async (email: string) => {
- await sendDiscordNotification(
- {
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
- },
- {
- title: "New User Registered",
- color: 0x00ff00,
- fields: [
- {
- name: "Email",
- value: email,
- inline: true,
- },
- ],
- timestamp: new Date(),
- footer: {
- text: "Dokploy User Registration Notification",
- },
- },
- );
-};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3ccae53df..710e0b548 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -119,6 +119,9 @@ importers:
'@codemirror/autocomplete':
specifier: ^6.18.6
version: 6.20.0
+ '@codemirror/lang-css':
+ specifier: ^6.3.1
+ version: 6.3.1
'@codemirror/lang-json':
specifier: ^6.0.1
version: 6.0.2
@@ -1264,6 +1267,9 @@ packages:
'@codemirror/commands@6.10.2':
resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
+ '@codemirror/lang-css@6.3.1':
+ resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
+
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
@@ -1816,6 +1822,9 @@ packages:
'@lezer/common@1.5.1':
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
+ '@lezer/css@1.3.1':
+ resolution: {integrity: sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==}
+
'@lezer/highlight@1.2.3':
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
@@ -8803,6 +8812,14 @@ snapshots:
'@codemirror/view': 6.39.15
'@lezer/common': 1.5.1
+ '@codemirror/lang-css@6.3.1':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.0
+ '@codemirror/language': 6.12.1
+ '@codemirror/state': 6.5.4
+ '@lezer/common': 1.5.1
+ '@lezer/css': 1.3.1
+
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.12.1
@@ -9294,6 +9311,12 @@ snapshots:
'@lezer/common@1.5.1': {}
+ '@lezer/css@1.3.1':
+ dependencies:
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+
'@lezer/highlight@1.2.3':
dependencies:
'@lezer/common': 1.5.1