+
{step === 1 && (
<>
{!haveAtleasOneProviderEnabled && (
@@ -192,29 +190,25 @@ export const TemplateGenerator = ({ projectId }: Props) => {
)}
{step === 3 && (
- )}
- {step === 4 && (
- {
- console.log("Submitting template:", data);
- setTemplateInfo(data);
+ onSubmit={async () => {
+ console.log("Submitting template:", templateInfo);
await mutateAsync({
projectId,
id: templateInfo.details?.id || "",
name: templateInfo?.details?.name || "",
- description: data?.details?.shortDescription || "",
- dockerCompose: data?.details?.dockerCompose || "",
- envVariables: (data?.details?.envVariables || [])
+ description: templateInfo?.details?.shortDescription || "",
+ dockerCompose: templateInfo?.details?.dockerCompose || "",
+ envVariables: (templateInfo?.details?.envVariables || [])
.map((env: any) => `${env.name}=${env.value}`)
.join("\n"),
- serverId: data.server || "",
+ domains: templateInfo?.details?.domains || [],
+ ...(templateInfo.server?.serverId && {
+ serverId: templateInfo.server?.serverId || "",
+ }),
})
.then(async () => {
toast.success("Compose Created");
@@ -227,7 +221,6 @@ export const TemplateGenerator = ({ projectId }: Props) => {
toast.error("Error creating the compose");
});
}}
- setOpen={setOpen}
/>
)}
diff --git a/apps/dokploy/components/dashboard/settings/ai-form.tsx b/apps/dokploy/components/dashboard/settings/ai-form.tsx
index 61bf24bf..05ab93a4 100644
--- a/apps/dokploy/components/dashboard/settings/ai-form.tsx
+++ b/apps/dokploy/components/dashboard/settings/ai-form.tsx
@@ -1,5 +1,6 @@
"use client";
+import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -12,7 +13,6 @@ import { api } from "@/utils/api";
import { BotIcon, Loader2, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandleAi } from "./handle-ai";
-import { DialogAction } from "@/components/shared/dialog-action";
export const AiForm = () => {
const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();
diff --git a/apps/dokploy/components/dashboard/settings/handle-ai.tsx b/apps/dokploy/components/dashboard/settings/handle-ai.tsx
index 9c050116..3f81a804 100644
--- a/apps/dokploy/components/dashboard/settings/handle-ai.tsx
+++ b/apps/dokploy/components/dashboard/settings/handle-ai.tsx
@@ -1,5 +1,14 @@
"use client";
+import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
import {
Form,
FormControl,
@@ -18,22 +27,13 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
-import { PenBoxIcon, PlusIcon } from "lucide-react";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import { useEffect, useState } from "react";
import { api } from "@/utils/api";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { PenBoxIcon, PlusIcon } from "lucide-react";
+import { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
import { toast } from "sonner";
-import { AlertBlock } from "@/components/shared/alert-block";
+import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, { message: "Name is required" }),
diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx
index c751e8ce..a6a5fc3a 100644
--- a/apps/dokploy/components/layouts/side.tsx
+++ b/apps/dokploy/components/layouts/side.tsx
@@ -7,6 +7,7 @@ import {
Bell,
BlocksIcon,
BookIcon,
+ BotIcon,
Boxes,
ChevronRight,
CircleHelp,
@@ -19,7 +20,6 @@ import {
GitBranch,
HeartIcon,
KeyRound,
- BotIcon,
type LucideIcon,
Package,
PieChart,
diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts
index 74931e97..605abc80 100644
--- a/apps/dokploy/server/api/routers/ai.ts
+++ b/apps/dokploy/server/api/routers/ai.ts
@@ -11,11 +11,12 @@ import {
apiUpdateAi,
deploySuggestionSchema,
} from "@dokploy/server/db/schema/ai";
+import { createDomain } from "@dokploy/server/index";
import {
- getAiSettingsByAdminId,
- getAiSettingById,
- saveAiSettings,
deleteAiSettings,
+ getAiSettingById,
+ getAiSettingsByAdminId,
+ saveAiSettings,
suggestVariants,
} from "@dokploy/server/services/ai";
import { createComposeByTemplate } from "@dokploy/server/services/compose";
@@ -84,11 +85,15 @@ export const aiRouter = createTRPCRouter({
.input(
z.object({
aiId: z.string(),
- prompt: z.string(),
+ input: z.string(),
+ serverId: z.string().optional(),
})
)
.mutation(async ({ ctx, input }) => {
- return await suggestVariants(ctx.user.adminId, input.aiId, input.prompt);
+ return await suggestVariants({
+ ...input,
+ adminId: ctx.user.adminId,
+ });
}),
deploy: protectedProcedure
.input(deploySuggestionSchema)
@@ -108,6 +113,8 @@ export const aiRouter = createTRPCRouter({
const projectName = slugify(`${project.name} ${input.id}`);
+ console.log(input);
+
const compose = await createComposeByTemplate({
...input,
composeFile: input.dockerCompose,
@@ -118,6 +125,17 @@ export const aiRouter = createTRPCRouter({
appName: `${projectName}-${generatePassword(6)}`,
});
+ if (input.domains && input.domains?.length > 0) {
+ for (const domain of input.domains) {
+ await createDomain({
+ ...domain,
+ domainType: "compose",
+ certificateType: "none",
+ composeId: compose.composeId,
+ });
+ }
+ }
+
if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId);
}
diff --git a/packages/server/src/db/schema/admin.ts b/packages/server/src/db/schema/admin.ts
index 1f49c843..f5b46fdd 100644
--- a/packages/server/src/db/schema/admin.ts
+++ b/packages/server/src/db/schema/admin.ts
@@ -3,119 +3,119 @@ import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
+import { ai } from "./ai";
import { auth } from "./auth";
import { certificates } from "./certificate";
import { registry } from "./registry";
import { certificateType } from "./shared";
import { sshKeys } from "./ssh-key";
import { users } from "./user";
-import { ai } from "./ai";
export const admins = pgTable("admin", {
- adminId: text("adminId")
- .notNull()
- .primaryKey()
- .$defaultFn(() => nanoid()),
- serverIp: text("serverIp"),
- certificateType: certificateType("certificateType").notNull().default("none"),
- host: text("host"),
- letsEncryptEmail: text("letsEncryptEmail"),
- sshPrivateKey: text("sshPrivateKey"),
- enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
- enableLogRotation: boolean("enableLogRotation").notNull().default(false),
- authId: text("authId")
- .notNull()
- .references(() => auth.id, { onDelete: "cascade" }),
- createdAt: text("createdAt")
- .notNull()
- .$defaultFn(() => new Date().toISOString()),
- stripeCustomerId: text("stripeCustomerId"),
- stripeSubscriptionId: text("stripeSubscriptionId"),
- serversQuantity: integer("serversQuantity").notNull().default(0),
+ adminId: text("adminId")
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => nanoid()),
+ serverIp: text("serverIp"),
+ certificateType: certificateType("certificateType").notNull().default("none"),
+ host: text("host"),
+ letsEncryptEmail: text("letsEncryptEmail"),
+ sshPrivateKey: text("sshPrivateKey"),
+ enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
+ enableLogRotation: boolean("enableLogRotation").notNull().default(false),
+ authId: text("authId")
+ .notNull()
+ .references(() => auth.id, { onDelete: "cascade" }),
+ createdAt: text("createdAt")
+ .notNull()
+ .$defaultFn(() => new Date().toISOString()),
+ stripeCustomerId: text("stripeCustomerId"),
+ stripeSubscriptionId: text("stripeSubscriptionId"),
+ serversQuantity: integer("serversQuantity").notNull().default(0),
});
export const adminsRelations = relations(admins, ({ one, many }) => ({
- auth: one(auth, {
- fields: [admins.authId],
- references: [auth.id],
- }),
- users: many(users),
- registry: many(registry),
- sshKeys: many(sshKeys),
- certificates: many(certificates),
- ai: many(ai),
+ auth: one(auth, {
+ fields: [admins.authId],
+ references: [auth.id],
+ }),
+ users: many(users),
+ registry: many(registry),
+ sshKeys: many(sshKeys),
+ certificates: many(certificates),
+ ai: many(ai),
}));
const createSchema = createInsertSchema(admins, {
- adminId: z.string(),
- enableDockerCleanup: z.boolean().optional(),
- sshPrivateKey: z.string().optional(),
- certificateType: z.enum(["letsencrypt", "none"]).default("none"),
- serverIp: z.string().optional(),
- letsEncryptEmail: z.string().optional(),
+ adminId: z.string(),
+ enableDockerCleanup: z.boolean().optional(),
+ sshPrivateKey: z.string().optional(),
+ certificateType: z.enum(["letsencrypt", "none"]).default("none"),
+ serverIp: z.string().optional(),
+ letsEncryptEmail: z.string().optional(),
});
export const apiUpdateAdmin = createSchema.partial();
export const apiSaveSSHKey = createSchema
- .pick({
- sshPrivateKey: true,
- })
- .required();
+ .pick({
+ sshPrivateKey: true,
+ })
+ .required();
export const apiAssignDomain = createSchema
- .pick({
- host: true,
- certificateType: true,
- letsEncryptEmail: true,
- })
- .required()
- .partial({
- letsEncryptEmail: true,
- });
+ .pick({
+ host: true,
+ certificateType: true,
+ letsEncryptEmail: true,
+ })
+ .required()
+ .partial({
+ letsEncryptEmail: true,
+ });
export const apiUpdateDockerCleanup = createSchema
- .pick({
- enableDockerCleanup: true,
- })
- .required()
- .extend({
- serverId: z.string().optional(),
- });
+ .pick({
+ enableDockerCleanup: true,
+ })
+ .required()
+ .extend({
+ serverId: z.string().optional(),
+ });
export const apiTraefikConfig = z.object({
- traefikConfig: z.string().min(1),
+ traefikConfig: z.string().min(1),
});
export const apiModifyTraefikConfig = z.object({
- path: z.string().min(1),
- traefikConfig: z.string().min(1),
- serverId: z.string().optional(),
+ path: z.string().min(1),
+ traefikConfig: z.string().min(1),
+ serverId: z.string().optional(),
});
export const apiReadTraefikConfig = z.object({
- path: z.string().min(1),
- serverId: z.string().optional(),
+ path: z.string().min(1),
+ serverId: z.string().optional(),
});
export const apiEnableDashboard = z.object({
- enableDashboard: z.boolean().optional(),
- serverId: z.string().optional(),
+ enableDashboard: z.boolean().optional(),
+ serverId: z.string().optional(),
});
export const apiServerSchema = z
- .object({
- serverId: z.string().optional(),
- })
- .optional();
+ .object({
+ serverId: z.string().optional(),
+ })
+ .optional();
export const apiReadStatsLogs = z.object({
- page: z
- .object({
- pageIndex: z.number(),
- pageSize: z.number(),
- })
- .optional(),
- status: z.string().array().optional(),
- search: z.string().optional(),
- sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
+ page: z
+ .object({
+ pageIndex: z.number(),
+ pageSize: z.number(),
+ })
+ .optional(),
+ status: z.string().array().optional(),
+ search: z.string().optional(),
+ sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
});
diff --git a/packages/server/src/db/schema/ai.ts b/packages/server/src/db/schema/ai.ts
index a13e4128..43f03dd0 100644
--- a/packages/server/src/db/schema/ai.ts
+++ b/packages/server/src/db/schema/ai.ts
@@ -1,9 +1,9 @@
+import { relations } from "drizzle-orm";
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
+import { nanoid } from "nanoid";
import { z } from "zod";
import { admins } from "./admin";
-import { relations } from "drizzle-orm";
-import { nanoid } from "nanoid";
export const ai = pgTable("ai", {
aiId: text("aiId")
@@ -63,4 +63,13 @@ export const deploySuggestionSchema = z.object({
serverId: z.string().optional(),
name: z.string().min(1),
description: z.string(),
+ domains: z
+ .array(
+ z.object({
+ host: z.string().min(1),
+ port: z.number().min(1),
+ serviceName: z.string().min(1),
+ })
+ )
+ .optional(),
});
diff --git a/packages/server/src/services/ai.ts b/packages/server/src/services/ai.ts
index d835b9c5..f810ff57 100644
--- a/packages/server/src/services/ai.ts
+++ b/packages/server/src/services/ai.ts
@@ -7,6 +7,7 @@ import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findAdminById } from "./admin";
+import { findServerById } from "./server";
export const getAiSettingsByAdminId = async (adminId: string) => {
const aiSettings = await db.query.ai.findMany({
@@ -51,11 +52,19 @@ export const deleteAiSettings = async (aiId: string) => {
return db.delete(ai).where(eq(ai.aiId, aiId));
};
-export const suggestVariants = async (
- adminId: string,
- aiId: string,
- input: string
-) => {
+interface Props {
+ adminId: string;
+ aiId: string;
+ input: string;
+ serverId?: string | undefined;
+}
+
+export const suggestVariants = async ({
+ adminId,
+ aiId,
+ input,
+ serverId,
+}: Props) => {
try {
const aiSettings = await getAiSettingById(aiId);
if (!aiSettings || !aiSettings.isEnabled) {
@@ -74,6 +83,13 @@ export const suggestVariants = async (
ip = admin?.serverIp || "";
}
+ if (serverId) {
+ const server = await findServerById(serverId);
+ ip = server.ipAddress;
+ } else if (process.env.NODE_ENV === "development") {
+ ip = "127.0.0.1";
+ }
+
const { object } = await generateObject({
model,
output: "array",
@@ -122,13 +138,13 @@ export const suggestVariants = async (
2. Use complex values for passwords/secrets variables
3. Don't set container_name field in services
4. Don't set version field in the docker compose
- 5. Don't set ports like 'ports: 3000:3000', use 'ports: ["3000"]' instead
+ 5. Don't set ports like 'ports: 3000:3000', use 'ports: "3000"' instead
6. Use dokploy-network in all services
7. Add dokploy-network at the end and mark it as external: true
For each service that needs to be exposed to the internet:
1. Define a domain configuration with:
- - host: the domain name for the service
+ - host: the domain name for the service in format: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me
- port: the internal port the service runs on
- serviceName: the name of the service in the docker-compose
2. Make sure the service is properly configured in the docker-compose to work with the specified port
@@ -145,7 +161,6 @@ export const suggestVariants = async (
}
} catch (error) {
console.error("Error in docker compose generation:", error);
- console.error("Error details:", error.cause?.issues || error);
}
}
return result;