Compare commits

...

17 Commits

Author SHA1 Message Date
Mauricio Siu
9498fbeff3 Update package.json 2025-12-31 00:28:03 -06:00
Mauricio Siu
d2aa60ddf7 Update package.json 2025-12-30 23:53:30 -06:00
Mauricio Siu
58b75205af Merge pull request #3327 from Dokploy/refactor/separate-settings-from-users-table
refactor(settings): migrate user settings to webServerSettings schema…
2025-12-28 13:21:55 -06:00
Mauricio Siu
9e03625586 refactor(auth): simplify trustedOrigins logic by removing redundant admin check and using optional chaining for settings access 2025-12-28 13:18:20 -06:00
Mauricio Siu
260efdc2bb Merge pull request #3353 from bdkopen/remove-rotating-file-stream
chore: uninstall `rotating-file-stream`
2025-12-28 13:09:34 -06:00
bdkopen
1b5bfe051d chore: uninstall rotating-file-stream 2025-12-27 12:33:39 -05:00
Mauricio Siu
e4384075f2 Merge pull request #3341 from dpulpeiro/fix/stack-registry-auth
fix: pass registry auth to stack deploy
2025-12-25 03:29:33 -06:00
Mauricio Siu
b355d44605 fix(web-server-settings): use optional chaining for safer ID access in update function 2025-12-24 12:24:27 -06:00
Daniel García Pulpeiro
f39aa23803 fix: pass registry auth to stack deploy 2025-12-23 22:37:00 +01:00
Mauricio Siu
3abc4cdc3b refactor(access-log): consolidate web server settings imports and enhance log cleanup status retrieval 2025-12-21 01:46:27 -06:00
Mauricio Siu
ec56062f17 fix(settings): update getIp function to return an empty string for cloud environments 2025-12-21 01:45:49 -06:00
Mauricio Siu
10c4f882a5 Update packages/server/src/services/web-server-settings.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-21 01:44:46 -06:00
Mauricio Siu
f1dfa9c6a2 refactor(preview-deployment): remove dynamic import of getWebServerSettings and streamline IP retrieval logic 2025-12-21 01:43:09 -06:00
Mauricio Siu
6010643d9e refactor(server): update server configuration handling to utilize webServerSettings schema and improve code clarity 2025-12-21 01:41:33 -06:00
Mauricio Siu
1ccb205495 fix(admin): add optional chaining to safely access settings properties 2025-12-21 01:35:21 -06:00
autofix-ci[bot]
b2be5bc09f [autofix.ci] apply automated fixes 2025-12-21 07:33:59 +00:00
Mauricio Siu
babd30a110 refactor(settings): migrate user settings to webServerSettings schema and update related components 2025-12-21 01:33:18 -06:00
33 changed files with 7506 additions and 331 deletions

View File

@@ -5,21 +5,27 @@ vi.mock("node:fs", () => ({
default: fs, default: fs,
})); }));
import type { FileConfig, User } from "@dokploy/server"; import type { FileConfig } from "@dokploy/server";
import { import {
createDefaultServerTraefikConfig, createDefaultServerTraefikConfig,
loadOrCreateConfig, loadOrCreateConfig,
updateServerTraefik, updateServerTraefik,
} from "@dokploy/server"; } from "@dokploy/server";
import type { webServerSettings } from "@dokploy/server/db/schema";
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = { type WebServerSettings = typeof webServerSettings.$inferSelect;
const baseSettings: WebServerSettings = {
id: "",
https: false, https: false,
enablePaidFeatures: false, certificateType: "none",
allowImpersonation: false, host: null,
role: "user", serverIp: null,
firstName: "", letsEncryptEmail: null,
lastName: "", sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
metricsConfig: { metricsConfig: {
containers: { containers: {
refreshRate: 20, refreshRate: 20,
@@ -45,29 +51,8 @@ const baseAdmin: User = {
cleanupCacheApplications: false, cleanupCacheApplications: false,
cleanupCacheOnCompose: false, cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false, cleanupCacheOnPreviews: false,
createdAt: new Date(), createdAt: null,
serverIp: null,
certificateType: "none",
host: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
banExpires: new Date(),
banned: true,
banReason: "",
email: "",
expirationDate: "",
id: "",
isRegistered: false,
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
updatedAt: new Date(), updatedAt: new Date(),
twoFactorEnabled: false,
}; };
beforeEach(() => { beforeEach(() => {
@@ -85,7 +70,7 @@ test("Should read the configuration file", () => {
test("Should apply redirect-to-https", () => { test("Should apply redirect-to-https", () => {
updateServerTraefik( updateServerTraefik(
{ {
...baseAdmin, ...baseSettings,
https: true, https: true,
certificateType: "letsencrypt", certificateType: "letsencrypt",
}, },
@@ -100,7 +85,7 @@ test("Should apply redirect-to-https", () => {
}); });
test("Should change only host when no certificate", () => { test("Should change only host when no certificate", () => {
updateServerTraefik(baseAdmin, "example.com"); updateServerTraefik(baseSettings, "example.com");
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -110,7 +95,7 @@ test("Should change only host when no certificate", () => {
test("Should not touch config without host", () => { test("Should not touch config without host", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy"); const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(baseAdmin, null); updateServerTraefik(baseSettings, null);
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -119,11 +104,14 @@ test("Should not touch config without host", () => {
test("Should remove websecure if https rollback to http", () => { test("Should remove websecure if https rollback to http", () => {
updateServerTraefik( updateServerTraefik(
{ ...baseAdmin, certificateType: "letsencrypt" }, { ...baseSettings, certificateType: "letsencrypt" },
"example.com", "example.com",
); );
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com"); updateServerTraefik(
{ ...baseSettings, certificateType: "none" },
"example.com",
);
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");

View File

@@ -7,9 +7,12 @@ interface Props {
serverId?: string; serverId?: string;
} }
export const ToggleDockerCleanup = ({ serverId }: Props) => { export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.user.get.useQuery(undefined, { const { data, refetch } = api.settings.getWebServerSettings.useQuery(
enabled: !serverId, undefined,
}); {
enabled: !serverId,
},
);
const { data: server, refetch: refetchServer } = api.server.one.useQuery( const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{ {
@@ -22,7 +25,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
const enabled = serverId const enabled = serverId
? server?.enableDockerCleanup ? server?.enableDockerCleanup
: data?.user.enableDockerCleanup; : data?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
@@ -30,7 +33,10 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
try { try {
await mutateAsync({ await mutateAsync({
enableDockerCleanup: checked, enableDockerCleanup: checked,
serverId: serverId, ...(serverId && { serverId }),
} as {
enableDockerCleanup: boolean;
serverId?: string;
}); });
if (serverId) { if (serverId) {
await refetchServer(); await refetchServer();

View File

@@ -80,7 +80,7 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>; type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => { export const SetupMonitoring = ({ serverId }: Props) => {
const { data } = serverId const { data: serverData } = serverId
? api.server.one.useQuery( ? api.server.one.useQuery(
{ {
serverId: serverId || "", serverId: serverId || "",
@@ -89,7 +89,14 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId, enabled: !!serverId,
}, },
) )
: api.user.getServerMetrics.useQuery(); : { data: null };
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery(undefined, {
enabled: !serverId,
});
const data = serverId ? serverData : webServerSettings;
const url = useUrl(); const url = useUrl();

View File

@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => { export const WebDomain = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { data, refetch } = api.user.get.useQuery(); const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation(); api.settings.assignDomainServer.useMutation();
@@ -82,15 +82,15 @@ export const WebDomain = () => {
}); });
const https = form.watch("https"); const https = form.watch("https");
const domain = form.watch("domain") || ""; const domain = form.watch("domain") || "";
const host = data?.user?.host || ""; const host = data?.host || "";
const hasChanged = domain !== host; const hasChanged = domain !== host;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
domain: data?.user?.host || "", domain: data?.host || "",
certificateType: data?.user?.certificateType, certificateType: data?.certificateType || "none",
letsEncryptEmail: data?.user?.letsEncryptEmail || "", letsEncryptEmail: data?.letsEncryptEmail || "",
https: data?.user?.https || false, https: data?.https || false,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);

View File

@@ -16,7 +16,8 @@ import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => { export const WebServer = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { data } = api.user.get.useQuery(); const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -53,7 +54,7 @@ export const WebServer = () => {
<div className="flex items-center flex-wrap justify-between gap-4"> <div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Server IP: {data?.user.serverIp} Server IP: {webServerSettings?.serverIp}
</span> </span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version: {dokployVersion} Version: {dokployVersion}

View File

@@ -46,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children }: Props) => { export const UpdateServerIp = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { data } = api.user.get.useQuery(); const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { data: ip } = api.server.publicIp.useQuery(); const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.user.update.useMutation(); api.settings.updateServerIp.useMutation();
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
serverIp: data?.user.serverIp || "", serverIp: data?.serverIp || "",
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
@@ -62,13 +62,11 @@ export const UpdateServerIp = ({ children }: Props) => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
serverIp: data.user.serverIp || "", serverIp: data.serverIp || "",
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
const utils = api.useUtils();
const setCurrentIp = () => { const setCurrentIp = () => {
if (!ip) return; if (!ip) return;
form.setValue("serverIp", ip); form.setValue("serverIp", ip);
@@ -80,7 +78,7 @@ export const UpdateServerIp = ({ children }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Server IP Updated"); toast.success("Server IP Updated");
await utils.user.get.invalidate(); await refetch();
setIsOpen(false); setIsOpen(false);
}) })
.catch(() => { .catch(() => {

View File

@@ -0,0 +1,114 @@
CREATE TABLE "webServerSettings" (
"id" text PRIMARY KEY NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"https" boolean DEFAULT false NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
"logCleanupCron" text DEFAULT '0 0 * * *',
"metricsConfig" jsonb 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 NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now() NOT NULL
);
-- Migrate data from user table to webServerSettings
-- Get the owner user's data and insert into webServerSettings
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
u."serverIp",
COALESCE(u."certificateType", 'none') as "certificateType",
COALESCE(u."https", false) as "https",
u."host",
u."letsEncryptEmail",
u."sshPrivateKey",
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
COALESCE(
u."metricsConfig",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
) as "metricsConfig",
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
FROM "user" u
INNER JOIN "member" m ON u."id" = m."user_id"
WHERE m."role" = 'owner'
ORDER BY m."created_at" ASC
LIMIT 1;
-- If no owner found, create a default entry
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
NULL as "serverIp",
'none' as "certificateType",
false as "https",
NULL as "host",
NULL as "letsEncryptEmail",
NULL as "sshPrivateKey",
true as "enableDockerCleanup",
'0 0 * * *' as "logCleanupCron",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
false as "cleanupCacheApplications",
false as "cleanupCacheOnPreviews",
false as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
WHERE NOT EXISTS (
SELECT 1 FROM "webServerSettings"
);
--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";

File diff suppressed because it is too large Load Diff

View File

@@ -932,6 +932,13 @@
"when": 1765346573500, "when": 1765346573500,
"tag": "0132_clean_layla_miller", "tag": "0132_clean_layla_miller",
"breakpoints": true "breakpoints": true
},
{
"idx": 133,
"version": "7",
"when": 1766301478005,
"tag": "0133_striped_the_order",
"breakpoints": true
} }
] ]
} }

View File

@@ -140,7 +140,6 @@
"react-i18next": "^15.5.2", "react-i18next": "^15.5.2",
"react-markdown": "^9.1.0", "react-markdown": "^9.1.0",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"rotating-file-stream": "3.2.3",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"ssh2": "1.15.0", "ssh2": "1.15.0",

View File

@@ -1,8 +1,8 @@
import { import {
findUserById, getWebServerSettings,
IS_CLOUD, IS_CLOUD,
setupWebMonitoring, setupWebMonitoring,
updateUser, updateWebServerSettings,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { apiUpdateWebServerMonitoring } from "@/server/db/schema"; import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({ export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure setupMonitoring: adminProcedure
.input(apiUpdateWebServerMonitoring) .input(apiUpdateWebServerMonitoring)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
try { try {
if (IS_CLOUD) { if (IS_CLOUD) {
throw new TRPCError({ throw new TRPCError({
@@ -19,15 +19,8 @@ export const adminRouter = createTRPCRouter({
message: "Feature disabled on cloud", message: "Feature disabled on cloud",
}); });
} }
const user = await findUserById(ctx.user.ownerId);
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup the monitoring",
});
}
await updateUser(user.id, { await updateWebServerSettings({
metricsConfig: { metricsConfig: {
server: { server: {
type: "Dokploy", type: "Dokploy",
@@ -52,8 +45,9 @@ export const adminRouter = createTRPCRouter({
}, },
}); });
const currentServer = await setupWebMonitoring(user.id); await setupWebMonitoring();
return currentServer; const settings = await getWebServerSettings();
return settings;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -17,8 +17,8 @@ import {
findGitProviderById, findGitProviderById,
findProjectById, findProjectById,
findServerById, findServerById,
findUserById,
getComposeContainer, getComposeContainer,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
loadServices, loadServices,
randomizeComposeFile, randomizeComposeFile,
@@ -569,8 +569,7 @@ export const composeRouter = createTRPCRouter({
const template = await fetchTemplateFiles(input.id, input.baseUrl); const template = await fetchTemplateFiles(input.id, input.baseUrl);
const admin = await findUserById(ctx.user.ownerId); let serverIp = "127.0.0.1";
let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(environment.projectId); const project = await findProjectById(environment.projectId);
@@ -579,6 +578,9 @@ export const composeRouter = createTRPCRouter({
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
@@ -803,14 +805,16 @@ export const composeRouter = createTRPCRouter({
const decodedData = Buffer.from(input.base64, "base64").toString( const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8", "utf-8",
); );
const admin = await findUserById(ctx.user.ownerId); let serverIp = "127.0.0.1";
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) { if (compose.serverId) {
const server = await findServerById(compose.serverId); const server = await findServerById(compose.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const templateData = JSON.parse(decodedData); const templateData = JSON.parse(decodedData);
const config = parse(templateData.config) as CompleteTemplate; const config = parse(templateData.config) as CompleteTemplate;
@@ -880,14 +884,16 @@ export const composeRouter = createTRPCRouter({
await removeDomainById(domain.domainId); await removeDomainById(domain.domainId);
} }
const admin = await findUserById(ctx.user.ownerId); let serverIp = "127.0.0.1";
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) { if (compose.serverId) {
const server = await findServerById(compose.serverId); const server = await findServerById(compose.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const templateData = JSON.parse(decodedData); const templateData = JSON.parse(decodedData);

View File

@@ -9,6 +9,7 @@ import {
findPreviewDeploymentById, findPreviewDeploymentById,
findServerById, findServerById,
generateTraefikMeDomain, generateTraefikMeDomain,
getWebServerSettings,
manageDomain, manageDomain,
removeDomain, removeDomain,
removeDomainById, removeDomainById,
@@ -107,16 +108,13 @@ export const domainRouter = createTRPCRouter({
}), }),
canGenerateTraefikMeDomains: protectedProcedure canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() })) .input(z.object({ serverId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
if (input.serverId) { if (input.serverId) {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
return server.ipAddress; return server.ipAddress;
} }
return organization?.owner.serverIp; const settings = await getWebServerSettings();
return settings?.serverIp || "";
}), }),
update: protectedProcedure update: protectedProcedure

View File

@@ -8,6 +8,7 @@ import {
createSlackNotification, createSlackNotification,
createTelegramNotification, createTelegramNotification,
findNotificationById, findNotificationById,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeNotificationById, removeNotificationById,
sendCustomNotification, sendCustomNotification,
@@ -66,7 +67,6 @@ import {
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
server, server,
user,
} from "@/server/db/schema"; } from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({ export const notificationRouter = createTRPCRouter({
@@ -364,21 +364,20 @@ export const notificationRouter = createTRPCRouter({
let organizationId = ""; let organizationId = "";
let ServerName = ""; let ServerName = "";
if (input.ServerType === "Dokploy") { if (input.ServerType === "Dokploy") {
const result = await db const settings = await getWebServerSettings();
.select() if (
.from(user) !settings?.metricsConfig?.server?.token ||
.where( settings.metricsConfig.server.token !== input.Token
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, ) {
);
if (!result?.[0]?.id) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Token not found", message: "Token not found",
}); });
} }
organizationId = result?.[0]?.id; // For Dokploy server type, we don't have a specific organizationId
// This might need to be adjusted based on your business logic
organizationId = "";
ServerName = "Dokploy"; ServerName = "Dokploy";
} else { } else {
const result = await db const result = await db

View File

@@ -12,11 +12,11 @@ import {
DEFAULT_UPDATE_DATA, DEFAULT_UPDATE_DATA,
execAsync, execAsync,
findServerById, findServerById,
findUserById,
getDokployImage, getDokployImage,
getDokployImageTag, getDokployImageTag,
getLogCleanupStatus, getLogCleanupStatus,
getUpdateData, getUpdateData,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
parseRawConfig, parseRawConfig,
paths, paths,
@@ -40,7 +40,7 @@ import {
updateLetsEncryptEmail, updateLetsEncryptEmail,
updateServerById, updateServerById,
updateServerTraefik, updateServerTraefik,
updateUser, updateWebServerSettings,
writeConfig, writeConfig,
writeMainConfig, writeMainConfig,
writeTraefikConfigInPath, writeTraefikConfigInPath,
@@ -77,6 +77,13 @@ import {
} from "../trpc"; } from "../trpc";
export const settingsRouter = createTRPCRouter({ export const settingsRouter = createTRPCRouter({
getWebServerSettings: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
return settings;
}),
reloadServer: adminProcedure.mutation(async () => { reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
@@ -209,11 +216,11 @@ export const settingsRouter = createTRPCRouter({
}), }),
saveSSHPrivateKey: adminProcedure saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey) .input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await updateUser(ctx.user.ownerId, { await updateWebServerSettings({
sshPrivateKey: input.sshPrivateKey, sshPrivateKey: input.sshPrivateKey,
}); });
@@ -221,36 +228,36 @@ export const settingsRouter = createTRPCRouter({
}), }),
assignDomainServer: adminProcedure assignDomainServer: adminProcedure
.input(apiAssignDomain) .input(apiAssignDomain)
.mutation(async ({ ctx, input }) => { .mutation(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
const user = await updateUser(ctx.user.ownerId, { const settings = await updateWebServerSettings({
host: input.host, host: input.host,
letsEncryptEmail: input.letsEncryptEmail, letsEncryptEmail: input.letsEncryptEmail,
certificateType: input.certificateType, certificateType: input.certificateType,
https: input.https, https: input.https,
}); });
if (!user) { if (!settings) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "Web server settings not found",
}); });
} }
updateServerTraefik(user, input.host); updateServerTraefik(settings, input.host);
if (input.letsEncryptEmail) { if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail); updateLetsEncryptEmail(input.letsEncryptEmail);
} }
return user; return settings;
}), }),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { cleanSSHPrivateKey: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await updateUser(ctx.user.ownerId, { await updateWebServerSettings({
sshPrivateKey: null, sshPrivateKey: null,
}); });
return true; return true;
@@ -310,11 +317,11 @@ export const settingsRouter = createTRPCRouter({
} }
} }
} else if (!IS_CLOUD) { } else if (!IS_CLOUD) {
const userUpdated = await updateUser(ctx.user.ownerId, { const settingsUpdated = await updateWebServerSettings({
enableDockerCleanup: input.enableDockerCleanup, enableDockerCleanup: input.enableDockerCleanup,
}); });
if (userUpdated?.enableDockerCleanup) { if (settingsUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`, `Docker Cleanup ${new Date().toLocaleString()}] Running...`,
@@ -488,13 +495,28 @@ export const settingsRouter = createTRPCRouter({
return readConfigInPath(input.path, input.serverId); return readConfigInPath(input.path, input.serverId);
}), }),
getIp: protectedProcedure.query(async ({ ctx }) => { getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return "";
} }
const user = await findUserById(ctx.user.ownerId); const settings = await getWebServerSettings();
return user.serverIp; return settings?.serverIp || "";
}), }),
updateServerIp: adminProcedure
.input(
z.object({
serverIp: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const settings = await updateWebServerSettings({
serverIp: input.serverIp,
});
return settings;
}),
getOpenApiDocument: protectedProcedure.query( getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => { async ({ ctx }): Promise<unknown> => {

View File

@@ -5,6 +5,7 @@ import {
findUserById, findUserById,
getDokployUrl, getDokployUrl,
getUserByToken, getUserByToken,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeUserById, removeUserById,
sendEmailNotification, sendEmailNotification,
@@ -214,10 +215,11 @@ export const userRouter = createTRPCRouter({
}), }),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => { getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId); const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return { return {
serverIp: user.serverIp, serverIp: settings?.serverIp,
enabledFeatures: user.enablePaidFeatures, enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig, metricsConfig: settings?.metricsConfig,
}; };
}), }),
remove: protectedProcedure remove: protectedProcedure

View File

@@ -75,7 +75,6 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"shell-quote": "^1.8.1", "shell-quote": "^1.8.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"ssh2": "1.15.0", "ssh2": "1.15.0",

View File

@@ -35,3 +35,4 @@ export * from "./ssh-key";
export * from "./user"; export * from "./user";
export * from "./utils"; export * from "./utils";
export * from "./volume-backups"; export * from "./volume-backups";
export * from "./web-server-settings";

View File

@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
integer, integer,
jsonb,
pgTable, pgTable,
text, text,
timestamp, timestamp,
@@ -15,7 +14,6 @@ import { account, apikey, organization } from "./account";
import { backups } from "./backups"; import { backups } from "./backups";
import { projects } from "./project"; import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -51,73 +49,10 @@ export const user = pgTable("user", {
banExpires: timestamp("ban_expires"), banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
// Admin // Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"), role: text("role").notNull().default("user"),
// Metrics // Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false), enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false), allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
stripeCustomerId: text("stripeCustomerId"), stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
@@ -203,33 +138,6 @@ export const apiFindOneUserByAuth = createSchema
// authId: true, // authId: true,
}) })
.required(); .required();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
https: true,
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({ export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1), traefikConfig: z.string().min(1),
@@ -298,32 +206,6 @@ export const apiReadStatsLogs = z.object({
.optional(), .optional(),
}); });
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
email: z email: z
.string() .string()
@@ -334,29 +216,4 @@ export const apiUpdateUser = createSchema.partial().extend({
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
lastName: z.string().optional(), lastName: z.string().optional(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
logCleanupCron: z.string().optional().nullable(),
}); });

View File

@@ -0,0 +1,178 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { certificateType } from "./shared";
export const webServerSettings = pgTable("webServerSettings", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
// Web Server Configuration
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
// Metrics Configuration
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const webServerSettingsRelations = relations(
webServerSettings,
() => ({}),
);
const createSchema = createInsertSchema(webServerSettings, {
id: z.string().min(1),
});
export const apiUpdateWebServerSettings = createSchema.partial().extend({
serverIp: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
https: z.boolean().optional(),
host: z.string().optional(),
letsEncryptEmail: z.string().email().optional().nullable(),
sshPrivateKey: z.string().optional(),
enableDockerCleanup: z.boolean().optional(),
logCleanupCron: z.string().optional().nullable(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
});
export const apiAssignDomain = z
.object({
host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(),
https: z.boolean().optional(),
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiSaveSSHKey = z
.object({
sshPrivateKey: z.string(),
})
.required();
export const apiUpdateDockerCleanup = z.object({
enableDockerCleanup: z.boolean(),
serverId: z.string().optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});

View File

@@ -41,6 +41,7 @@ export * from "./services/settings";
export * from "./services/ssh-key"; export * from "./services/ssh-key";
export * from "./services/user"; export * from "./services/user";
export * from "./services/volume-backups"; export * from "./services/volume-backups";
export * from "./services/web-server-settings";
export * from "./setup/config-paths"; export * from "./setup/config-paths";
export * from "./setup/monitoring-setup"; export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup"; export * from "./setup/postgres-setup";

View File

@@ -9,7 +9,10 @@ import { IS_CLOUD } from "../constants";
import { db } from "../db"; import { db } from "../db";
import * as schema from "../db/schema"; import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin"; import { getUserByToken } from "../services/admin";
import { updateUser } from "../services/user"; import {
getWebServerSettings,
updateWebServerSettings,
} from "../services/web-server-settings";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot"; import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email"; import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils"; import { getPublicIpWithFallback } from "../wss/utils";
@@ -35,22 +38,14 @@ const { handler, api } = betterAuth({
}, },
...(!IS_CLOUD && { ...(!IS_CLOUD && {
async trustedOrigins() { async trustedOrigins() {
const admin = await db.query.member.findFirst({ const settings = await getWebServerSettings();
where: eq(schema.member.role, "owner"), if (!settings) {
with: { return [];
user: true,
},
});
if (admin?.user) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
];
} }
return []; return [
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
...(settings?.host ? [`https://${settings?.host}`] : []),
];
}, },
}), }),
emailVerification: { emailVerification: {
@@ -122,7 +117,7 @@ const { handler, api } = betterAuth({
}); });
if (!IS_CLOUD) { if (!IS_CLOUD) {
await updateUser(user.id, { await updateWebServerSettings({
serverIp: await getPublicIpWithFallback(), serverIp: await getPublicIpWithFallback(),
}); });
} }

View File

@@ -8,6 +8,7 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { getWebServerSettings } from "./web-server-settings";
export const findUserById = async (userId: string) => { export const findUserById = async (userId: string) => {
const userResult = await db.query.user.findFirst({ const userResult = await db.query.user.findFirst({
@@ -107,11 +108,11 @@ export const getDokployUrl = async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return "https://app.dokploy.com"; return "https://app.dokploy.com";
} }
const owner = await findOwner(); const settings = await getWebServerSettings();
if (owner.user.host) { if (settings?.host) {
const protocol = owner.user.https ? "https" : "http"; const protocol = settings?.https ? "https" : "http";
return `${protocol}://${owner.user.host}`; return `${protocol}://${settings?.host}`;
} }
return `http://${owner.user.serverIp}:${process.env.PORT}`; return `http://${settings?.serverIp}:${process.env.PORT}`;
}; };

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server"; import { findServerById } from "./server";
import { getWebServerSettings } from "./web-server-settings";
export const getAiSettingsByOrganizationId = async (organizationId: string) => { export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({ const aiSettings = await db.query.ai.findMany({
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
let ip = ""; let ip = "";
if (!IS_CLOUD) { if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId); const settings = await getWebServerSettings();
ip = organization?.owner.serverIp || ""; ip = settings?.serverIp || "";
} }
if (serverId) { if (serverId) {

View File

@@ -3,10 +3,10 @@ import { promisify } from "node:util";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates"; import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain"; import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema"; import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn"; import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server"; import { findServerById } from "./server";
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
projectName: appName, projectName: appName,
}); });
} }
const admin = await findUserById(userId); const settings = await getWebServerSettings();
return generateRandomDomain({ return generateRandomDomain({
serverIp: admin?.serverIp || "", serverIp: settings?.serverIp || "",
projectName: appName, projectName: appName,
}); });
}; };

View File

@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github"; import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application"; import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain"; import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment"; import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain"; import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github"; import { type Github, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect; export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
} }
if (!ip) { if (!ip) {
const admin = await findUserById(userId); const settings = await getWebServerSettings();
ip = admin?.serverIp || ""; ip = settings?.serverIp || "";
} }
const slugIp = ip.replaceAll(".", "-"); const slugIp = ip.replaceAll(".", "-");

View File

@@ -0,0 +1,44 @@
import { db } from "@dokploy/server/db";
import { webServerSettings } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
/**
* Get the web server settings (singleton - only one row should exist)
*/
export const getWebServerSettings = async () => {
const settings = await db.query.webServerSettings.findFirst({
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
});
if (!settings) {
// Create default settings if none exist
const [newSettings] = await db
.insert(webServerSettings)
.values({})
.returning();
return newSettings;
}
return settings;
};
/**
* Update web server settings
*/
export const updateWebServerSettings = async (
updates: Partial<typeof webServerSettings.$inferInsert>,
) => {
const current = await getWebServerSettings();
const [updated] = await db
.update(webServerSettings)
.set({
...updates,
updatedAt: new Date(),
})
.where(eq(webServerSettings.id, current?.id ?? ""))
.returning();
return updated;
};

View File

@@ -1,7 +1,7 @@
import { findServerById } from "@dokploy/server/services/server"; import { findServerById } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import type { ContainerCreateOptions } from "dockerode"; import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings"; import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
} }
}; };
export const setupWebMonitoring = async (userId: string) => { export const setupWebMonitoring = async () => {
const user = await findUserById(userId); const webServerSettings = await getWebServerSettings();
const containerName = "dokploy-monitoring"; const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest"; let imageName = "dokploy/monitoring:latest";
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
const settings: ContainerCreateOptions = { const settings: ContainerCreateOptions = {
name: containerName, name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`], Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
Image: imageName, Image: imageName,
HostConfig: { HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes // Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
Name: "always", Name: "always",
}, },
PortBindings: { PortBindings: {
[`${user?.metricsConfig?.server?.port}/tcp`]: [ [`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
{ {
HostPort: user?.metricsConfig?.server?.port.toString(), HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
}, },
], ],
}, },
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
// NetworkMode: "host", // NetworkMode: "host",
}, },
ExposedPorts: { ExposedPorts: {
[`${user?.metricsConfig?.server?.port}/tcp`]: {}, [`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
}, },
}; };
const docker = await getRemoteDocker(); const docker = await getRemoteDocker();

View File

@@ -1,6 +1,8 @@
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { findOwner } from "@dokploy/server/services/admin"; import {
import { updateUser } from "@dokploy/server/services/user"; getWebServerSettings,
updateWebServerSettings,
} from "@dokploy/server/services/web-server-settings";
import { scheduledJobs, scheduleJob } from "node-schedule"; import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
@@ -29,12 +31,9 @@ export const startLogCleanup = async (
} }
}); });
const owner = await findOwner(); await updateWebServerSettings({
if (owner) { logCleanupCron: cronExpression,
await updateUser(owner.user.id, { });
logCleanupCron: cronExpression,
});
}
return true; return true;
} catch (error) { } catch (error) {
@@ -51,12 +50,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
} }
// Update database // Update database
const owner = await findOwner(); await updateWebServerSettings({
if (owner) { logCleanupCron: null,
await updateUser(owner.user.id, { });
logCleanupCron: null,
});
}
return true; return true;
} catch (error) { } catch (error) {
@@ -69,8 +65,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean; enabled: boolean;
cronExpression: string | null; cronExpression: string | null;
}> => { }> => {
const owner = await findOwner(); const settings = await getWebServerSettings();
const cronExpression = owner?.user.logCleanupCron ?? null; const cronExpression = settings?.logCleanupCron ?? null;
return { return {
enabled: cronExpression !== null, enabled: cronExpression !== null,
cronExpression, cronExpression,

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { member } from "@dokploy/server/db/schema"; import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server"; import { getAllServers } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { db } from "../../db/index"; import { db } from "../../db/index";
@@ -25,7 +26,9 @@ export const initCronJobs = async () => {
return; return;
} }
if (admin?.user?.enableDockerCleanup) { const webServerSettings = await getWebServerSettings();
if (webServerSettings?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -82,9 +85,12 @@ export const initCronJobs = async () => {
} }
} }
if (admin?.user?.logCleanupCron) { if (webServerSettings?.logCleanupCron) {
console.log("Starting log requests cleanup", admin.user.logCleanupCron); console.log(
await startLogCleanup(admin.user.logCleanupCron); "Starting log requests cleanup",
webServerSettings.logCleanupCron,
);
await startLogCleanup(webServerSettings.logCleanupCron);
} }
}; };

View File

@@ -90,7 +90,7 @@ export const createCommand = (compose: ComposeNested) => {
if (composeType === "docker-compose") { if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
} else if (composeType === "stack") { } else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune`; command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
} }
return command; return command;

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user"; import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
import { import {
loadOrCreateConfig, loadOrCreateConfig,
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types"; import type { MainTraefikConfig } from "./types";
export const updateServerTraefik = ( export const updateServerTraefik = (
user: User | null, settings: typeof webServerSettings.$inferSelect | null,
newHost: string | null, newHost: string | null,
) => { ) => {
const { https, certificateType } = user || {}; const { https, certificateType } = settings || {};
const appName = "dokploy"; const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName); const config: FileConfig = loadOrCreateConfig(appName);

12
pnpm-lock.yaml generated
View File

@@ -406,9 +406,6 @@ importers:
recharts: recharts:
specifier: ^2.15.3 specifier: ^2.15.3
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote: shell-quote:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.2 version: 1.8.2
@@ -732,9 +729,6 @@ importers:
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote: shell-quote:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.2 version: 1.8.2
@@ -7064,10 +7058,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
rotating-file-stream@3.2.3:
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
engines: {node: '>=14.0'}
rou3@0.5.1: rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
@@ -14660,8 +14650,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.41.1 '@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3 fsevents: 2.3.3
rotating-file-stream@3.2.3: {}
rou3@0.5.1: {} rou3@0.5.1: {}
run-parallel@1.2.0: run-parallel@1.2.0: