mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
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:
@@ -89,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
enabled: !!serverId,
|
||||
},
|
||||
)
|
||||
: api.user.getServerMetrics.useQuery();
|
||||
: api.webServer.get.useQuery();
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
56
apps/dokploy/drizzle/0107_clever_cobalt_man.sql
Normal file
56
apps/dokploy/drizzle/0107_clever_cobalt_man.sql
Normal 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";
|
||||
6232
apps/dokploy/drizzle/meta/0107_snapshot.json
Normal file
6232
apps/dokploy/drizzle/meta/0107_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
847
apps/dokploy/server/api/routers/web-server.ts
Normal file
847
apps/dokploy/server/api/routers/web-server.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -35,3 +35,4 @@ export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
export * from "./volume-backups";
|
||||
export * from "./web-server";
|
||||
|
||||
@@ -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),
|
||||
|
||||
104
packages/server/src/db/schema/web-server.ts
Normal file
104
packages/server/src/db/schema/web-server.ts
Normal 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(),
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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(".", "-");
|
||||
|
||||
42
packages/server/src/services/web-server.ts
Normal file
42
packages/server/src/services/web-server.ts
Normal 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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user