diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index 1eadf8bab..57f851c9e 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -40,7 +40,7 @@ interface Props { } const AddRedirectchema = z.object({ - replicas: z.number(), + replicas: z.number().min(1, "Replicas must be at least 1"), registryId: z.string(), }); @@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => { placeholder="1" {...field} onChange={(e) => { - field.onChange(Number(e.target.value)); + const value = e.target.value; + field.onChange(value === "" ? 0 : Number(value)); }} type="number" + value={field.value || ""} /> diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index f91218cee..8b1fa7e1c 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -42,6 +42,7 @@ import { domain } from "@/server/db/validations/domain"; import { zodResolver } from "@hookform/resolvers/zod"; import { Dices } from "lucide-react"; import type z from "zod"; +import Link from "next/link"; type Domain = z.infer; @@ -83,6 +84,13 @@ export const AddDomain = ({ const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } = api.domain.generateDomain.useMutation(); + const { data: canGenerateTraefikMeDomains } = + api.domain.canGenerateTraefikMeDomains.useQuery({ + serverId: application?.serverId || "", + }); + + console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains); + const form = useForm({ resolver: zodResolver(domain), defaultValues: { @@ -186,6 +194,21 @@ export const AddDomain = ({ name="host" render={({ field }) => ( + {!canGenerateTraefikMeDomains && + field.value.includes("traefik.me") && ( + + You need to set an IP address in your{" "} + + {application?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to make your traefik.me domain work. + + )} Host
diff --git a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx index 9b412c83a..975ce1ffe 100644 --- a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx @@ -42,6 +42,7 @@ import { domainCompose } from "@/server/db/validations/domain"; import { zodResolver } from "@hookform/resolvers/zod"; import { DatabaseZap, Dices, RefreshCw } from "lucide-react"; import type z from "zod"; +import Link from "next/link"; type Domain = z.infer; @@ -102,6 +103,11 @@ export const AddDomainCompose = ({ ? api.domain.update.useMutation() : api.domain.create.useMutation(); + const { data: canGenerateTraefikMeDomains } = + api.domain.canGenerateTraefikMeDomains.useQuery({ + serverId: compose?.serverId || "", + }); + const form = useForm({ resolver: zodResolver(domainCompose), defaultValues: { @@ -313,6 +319,21 @@ export const AddDomainCompose = ({ name="host" render={({ field }) => ( + {!canGenerateTraefikMeDomains && + field.value.includes("traefik.me") && ( + + You need to set an IP address in your{" "} + + {compose?.serverId + ? "Remote Servers -> Server -> Edit Server -> Update IP Address" + : "Web Server -> Server -> Update Server IP"} + {" "} + to make your traefik.me domain work. + + )} Host
diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index c761fc701..5dcd77327 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -48,6 +48,7 @@ import { toast } from "sonner"; interface Props { databaseId: string; databaseType: Exclude; + serverId: string | null; } const RestoreBackupSchema = z.object({ @@ -76,7 +77,11 @@ const RestoreBackupSchema = z.object({ type RestoreBackup = z.infer; -export const RestoreBackup = ({ databaseId, databaseType }: Props) => { +export const RestoreBackup = ({ + databaseId, + databaseType, + serverId, +}: Props) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); @@ -101,6 +106,7 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => { { destinationId: destionationId, search, + serverId: serverId ?? "", }, { enabled: isOpen && !!destionationId, @@ -304,7 +310,9 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => { form.setValue("backupFile", file); }} > - {file} +
+ {file} +
{ {postgres && postgres?.backups?.length > 0 && (
- +
)} @@ -108,7 +112,11 @@ export const ShowBackups = ({ id, type }: Props) => { databaseType={type} refetch={refetch} /> - +
) : ( diff --git a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx index dfbf501eb..fdc28adc3 100644 --- a/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx +++ b/apps/dokploy/components/dashboard/mongo/general/show-general-mongo.tsx @@ -3,10 +3,10 @@ import { DrawerLogs } from "@/components/shared/drawer-logs"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; import * as TooltipPrimitive from "@radix-ui/react-tooltip"; @@ -16,236 +16,246 @@ import { toast } from "sonner"; import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; interface Props { - mongoId: string; + mongoId: string; } export const ShowGeneralMongo = ({ mongoId }: Props) => { - const { data, refetch } = api.mongo.one.useQuery( - { - mongoId, - }, - { enabled: !!mongoId } - ); + const { data, refetch } = api.mongo.one.useQuery( + { + mongoId, + }, + { enabled: !!mongoId }, + ); - const { mutateAsync: reload, isLoading: isReloading } = - api.mongo.reload.useMutation(); + const { mutateAsync: reload, isLoading: isReloading } = + api.mongo.reload.useMutation(); - const { mutateAsync: start, isLoading: isStarting } = - api.mongo.start.useMutation(); + const { mutateAsync: start, isLoading: isStarting } = + api.mongo.start.useMutation(); - const { mutateAsync: stop, isLoading: isStopping } = - api.mongo.stop.useMutation(); + const { mutateAsync: stop, isLoading: isStopping } = + api.mongo.stop.useMutation(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const [filteredLogs, setFilteredLogs] = useState([]); - const [isDeploying, setIsDeploying] = useState(false); - api.mongo.deployWithLogs.useSubscription( - { - mongoId: mongoId, - }, - { - enabled: isDeploying, - onData(log) { - if (!isDrawerOpen) { - setIsDrawerOpen(true); - } + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [filteredLogs, setFilteredLogs] = useState([]); + const [isDeploying, setIsDeploying] = useState(false); + api.mongo.deployWithLogs.useSubscription( + { + mongoId: mongoId, + }, + { + enabled: isDeploying, + onData(log) { + if (!isDrawerOpen) { + setIsDrawerOpen(true); + } - if (log === "Deployment completed successfully!") { - setIsDeploying(false); - } + if (log === "Deployment completed successfully!") { + setIsDeploying(false); + } - const parsedLogs = parseLogs(log); - setFilteredLogs((prev) => [...prev, ...parsedLogs]); - }, - onError(error) { - console.error("Deployment logs error:", error); - setIsDeploying(false); - }, - } - ); - return ( - <> -
- - - Deploy Settings - - - - { - setIsDeploying(true); - await new Promise((resolve) => setTimeout(resolve, 1000)); - refetch(); - }} - > - - - - - - -

Downloads and sets up the MongoDB database

-
-
-
-
- { - await reload({ - mongoId: mongoId, - appName: data?.appName || "", - }) - .then(() => { - toast.success("Mongo reloaded successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error reloading Mongo"); - }); - }} - > - - - - - - -

Restart the MongoDB service without rebuilding

-
-
-
-
- {data?.applicationStatus === "idle" ? ( - { - await start({ - mongoId: mongoId, - }) - .then(() => { - toast.success("Mongo started successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error starting Mongo"); - }); - }} - > - - - - - - -

- Start the MongoDB database (requires a previous - successful setup) -

-
-
-
-
- ) : ( - { - await stop({ - mongoId: mongoId, - }) - .then(() => { - toast.success("Mongo stopped successfully"); - refetch(); - }) - .catch(() => { - toast.error("Error stopping Mongo"); - }); - }} - > - - - - - - -

Stop the currently running MongoDB database

-
-
-
-
- )} -
- - - - - - - -

Open a terminal to the MongoDB container

-
-
-
-
-
-
- { - setIsDrawerOpen(false); - setFilteredLogs([]); - setIsDeploying(false); - refetch(); - }} - filteredLogs={filteredLogs} - /> -
- - ); + const parsedLogs = parseLogs(log); + setFilteredLogs((prev) => [...prev, ...parsedLogs]); + }, + onError(error) { + console.error("Deployment logs error:", error); + setIsDeploying(false); + }, + }, + ); + return ( + <> +
+ + + Deploy Settings + + + + { + setIsDeploying(true); + await new Promise((resolve) => setTimeout(resolve, 1000)); + refetch(); + }} + > + + + { + await reload({ + mongoId: mongoId, + appName: data?.appName || "", + }) + .then(() => { + toast.success("Mongo reloaded successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error reloading Mongo"); + }); + }} + > + + + {data?.applicationStatus === "idle" ? ( + { + await start({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo started successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error starting Mongo"); + }); + }} + > + + + ) : ( + { + await stop({ + mongoId: mongoId, + }) + .then(() => { + toast.success("Mongo stopped successfully"); + refetch(); + }) + .catch(() => { + toast.error("Error stopping Mongo"); + }); + }} + > + + + )} + + + + + + + { + setIsDrawerOpen(false); + setFilteredLogs([]); + setIsDeploying(false); + refetch(); + }} + filteredLogs={filteredLogs} + /> +
+ + ); }; diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/add-node.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/add-node.tsx index eaa9851fc..a59681ba0 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/add-node.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/add-node.tsx @@ -56,10 +56,10 @@ export const AddNode = ({ serverId }: Props) => { Worker Manager - + - + diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx index 055c3f1cc..bb2064d4e 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/manager/add-manager.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { CardContent } from "@/components/ui/card"; import { DialogDescription, @@ -6,7 +7,7 @@ import { } from "@/components/ui/dialog"; import { api } from "@/utils/api"; import copy from "copy-to-clipboard"; -import { CopyIcon } from "lucide-react"; +import { CopyIcon, Loader2 } from "lucide-react"; import { toast } from "sonner"; interface Props { @@ -14,56 +15,66 @@ interface Props { } export const AddManager = ({ serverId }: Props) => { - const { data } = api.cluster.addManager.useQuery({ serverId }); + const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({ + serverId, + }); return ( <> -
- - - Add a new manager - Add a new manager - -
- 1. Go to your new server and run the following command - - curl https://get.docker.com | sh -s -- --version {data?.version} - - -
+ + + Add a new manager + Add a new manager + + {isError && {error?.message}} + {isLoading ? ( + + ) : ( + <> +
+ + 1. Go to your new server and run the following command + + + curl https://get.docker.com | sh -s -- --version {data?.version} + + +
-
- - 2. Run the following command to add the node(manager) to your - cluster - - - {data?.command} - - -
-
-
+
+ + 2. Run the following command to add the node(manager) to your + cluster + + + + {data?.command} + + +
+ + )} + ); }; diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes-modal.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes-modal.tsx index 32339e668..82e6e1f9a 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-nodes-modal.tsx @@ -17,7 +17,7 @@ export const ShowNodesModal = ({ serverId }: Props) => { className="w-full cursor-pointer " onSelect={(e) => e.preventDefault()} > - Show Nodes + Show Swarm Nodes diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx index 05f9838e7..2623081bb 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/workers/add-worker.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { CardContent } from "@/components/ui/card"; import { DialogDescription, @@ -6,7 +7,7 @@ import { } from "@/components/ui/dialog"; import { api } from "@/utils/api"; import copy from "copy-to-clipboard"; -import { CopyIcon } from "lucide-react"; +import { CopyIcon, Loader2 } from "lucide-react"; import { toast } from "sonner"; interface Props { @@ -14,54 +15,62 @@ interface Props { } export const AddWorker = ({ serverId }: Props) => { - const { data } = api.cluster.addWorker.useQuery({ serverId }); + const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({ + serverId, + }); return ( -
- - - Add a new worker - Add a new worker - -
- 1. Go to your new server and run the following command - - curl https://get.docker.com | sh -s -- --version {data?.version} - - -
+ + + Add a new worker + Add a new worker + + {isError && {error?.message}} + {isLoading ? ( + + ) : ( + <> +
+ 1. Go to your new server and run the following command + + curl https://get.docker.com | sh -s -- --version {data?.version} + + +
-
- - 2. Run the following command to add the node(worker) to your cluster - +
+ + 2. Run the following command to add the node(worker) to your + cluster + - - {data?.command} - - -
- -
+ + {data?.command} + + +
+ + )} + ); }; diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 7a3e286e3..e04765298 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => { {...field} onChange={(e) => { const value = e.target.value; - if (value) { + if (value === "") { + field.onChange(undefined); + } else { const port = Number.parseInt(value); if (port > 0 && port < 65536) { field.onChange(port); } } }} + value={field.value || ""} type="number" /> diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx index 92ef9f128..d20b7c91a 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -159,9 +159,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { - field.onChange(Number(e.target.value)) - } + onChange={(e) => { + const value = e.target.value; + field.onChange(value === "" ? undefined : Number(value)); + }} + value={field.value || ""} className="w-full dark:bg-black" placeholder="e.g. 8080" /> @@ -185,9 +187,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { - field.onChange(Number(e.target.value)) - } + onChange={(e) => { + const value = e.target.value; + field.onChange(value === "" ? undefined : Number(value)); + }} + value={field.value || ""} className="w-full dark:bg-black" placeholder="e.g. 80" /> diff --git a/apps/dokploy/components/ui/input.tsx b/apps/dokploy/components/ui/input.tsx index 7339d21a2..18b713af5 100644 --- a/apps/dokploy/components/ui/input.tsx +++ b/apps/dokploy/components/ui/input.tsx @@ -39,7 +39,7 @@ const NumberInput = React.forwardRef( className={cn("text-left", className)} ref={ref} {...props} - value={props.value === undefined || props.value === "" ? "" : String(props.value)} + value={props.value === undefined ? undefined : String(props.value)} onChange={(e) => { const value = e.target.value; if (value === "") { @@ -60,21 +60,6 @@ const NumberInput = React.forwardRef( } } }} - onBlur={(e) => { - // If input is empty, make 0 when focus is lost - if (e.target.value === "") { - const syntheticEvent = { - ...e, - target: { - ...e.target, - value: "0", - }, - }; - props.onChange?.( - syntheticEvent as unknown as React.ChangeEvent, - ); - } - }} /> ); }, diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 84d248975..1d7ad1311 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.20.5", + "version": "v0.20.7", "private": true, "license": "Apache-2.0", "type": "module", diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index 8e585b7c9..9ed8c6f93 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -31,7 +31,10 @@ import { import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { execAsync } from "@dokploy/server/utils/process/execAsync"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; import { getS3Credentials } from "@dokploy/server/utils/backups/utils"; import { findDestinationById } from "@dokploy/server/services/destination"; import { @@ -229,6 +232,7 @@ export const backupRouter = createTRPCRouter({ z.object({ destinationId: z.string(), search: z.string(), + serverId: z.string().optional(), }), ) .query(async ({ input }) => { @@ -250,7 +254,16 @@ export const backupRouter = createTRPCRouter({ const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath; const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`; - const { stdout } = await execAsync(listCommand); + let stdout = ""; + + if (input.serverId) { + const result = await execAsyncRemote(listCommand, input.serverId); + stdout = result.stdout; + } else { + const result = await execAsync(listCommand); + stdout = result.stdout; + } + const files = stdout.split("\n").filter(Boolean); const results = baseDir diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index aac2a016f..9e81bee16 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -13,7 +13,9 @@ import { findDomainById, findDomainsByApplicationId, findDomainsByComposeId, + findOrganizationById, findPreviewDeploymentById, + findServerById, generateTraefikMeDomain, manageDomain, removeDomain, @@ -94,6 +96,19 @@ export const domainRouter = createTRPCRouter({ input.serverId, ); }), + canGenerateTraefikMeDomains: protectedProcedure + .input(z.object({ serverId: z.string() })) + .query(async ({ input, ctx }) => { + const organization = await findOrganizationById( + ctx.session.activeOrganizationId, + ); + + if (input.serverId) { + const server = await findServerById(input.serverId); + return server.ipAddress; + } + return organization?.owner.serverIp; + }), update: protectedProcedure .input(apiUpdateDomain) diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 9043f2030..6695756c5 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -8,6 +8,10 @@ import { db } from "../db"; import * as schema from "../db/schema"; import { sendEmail } from "../verification/send-verification-email"; import { IS_CLOUD } from "../constants"; +import { getPublicIpWithFallback } from "../wss/utils"; +import { updateUser } from "../services/user"; +import { getUserByToken } from "../services/admin"; +import { APIError } from "better-auth/api"; const { handler, api } = betterAuth({ database: drizzleAdapter(db, { @@ -88,11 +92,40 @@ const { handler, api } = betterAuth({ databaseHooks: { user: { create: { + before: async (_user, context) => { + if (!IS_CLOUD) { + const xDokployToken = + context?.request?.headers?.get("x-dokploy-token"); + if (xDokployToken) { + const user = await getUserByToken(xDokployToken); + if (!user) { + throw new APIError("BAD_REQUEST", { + message: "User not found", + }); + } + } else { + const isAdminPresent = await db.query.member.findFirst({ + where: eq(schema.member.role, "owner"), + }); + if (isAdminPresent) { + throw new APIError("BAD_REQUEST", { + message: "Admin is already created", + }); + } + } + } + }, after: async (user) => { const isAdminPresent = await db.query.member.findFirst({ where: eq(schema.member.role, "owner"), }); + if (!IS_CLOUD) { + await updateUser(user.id, { + serverIp: await getPublicIpWithFallback(), + }); + } + if (IS_CLOUD || !isAdminPresent) { await db.transaction(async (tx) => { const organization = await tx diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 677ce74ac..aaacdb33c 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -361,7 +361,7 @@ const installUtilities = () => ` alpine) sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories apk update >/dev/null - apk add curl wget git jq openssl >/dev/null + apk add curl wget git jq openssl sudo unzip tar >/dev/null ;; ubuntu | debian | raspbian) DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null