feat(web-server): migrate user-related functionality to web server model

- Refactored components and API routes to utilize the new web server schema, replacing user references with web server data.
- Updated the dashboard settings to fetch and manage web server domains, IPs, and configurations.
- Introduced a new web server router to handle related API requests, enhancing the overall architecture and data management.
- Added SQL migration for the new web server table and adjusted the database schema accordingly.
This commit is contained in:
Mauricio Siu
2025-07-12 22:57:36 -06:00
parent 733777eeb1
commit 2ec4868a09
27 changed files with 7366 additions and 364 deletions

View File

@@ -35,3 +35,4 @@ export * from "./account";
export * from "./schedule";
export * from "./rollbacks";
export * from "./volume-backups";
export * from "./web-server";

View File

@@ -2,7 +2,6 @@ import { relations } from "drizzle-orm";
import {
boolean,
integer,
jsonb,
pgTable,
text,
timestamp,
@@ -14,7 +13,6 @@ import { account, apikey, organization } from "./account";
import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
import { paths } from "@dokploy/server/constants";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
@@ -23,8 +21,6 @@ import { paths } from "@dokploy/server/constants";
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
*/
// OLD TABLE
// TEMP
export const users = pgTable("users", {
id: text("id")
@@ -36,10 +32,10 @@ export const users = pgTable("users", {
expirationDate: text("expirationDate")
.notNull()
.$defaultFn(() => new Date().toISOString()),
createdAt2: text("createdAt")
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
createdAt: timestamp("created_at").defaultNow(),
// createdAt: timestamp("created_at").defaultNow(),
// Auth
twoFactorEnabled: boolean("two_factor_enabled"),
email: text("email").notNull().unique(),
@@ -49,74 +45,10 @@ export const users = pgTable("users", {
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(),
// 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(false),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").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"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
@@ -199,33 +131,6 @@ export const apiFindOneUserByAuth = createSchema
// authId: true,
})
.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({
traefikConfig: z.string().min(1),

View File

@@ -0,0 +1,104 @@
import { boolean, jsonb, pgTable, text } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { certificateType } from "./shared";
import { z } from "zod";
import { createInsertSchema } from "drizzle-zod";
export const webServer = pgTable("web_server", {
webServerId: text("webServerId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
// 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(false),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
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: [],
},
},
}),
});
export type WebServer = typeof webServer.$inferSelect;
const createSchema = createInsertSchema(webServer);
export const updateWebServerSchema = createSchema.omit({
webServerId: true,
metricsConfig: true,
});
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(),
});

View File

@@ -35,6 +35,7 @@ export * from "./services/server";
export * from "./services/schedule";
export * from "./services/application";
export * from "./services/rollbacks";
export * from "./services/web-server";
export * from "./utils/databases/rebuild";
export * from "./setup/config-paths";
export * from "./setup/postgres-setup";

View File

@@ -9,10 +9,13 @@ import { IS_CLOUD } from "../constants";
import { db } from "../db";
import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin";
import { updateUser } from "../services/user";
import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
import { createDefaultRoles } from "../services/role";
import {
findWebServer,
updateWebServer,
} from "@dokploy/server/services/web-server";
const { handler, api } = betterAuth({
database: drizzleAdapter(db, {
@@ -32,19 +35,12 @@ const { handler, api } = betterAuth({
},
...(!IS_CLOUD && {
async trustedOrigins() {
const admin = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
with: {
user: true,
},
});
const admin = await findWebServer();
if (admin) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
...(admin.serverIp ? [`http://${admin.serverIp}:3000`] : []),
...(admin.host ? [`https://${admin.host}`] : []),
];
}
return [];
@@ -161,7 +157,7 @@ const { handler, api } = betterAuth({
});
if (!IS_CLOUD) {
await updateUser(user.id, {
await updateWebServer({
serverIp: await getPublicIpWithFallback(),
});
}

View File

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

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server";
import { findWebServer } from "./web-server";
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({
@@ -53,18 +53,12 @@ export const deleteAiSettings = async (aiId: string) => {
};
interface Props {
organizationId: string;
aiId: string;
input: string;
serverId?: string | undefined;
}
export const suggestVariants = async ({
organizationId,
aiId,
input,
serverId,
}: Props) => {
export const suggestVariants = async ({ aiId, input, serverId }: Props) => {
try {
const aiSettings = await getAiSettingById(aiId);
if (!aiSettings || !aiSettings.isEnabled) {
@@ -79,8 +73,8 @@ export const suggestVariants = async ({
let ip = "";
if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId);
ip = organization?.owner.serverIp || "";
const webServer = await findWebServer();
ip = webServer?.serverIp || "";
}
if (serverId) {

View File

@@ -6,10 +6,10 @@ import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server";
import { findWebServer } from "./web-server";
export type Domain = typeof domains.$inferSelect;
@@ -43,7 +43,6 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => {
export const generateTraefikMeDomain = async (
appName: string,
userId: string,
serverId?: string,
) => {
if (serverId) {
@@ -60,9 +59,9 @@ export const generateTraefikMeDomain = async (
projectName: appName,
});
}
const admin = await findUserById(userId);
const webServer = await findWebServer();
return generateRandomDomain({
serverIp: admin?.serverIp || "",
serverIp: webServer?.serverIp || "",
projectName: appName,
});
};

View File

@@ -2,7 +2,6 @@ import { db } from "@dokploy/server/db";
import {
type apiCreatePreviewDeployment,
deployments,
organization,
previewDeployments,
} from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server";
@@ -13,11 +12,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
import { findWebServer } from "./web-server";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -156,14 +155,10 @@ export const createPreviewDeployment = async (
const application = await findApplicationById(schema.applicationId);
const appName = `preview-${application.appName}-${generatePassword(6)}`;
const org = await db.query.organization.findFirst({
where: eq(organization.id, application.project.organizationId),
});
const generateDomain = await generateWildcardDomain(
application.previewWildcard || "*.traefik.me",
appName,
application.server?.ipAddress || "",
org?.ownerId || "",
);
const octokit = authGithub(application?.github as Github);
@@ -256,7 +251,6 @@ const generateWildcardDomain = async (
baseDomain: string,
appName: string,
serverIp: string,
userId: string,
): Promise<string> => {
if (!baseDomain.startsWith("*.")) {
throw new Error('The base domain must start with "*."');
@@ -274,8 +268,8 @@ const generateWildcardDomain = async (
}
if (!ip) {
const admin = await findUserById(userId);
ip = admin?.serverIp || "";
const webServer = await findWebServer();
ip = webServer?.serverIp || "";
}
const slugIp = ip.replaceAll(".", "-");

View File

@@ -0,0 +1,42 @@
import { webServer, type updateWebServerSchema } from "../db/schema";
import { db } from "../db";
import type { z } from "zod";
import { TRPCError } from "@trpc/server";
export const createWebServer = async () => {
const exists = await findWebServer();
if (exists) {
return exists;
}
const server = await db?.insert(webServer).values({});
return server;
};
export const findWebServer = async () => {
const server = await db?.query.webServer.findFirst();
if (!server) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Web server not found",
});
}
return server;
};
export const updateWebServer = async (
input: z.infer<typeof updateWebServerSchema>,
) => {
const server = await findWebServer();
if (!server) {
await createWebServer();
}
const updated = await db
.update(webServer)
.set({
...input,
})
.returning()
.then(([updated]) => updated);
return updated;
};

View File

@@ -1,11 +1,11 @@
import { findServerById } from "@dokploy/server/services/server";
import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { findWebServer } from "../services/web-server";
export const setupMonitoring = async (serverId: string) => {
const server = await findServerById(serverId);
@@ -80,8 +80,8 @@ export const setupMonitoring = async (serverId: string) => {
}
};
export const setupWebMonitoring = async (userId: string) => {
const user = await findUserById(userId);
export const setupWebMonitoring = async () => {
const webServer = await findWebServer();
const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest";
@@ -96,7 +96,7 @@ export const setupWebMonitoring = async (userId: string) => {
const settings: ContainerCreateOptions = {
name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
Env: [`METRICS_CONFIG=${JSON.stringify(webServer?.metricsConfig)}`],
Image: imageName,
HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -104,9 +104,9 @@ export const setupWebMonitoring = async (userId: string) => {
// CapAdd: ["NET_ADMIN", "SYS_ADMIN"],
// Privileged: true,
PortBindings: {
[`${user?.metricsConfig?.server?.port}/tcp`]: [
[`${webServer?.metricsConfig?.server?.port}/tcp`]: [
{
HostPort: user?.metricsConfig?.server?.port.toString(),
HostPort: webServer?.metricsConfig?.server?.port.toString(),
},
],
},
@@ -120,7 +120,7 @@ export const setupWebMonitoring = async (userId: string) => {
// NetworkMode: "host",
},
ExposedPorts: {
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
[`${webServer?.metricsConfig?.server?.port}/tcp`]: {},
},
};
const docker = await getRemoteDocker();

View File

@@ -1,8 +1,11 @@
import { paths } from "@dokploy/server/constants";
import { findOwner } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { execAsync } from "../process/execAsync";
import {
findWebServer,
updateWebServer,
} from "@dokploy/server/services/web-server";
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
@@ -31,7 +34,7 @@ export const startLogCleanup = async (
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
await updateWebServer({
logCleanupCron: cronExpression,
});
}
@@ -53,7 +56,7 @@ export const stopLogCleanup = async (): Promise<boolean> => {
// Update database
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
await updateWebServer({
logCleanupCron: null,
});
}
@@ -69,8 +72,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean;
cronExpression: string | null;
}> => {
const owner = await findOwner();
const cronExpression = owner?.user.logCleanupCron ?? null;
const webServer = await findWebServer();
const cronExpression = webServer?.logCleanupCron ?? null;
return {
enabled: cronExpression !== null,
cronExpression,

View File

@@ -15,6 +15,7 @@ import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
import { findWebServer } from "@dokploy/server/services/web-server";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");
@@ -26,11 +27,13 @@ export const initCronJobs = async () => {
},
});
if (!admin) {
const webServer = await findWebServer();
if (!webServer || !admin) {
return;
}
if (admin.user.enableDockerCleanup) {
if (webServer.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -87,9 +90,9 @@ export const initCronJobs = async () => {
}
}
if (admin?.user.logCleanupCron) {
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
await startLogCleanup(admin.user.logCleanupCron);
if (webServer.logCleanupCron) {
console.log("Starting log requests cleanup", webServer.logCleanupCron);
await startLogCleanup(webServer.logCleanupCron);
}
};

View File

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