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

@@ -89,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId,
},
)
: api.user.getServerMetrics.useQuery();
: api.webServer.get.useQuery();
const url = useUrl();

View File

@@ -62,9 +62,9 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data, refetch } = api.user.get.useQuery();
const { data, refetch } = api.webServer.get.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
api.webServer.assignDomainServer.useMutation();
const form = useForm<AddServerDomain>({
defaultValues: {
@@ -79,10 +79,10 @@ export const WebDomain = () => {
useEffect(() => {
if (data) {
form.reset({
domain: data?.user?.host || "",
certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
https: data?.user?.https || false,
domain: data?.host || "",
certificateType: data?.certificateType,
letsEncryptEmail: data?.letsEncryptEmail || "",
https: data?.https || false,
});
}
}, [form, form.reset, data]);

View File

@@ -16,13 +16,12 @@ import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => {
const { t } = useTranslation("settings");
const { data } = api.user.get.useQuery();
const { data } = api.webServer.get.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
return (
<div className="w-full">
{/* <Card className={cn("rounded-lg w-full bg-transparent p-0", className)}></Card> */}
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
@@ -34,14 +33,6 @@ export const WebServer = () => {
{t("settings.server.webServer.description")}
</CardDescription>
</CardHeader>
{/* <CardHeader>
<CardTitle className="text-xl">
{t("settings.server.webServer.title")}
</CardTitle>
<CardDescription>
{t("settings.server.webServer.description")}
</CardDescription>
</CardHeader> */}
<CardContent className="space-y-6 py-6 border-t">
<div className="grid md:grid-cols-2 gap-4">
<ShowDokployActions />
@@ -53,7 +44,7 @@ export const WebServer = () => {
<div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground">
Server IP: {data?.user.serverIp}
Server IP: {data?.serverIp}
</span>
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}

View File

@@ -46,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.user.get.useQuery();
const { data } = api.webServer.get.useQuery();
const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.user.update.useMutation();
api.webServer.update.useMutation();
const form = useForm<Schema>({
defaultValues: {
serverIp: data?.user.serverIp || "",
serverIp: data?.serverIp || "",
},
resolver: zodResolver(schema),
});
@@ -62,7 +62,7 @@ export const UpdateServerIp = ({ children }: Props) => {
useEffect(() => {
if (data) {
form.reset({
serverIp: data.user.serverIp || "",
serverIp: data.serverIp || "",
});
}
}, [form, form.reset, data]);

View File

@@ -0,0 +1,56 @@
CREATE TABLE "web_server" (
"webServerId" 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 false 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
);
--> statement-breakpoint
-- Migrar datos del usuario owner único hacia web_server
INSERT INTO "web_server" (
"webServerId",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig"
)
SELECT
gen_random_uuid() as "webServerId",
u."serverIp",
COALESCE(u."certificateType", 'none') as "certificateType",
COALESCE(u."https", false) as "https",
u."host",
u."letsEncryptEmail",
u."sshPrivateKey",
COALESCE(u."enableDockerCleanup", false) 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":[]}}}') as "metricsConfig"
FROM "users" u
INNER JOIN "organization" o ON u.id = o.owner_id
LIMIT 1;
--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "created_at";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "serverIp";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "certificateType";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "https";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "host";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "logCleanupCron";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "metricsConfig";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
ALTER TABLE "users" DROP COLUMN "cleanupCacheOnCompose";

File diff suppressed because it is too large Load Diff

View File

@@ -750,6 +750,13 @@
"when": 1752358951289,
"tag": "0106_low_fat_cobra",
"breakpoints": true
},
{
"idx": 107,
"version": "7",
"when": 1752359583291,
"tag": "0107_clever_cobalt_man",
"breakpoints": true
}
]
}

View File

@@ -39,6 +39,7 @@ import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
import { volumeBackupsRouter } from "./routers/volume-backups";
import { roleRouter } from "./routers/role";
import { webServerRouter } from "./routers/web-server";
/**
* This is the primary router for your server.
*
@@ -86,6 +87,7 @@ export const appRouter = createTRPCRouter({
rollback: rollbackRouter,
volumeBackups: volumeBackupsRouter,
role: roleRouter,
webServer: webServerRouter,
});
// export type definition of API

View File

@@ -147,11 +147,10 @@ export const aiRouter = createTRPCRouter({
serverId: z.string().optional(),
}),
)
.mutation(async ({ ctx, input }) => {
.mutation(async ({ input }) => {
try {
return await suggestVariants({
...input,
organizationId: ctx.session.activeOrganizationId,
});
} catch (error) {
throw new TRPCError({

View File

@@ -13,9 +13,9 @@ import {
findDomainById,
findDomainsByApplicationId,
findDomainsByComposeId,
findOrganizationById,
findPreviewDeploymentById,
findServerById,
findWebServer,
generateTraefikMeDomain,
manageDomain,
removeDomain,
@@ -93,25 +93,19 @@ export const domainRouter = createTRPCRouter({
}),
generateDomain: protectedProcedure
.input(z.object({ appName: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input, ctx }) => {
return generateTraefikMeDomain(
input.appName,
ctx.user.ownerId,
input.serverId,
);
.mutation(async ({ input }) => {
return generateTraefikMeDomain(input.appName, input.serverId);
}),
canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() }))
.query(async ({ input, ctx }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
.query(async ({ input }) => {
const webServer = await findWebServer();
if (input.serverId) {
const server = await findServerById(input.serverId);
return server.ipAddress;
}
return organization?.owner.serverIp;
return webServer?.serverIp;
}),
update: protectedProcedure

View File

@@ -1,16 +1,12 @@
import { db } from "@/server/db";
import {
apiAssignDomain,
apiEnableDashboard,
apiModifyTraefikConfig,
apiReadStatsLogs,
apiReadTraefikConfig,
apiSaveSSHKey,
apiServerSchema,
apiTraefikConfig,
apiUpdateDockerCleanup,
} from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup";
import {
DEFAULT_UPDATE_DATA,
IS_CLOUD,
@@ -23,7 +19,6 @@ import {
execAsync,
execAsyncRemote,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
@@ -40,14 +35,9 @@ import {
readMainConfig,
readMonitoringConfig,
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
@@ -57,7 +47,6 @@ import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import packageInfo from "../../../package.json";
import { appRouter } from "../root";
@@ -187,135 +176,6 @@ export const settingsRouter = createTRPCRouter({
await recreateDirectory(MONITORING_PATH);
return true;
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return true;
}
await updateUser(ctx.user.id, {
sshPrivateKey: input.sshPrivateKey,
});
return true;
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ ctx, input }) => {
if (IS_CLOUD) {
return true;
}
const user = await updateUser(ctx.user.id, {
host: input.host,
...(input.letsEncryptEmail && {
letsEncryptEmail: input.letsEncryptEmail,
}),
certificateType: input.certificateType,
https: input.https,
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
updateServerTraefik(user, input.host);
if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail);
}
return user;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
await updateUser(ctx.user.id, {
sshPrivateKey: null,
});
return true;
}),
updateDockerCleanup: adminProcedure
.input(apiUpdateDockerCleanup)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
await updateServerById(input.serverId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
if (server.enableDockerCleanup) {
const server = await findServerById(input.serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
if (IS_CLOUD) {
await schedule({
cronSchedule: "0 0 * * *",
serverId: input.serverId,
type: "server",
});
} else {
scheduleJob(server.serverId, "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages(server.serverId);
await cleanUpDockerBuilder(server.serverId);
await cleanUpSystemPrune(server.serverId);
await sendDockerCleanupNotifications(server.organizationId);
});
}
} else {
if (IS_CLOUD) {
await removeJob({
cronSchedule: "0 0 * * *",
serverId: input.serverId,
type: "server",
});
} else {
const currentJob = scheduledJobs[server.serverId];
currentJob?.cancel();
}
}
} else if (!IS_CLOUD) {
const userUpdated = await updateUser(ctx.user.id, {
enableDockerCleanup: input.enableDockerCleanup,
});
if (userUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await sendDockerCleanupNotifications(
ctx.session.activeOrganizationId,
);
});
} else {
const currentJob = scheduledJobs["docker-cleanup"];
currentJob?.cancel();
}
}
return true;
}),
readTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
@@ -470,13 +330,6 @@ export const settingsRouter = createTRPCRouter({
return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async ({ ctx }) => {
if (IS_CLOUD) {
return true;
}
const user = await findUserById(ctx.user.ownerId);
return user.serverIp;
}),
getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => {

View File

@@ -1,14 +1,13 @@
import {
IS_CLOUD,
createApiKey,
findOwner,
findNotificationById,
findOrganizationById,
findUserById,
getUserByToken,
removeUserById,
sendEmailNotification,
updateUser,
findWebServer,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
@@ -148,19 +147,6 @@ export const userRouter = createTRPCRouter({
return memberResult?.user;
}),
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
return memberResult?.user;
}),
update: protectedProcedure
.input(apiUpdateUser)
.mutation(async ({ input, ctx }) => {
@@ -200,14 +186,6 @@ export const userRouter = createTRPCRouter({
.query(async ({ input }) => {
return await getUserByToken(input.token);
}),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
return {
serverIp: user.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig,
};
}),
remove: protectedProcedure
.input(
z.object({
@@ -411,11 +389,11 @@ export const userRouter = createTRPCRouter({
});
}
const owner = await findOwner();
const webServer = await findWebServer();
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: owner.user.host;
: webServer.host;
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
const organization = await findOrganizationById(

View File

@@ -0,0 +1,847 @@
import { db } from "@/server/db";
import {
apiAssignDomain,
apiEnableDashboard,
apiModifyTraefikConfig,
apiReadStatsLogs,
apiReadTraefikConfig,
apiSaveSSHKey,
apiServerSchema,
apiTraefikConfig,
apiUpdateDockerCleanup,
updateWebServerSchema,
} from "@/server/db/schema";
import { removeJob, schedule } from "@/server/utils/backup";
import {
DEFAULT_UPDATE_DATA,
IS_CLOUD,
canAccessToTraefikFiles,
cleanStoppedContainers,
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
cleanUpUnusedVolumes,
execAsync,
execAsyncRemote,
findServerById,
findWebServer,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
initializeTraefik,
parseRawConfig,
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
readConfig,
readConfigInPath,
readDirectory,
readMainConfig,
readMonitoringConfig,
recreateDirectory,
sendDockerCleanupNotifications,
spawnAsync,
startLogCleanup,
stopLogCleanup,
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateWebServer,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
} from "@dokploy/server";
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { sql } from "drizzle-orm";
import { dump, load } from "js-yaml";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { z } from "zod";
import packageInfo from "../../../package.json";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from "../trpc";
export const webServerRouter = createTRPCRouter({
get: adminProcedure.query(async () => {
return await findWebServer();
}),
update: adminProcedure
.input(updateWebServerSchema)
.mutation(async ({ input }) => {
return await updateWebServer(input);
}),
reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { stdout } = await execAsync(
"docker service inspect dokploy --format '{{.ID}}'",
);
await execAsync(`docker service update --force ${stdout.trim()}`);
return true;
}),
cleanRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { stdout: containerId } = await execAsync(
`docker ps --filter "name=dokploy-redis" --filter "status=running" -q | head -n 1`,
);
if (!containerId) {
throw new Error("Redis container not found");
}
const redisContainerId = containerId.trim();
await execAsync(`docker exec -i ${redisContainerId} redis-cli flushall`);
return true;
}),
reloadRedis: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await execAsync("docker service scale dokploy-redis=0");
await execAsync("docker service scale dokploy-redis=1");
return true;
}),
reloadTraefik: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
try {
if (input?.serverId) {
await execAsync("docker restart dokploy-traefik");
} else if (!IS_CLOUD) {
await execAsync("docker restart dokploy-traefik");
}
} catch (err) {
console.error(err);
}
return true;
}),
toggleDashboard: adminProcedure
.input(apiEnableDashboard)
.mutation(async ({ input }) => {
const ports = (await getTraefikPorts(input.serverId)).filter(
(port) =>
port.targetPort !== 80 &&
port.targetPort !== 443 &&
port.targetPort !== 8080,
);
await initializeTraefik({
additionalPorts: ports,
enableDashboard: input.enableDashboard,
serverId: input.serverId,
force: true,
});
return true;
}),
cleanUnusedImages: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
return true;
}),
cleanUnusedVolumes: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedVolumes(input?.serverId);
return true;
}),
cleanStoppedContainers: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanStoppedContainers(input?.serverId);
return true;
}),
cleanDockerBuilder: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpDockerBuilder(input?.serverId);
}),
cleanDockerPrune: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpSystemPrune(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
return true;
}),
cleanAll: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
await cleanStoppedContainers(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
await cleanUpSystemPrune(input?.serverId);
return true;
}),
cleanMonitoring: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
const { MONITORING_PATH } = paths();
await recreateDirectory(MONITORING_PATH);
return true;
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
await updateWebServer({
sshPrivateKey: input.sshPrivateKey,
});
return true;
}),
getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
const webServer = await findWebServer();
return webServer?.serverIp;
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const webServer = await updateWebServer({
host: input.host,
...(input.letsEncryptEmail && {
letsEncryptEmail: input.letsEncryptEmail,
}),
certificateType: input.certificateType,
https: input.https,
});
if (!webServer) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
updateServerTraefik(webServer, input.host);
if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail);
}
return webServer;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await updateWebServer({
sshPrivateKey: null,
});
return true;
}),
updateDockerCleanup: adminProcedure
.input(apiUpdateDockerCleanup)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
await updateServerById(input.serverId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to access this server",
});
}
if (server.enableDockerCleanup) {
const server = await findServerById(input.serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
if (IS_CLOUD) {
await schedule({
cronSchedule: "0 0 * * *",
serverId: input.serverId,
type: "server",
});
} else {
scheduleJob(server.serverId, "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages(server.serverId);
await cleanUpDockerBuilder(server.serverId);
await cleanUpSystemPrune(server.serverId);
await sendDockerCleanupNotifications(server.organizationId);
});
}
} else {
if (IS_CLOUD) {
await removeJob({
cronSchedule: "0 0 * * *",
serverId: input.serverId,
type: "server",
});
} else {
const currentJob = scheduledJobs[server.serverId];
currentJob?.cancel();
}
}
} else if (!IS_CLOUD) {
const userUpdated = await updateWebServer({
enableDockerCleanup: input.enableDockerCleanup,
});
if (userUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await sendDockerCleanupNotifications(
ctx.session.activeOrganizationId,
);
});
} else {
const currentJob = scheduledJobs["docker-cleanup"];
currentJob?.cancel();
}
}
return true;
}),
readTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;
}
const traefikConfig = readMainConfig();
return traefikConfig;
}),
updateTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
writeMainConfig(input.traefikConfig);
return true;
}),
readWebServerTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;
}
const traefikConfig = readConfig("dokploy");
return traefikConfig;
}),
updateWebServerTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
writeConfig("dokploy", input.traefikConfig);
return true;
}),
readMiddlewareTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;
}
const traefikConfig = readConfig("middlewares");
return traefikConfig;
}),
updateMiddlewareTraefikConfig: adminProcedure
.input(apiTraefikConfig)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
writeConfig("middlewares", input.traefikConfig);
return true;
}),
getUpdateData: protectedProcedure.mutation(async () => {
if (IS_CLOUD) {
return DEFAULT_UPDATE_DATA;
}
return await getUpdateData();
}),
updateServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await pullLatestRelease();
// This causes restart of dokploy, thus it will not finish executing properly, so don't await it
// Status after restart is checked via frontend /api/health endpoint
void spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(),
"dokploy",
]);
return true;
}),
getDokployVersion: protectedProcedure.query(() => {
return packageInfo.version;
}),
getReleaseTag: protectedProcedure.query(() => {
return getDokployImageTag();
}),
readDirectories: protectedProcedure
.input(apiServerSchema)
.query(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
const canAccess = await canAccessToTraefikFiles(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
return result || [];
} catch (error) {
throw error;
}
}),
updateTraefikFile: protectedProcedure
.input(apiModifyTraefikConfig)
.mutation(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const canAccess = await canAccessToTraefikFiles(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await writeTraefikConfigInPath(
input.path,
input.traefikConfig,
input?.serverId,
);
return true;
}),
readTraefikFile: protectedProcedure
.input(apiReadTraefikConfig)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const canAccess = await canAccessToTraefikFiles(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!canAccess) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
return readConfigInPath(input.path, input.serverId);
}),
readTraefikEnv: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command =
"docker container inspect dokploy-traefik --format '{{json .Config.Env}}'";
let result = "";
if (input?.serverId) {
const execResult = await execAsyncRemote(input.serverId, command);
result = execResult.stdout;
} else {
const execResult = await execAsync(command);
result = execResult.stdout;
}
const envVars = JSON.parse(result.trim());
return envVars.join("\n");
}),
writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
.mutation(async ({ input }) => {
const envs = prepareEnvironmentVariables(input.env);
await initializeTraefik({
env: envs,
serverId: input.serverId,
force: true,
});
return true;
}),
haveTraefikDashboardPortEnabled: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const ports = JSON.parse(stdout.trim());
return Object.entries(ports).some(([containerPort, bindings]) => {
const [port] = containerPort.split("/");
return port === "8080" && bindings && (bindings as any[]).length > 0;
});
}),
readStatsLogs: adminProcedure
.meta({
openapi: {
path: "/read-stats-logs",
method: "POST",
override: true,
enabled: false,
},
})
.input(apiReadStatsLogs)
.query(async ({ input }) => {
if (IS_CLOUD) {
return {
data: [],
totalCount: 0,
};
}
const rawConfig = await readMonitoringConfig(
!!input.dateRange?.start && !!input.dateRange?.end,
);
const parsedConfig = parseRawConfig(
rawConfig as string,
input.page,
input.sort,
input.search,
input.status,
input.dateRange,
);
return parsedConfig;
}),
readStats: adminProcedure
.meta({
openapi: {
path: "/read-stats",
method: "POST",
override: true,
enabled: false,
},
})
.input(
z
.object({
dateRange: z
.object({
start: z.string().optional(),
end: z.string().optional(),
})
.optional(),
})
.optional(),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
const rawConfig = await readMonitoringConfig(
!!input?.dateRange?.start || !!input?.dateRange?.end,
);
const processedLogs = processLogs(rawConfig as string, input?.dateRange);
return processedLogs || [];
}),
haveActivateRequests: adminProcedure.query(async () => {
if (IS_CLOUD) {
return true;
}
const config = readMainConfig();
if (!config) return false;
const parsedConfig = load(config) as {
accessLog?: {
filePath: string;
};
};
return !!parsedConfig?.accessLog?.filePath;
}),
toggleRequests: adminProcedure
.input(
z.object({
enable: z.boolean(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const mainConfig = readMainConfig();
if (!mainConfig) return false;
const currentConfig = load(mainConfig) as {
accessLog?: {
filePath: string;
};
};
if (input.enable) {
const config = {
accessLog: {
filePath: "/etc/dokploy/traefik/dynamic/access.log",
format: "json",
bufferingSize: 100,
filters: {
retryAttempts: true,
minDuration: "10ms",
},
},
};
currentConfig.accessLog = config.accessLog;
} else {
currentConfig.accessLog = undefined;
}
writeMainConfig(dump(currentConfig));
return true;
}),
isCloud: publicProcedure.query(async () => {
return IS_CLOUD;
}),
health: publicProcedure.query(async () => {
if (IS_CLOUD) {
try {
await db.execute(sql`SELECT 1`);
return { status: "ok" };
} catch (error) {
console.error("Database connection error:", error);
throw error;
}
}
return { status: "not_cloud" };
}),
setupGPU: adminProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD && !input.serverId) {
throw new Error("Select a server to enable the GPU Setup");
}
try {
await setupGPUSupport(input.serverId);
return { success: true };
} catch (error) {
console.error("GPU Setup Error:", error);
throw error;
}
}),
checkGPUStatus: adminProcedure
.input(
z.object({
serverId: z.string().optional(),
}),
)
.query(async ({ input }) => {
if (IS_CLOUD && !input.serverId) {
return {
driverInstalled: false,
driverVersion: undefined,
gpuModel: undefined,
runtimeInstalled: false,
runtimeConfigured: false,
cudaSupport: undefined,
cudaVersion: undefined,
memoryInfo: undefined,
availableGPUs: 0,
swarmEnabled: false,
gpuResources: 0,
};
}
try {
return await checkGPUStatus(input.serverId || "");
} catch (error) {
const message =
error instanceof Error ? error.message : "Failed to check GPU status";
throw new TRPCError({
code: "BAD_REQUEST",
message,
});
}
}),
updateTraefikPorts: adminProcedure
.input(
z.object({
serverId: z.string().optional(),
additionalPorts: z.array(
z.object({
targetPort: z.number(),
publishedPort: z.number(),
}),
),
}),
)
.mutation(async ({ input }) => {
try {
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Please set a serverId to update Traefik ports",
});
}
await initializeTraefik({
serverId: input.serverId,
additionalPorts: input.additionalPorts,
force: true,
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Error updating Traefik ports",
cause: error,
});
}
}),
getTraefikPorts: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
return await getTraefikPorts(input?.serverId);
}),
updateLogCleanup: adminProcedure
.input(
z.object({
cronExpression: z.string().nullable(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
if (input.cronExpression) {
return startLogCleanup(input.cronExpression);
}
return stopLogCleanup();
}),
getLogCleanupStatus: adminProcedure.query(async () => {
return getLogCleanupStatus();
}),
getDokployCloudIps: adminProcedure.query(async () => {
if (!IS_CLOUD) {
return [];
}
const ips = process.env.DOKPLOY_CLOUD_IPS?.split(",");
return ips;
}),
});
export const getTraefikPorts = async (serverId?: string) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
try {
let stdout = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
}> = [];
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
});
}
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
};

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);