diff --git a/apps/dokploy/components/auth/login-2fa.tsx b/apps/dokploy/components/auth/login-2fa.tsx index dcb004f1d..836c3b46c 100644 --- a/apps/dokploy/components/auth/login-2fa.tsx +++ b/apps/dokploy/components/auth/login-2fa.tsx @@ -13,10 +13,12 @@ import { CardTitle } from "@/components/ui/card"; import { InputOTP, InputOTPGroup, + InputOTPSeparator, InputOTPSlot, } from "@/components/ui/input-otp"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; +import { REGEXP_ONLY_DIGITS } from "input-otp"; import { AlertTriangle } from "lucide-react"; import { useRouter } from "next/router"; import { useEffect } from "react"; @@ -96,16 +98,22 @@ export const Login2FA = ({ authId }: Props) => { Pin - - - - - - - - - - +
+ + + + + + + + + + +
Please enter the 6 digits code provided by your authenticator diff --git a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx index f9e70b422..4cd839a11 100644 --- a/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx @@ -81,7 +81,8 @@ export const AddCommand = ({ applicationId }: Props) => {
Run Command - Run a custom command in the container + Run a custom command in the container after the application + initialized
diff --git a/apps/dokploy/components/dashboard/application/environment/show.tsx b/apps/dokploy/components/dashboard/application/environment/show.tsx index ec5558043..970464524 100644 --- a/apps/dokploy/components/dashboard/application/environment/show.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show.tsx @@ -55,13 +55,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => { }); }; - return ( -
- - + return ( + + + { placeholder="NPM_TOKEN=xyz" /> )} - -
- -
-
-
- - - ); +
+ +
+ + +
+ ); }; diff --git a/apps/dokploy/components/dashboard/requests/show-requests.tsx b/apps/dokploy/components/dashboard/requests/show-requests.tsx index 643d2bcb9..a12e250b8 100644 --- a/apps/dokploy/components/dashboard/requests/show-requests.tsx +++ b/apps/dokploy/components/dashboard/requests/show-requests.tsx @@ -94,10 +94,18 @@ export const ShowRequests = () => { - + {isActive ? ( + + ) : ( +
+ + You need to activate requests + +
+ )}
- + {isActive && } ); }; 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 6c784b520..a30823363 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 @@ -18,6 +18,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, @@ -114,7 +115,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
- {t("settings.server.webServer.traefik.managePortsDescription")} +
+ {t( + "settings.server.webServer.traefik.managePortsDescription", + )} + + {fields.length} port mapping{fields.length !== 1 ? "s" : ""}{" "} + configured + +
) : ( -
- {fields.map((field, index) => ( - - - ( - - - {t( - "settings.server.webServer.traefik.targetPort", - )} - - - - field.onChange(Number(e.target.value)) - } - className="w-full dark:bg-black" - placeholder="e.g. 8080" - /> - - - - )} - /> - - ( - - - {t( - "settings.server.webServer.traefik.publishedPort", - )} - - - - field.onChange(Number(e.target.value)) - } - className="w-full dark:bg-black" - placeholder="e.g. 80" - /> - - - - )} - /> - - ( - - - {t( - "settings.server.webServer.traefik.publishMode", - )} - - + field.onChange(Number(e.target.value)) + } + className="w-full dark:bg-black" + placeholder="e.g. 8080" + /> - - - Host Mode - - - Ingress Mode - - - - - - )} - /> + + + )} + /> -
- -
-
-
- ))} -
+ ( + + + {t( + "settings.server.webServer.traefik.publishedPort", + )} + + + + field.onChange(Number(e.target.value)) + } + className="w-full dark:bg-black" + placeholder="e.g. 80" + /> + + + + )} + /> + + ( + + + {t( + "settings.server.webServer.traefik.publishMode", + )} + + + + + )} + /> + +
+ +
+ + + ))} + + )} {fields.length > 0 && ( @@ -281,7 +292,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { )} - @@ -99,10 +123,6 @@ export const UpdateServer = () => {
- - - - New version available: diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx index c1e5de706..537a1adde 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx @@ -11,30 +11,50 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; -import { HardDriveDownload } from "lucide-react"; +import { HardDriveDownload, Loader2 } from "lucide-react"; +import { useState } from "react"; import { toast } from "sonner"; -interface Props { - isNavbar?: boolean; -} +export const UpdateWebServer = () => { + const [updating, setUpdating] = useState(false); + const [open, setOpen] = useState(false); -export const UpdateWebServer = ({ isNavbar }: Props) => { - const { mutateAsync: updateServer, isLoading } = - api.settings.updateServer.useMutation(); + const { mutateAsync: updateServer } = api.settings.updateServer.useMutation(); - const buttonLabel = isNavbar ? "Update available" : "Update Server"; - - const handleConfirm = async () => { + const checkIsUpdateFinished = async () => { try { - await updateServer(); + const response = await fetch("/api/health"); + if (!response.ok) { + throw new Error("Health check failed"); + } + toast.success( "The server has been updated. The page will be reloaded to reflect the changes...", ); + setTimeout(() => { // Allow seeing the toast before reloading window.location.reload(); }, 2000); + } catch { + // Delay each request + await new Promise((resolve) => setTimeout(resolve, 2000)); + // Keep running until it returns 200 + void checkIsUpdateFinished(); + } + }; + + const handleConfirm = async () => { + try { + setUpdating(true); + await updateServer(); + + // Give some time for docker service restart before starting to check status + await new Promise((resolve) => setTimeout(resolve, 8000)); + + await checkIsUpdateFinished(); } catch (error) { + setUpdating(false); console.error("Error updating server:", error); toast.error( "An error occurred while updating the server, please try again.", @@ -43,35 +63,54 @@ export const UpdateWebServer = ({ isNavbar }: Props) => { }; return ( - + - Are you absolutely sure? + + {updating + ? "Server update in progress" + : "Are you absolutely sure?"} + - This action cannot be undone. This will update the web server to the - new version. The page will be reloaded once the update is finished. + {updating ? ( + + + The server is being updated, please wait... + + ) : ( + <> + This action cannot be undone. This will update the web server to + the new version. You will not be able to use the panel during + the update process. The page will be reloaded once the update is + finished. + + )} - - Cancel - Confirm - + {!updating && ( + + setOpen(false)}> + Cancel + + + Confirm + + + )} ); diff --git a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx index 2de0901a0..eb06e79cb 100644 --- a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx @@ -63,7 +63,7 @@ export function NodeCard({ node, serverId }: Props) {
-
+
{node.Hostname}
diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index bd8570db9..0dd34852a 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -2,29 +2,23 @@ import { Badge } from "@/components/ui/badge"; 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 { - Activity, - Loader2, - Monitor, - Settings, - Server, -} from "lucide-react"; +import { Activity, Loader2, Monitor, Settings, Server } from "lucide-react"; import { NodeCard } from "./details/details-card"; interface Props { - serverId?: string; + serverId?: string; } export default function SwarmMonitorCard({ serverId }: Props) { - const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({ - serverId, - }); + const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({ + serverId, + }); if (isLoading) { return ( @@ -50,116 +44,132 @@ export default function SwarmMonitorCard({ serverId }: Props) { ); } - const totalNodes = nodes.length; - const activeNodesCount = nodes.filter((node) => node.Status === "Ready").length; - const managerNodesCount = nodes.filter((node) =>node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable").length; - const activeNodes = nodes.filter((node) => node.Status === "Ready"); - const managerNodes = nodes.filter((node) => node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable"); + const totalNodes = nodes.length; + const activeNodesCount = nodes.filter( + (node) => node.Status === "Ready", + ).length; + const managerNodesCount = nodes.filter( + (node) => + node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable", + ).length; + const activeNodes = nodes.filter((node) => node.Status === "Ready"); + const managerNodes = nodes.filter( + (node) => + node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable", + ); - return ( -
-
-
-
-

Docker Swarm Overview

-

Monitor and manage your Docker Swarm cluster

-
- {!serverId && ( - - )} -
+ return ( +
+
+
+
+

+ Docker Swarm Overview +

+

+ Monitor and manage your Docker Swarm cluster +

+
+ {!serverId && ( + + )} +
-
- - - Total Nodes -
- -
-
- -
{totalNodes}
-
-
+
+ + + Total Nodes +
+ +
+
+ +
{totalNodes}
+
+
- - - - Active Nodes - - Online - - -
- -
-
- - - - -
- {activeNodesCount} / {totalNodes} -
-
- -
- {activeNodes.map((node) => ( -
- {node.Hostname} -
- ))} -
-
-
-
-
-
+ + +
+ + Active Nodes + + Online +
+
+ +
+
+ + + + +
+ {activeNodesCount} / {totalNodes} +
+
+ +
+ {activeNodes.map((node) => ( +
+ {node.Hostname} +
+ ))} +
+
+
+
+
+
- - - - Manager Nodes - - Online - - -
- -
-
- - - - -
- {managerNodesCount} / {totalNodes} -
-
- -
- {managerNodes.map((node) => ( -
- {node.Hostname} -
- ))} -
-
-
-
-
-
-
+ + +
+ + Manager Nodes + + Online +
+
+ +
+
+ + + + +
+ {managerNodesCount} / {totalNodes} +
+
+ +
+ {managerNodes.map((node) => ( +
+ {node.Hostname} +
+ ))} +
+
+
+
+
+
+
-
- {nodes.map((node) => ( - - ))} -
-
-
- ); -} \ No newline at end of file +
+ {nodes.map((node) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index 0e52d7019..bf733bd23 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -13,15 +13,19 @@ import { HeartIcon } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; -import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; import { Logo } from "../shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { buttonVariants } from "../ui/button"; +import UpdateServer from "../dashboard/settings/web-server/update-server"; +import type { IUpdateData } from "@dokploy/server/index"; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; export const Navbar = () => { - const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); + const [updateData, setUpdateData] = useState({ + latestVersion: null, + updateAvailable: false, + }); const router = useRouter(); const { data } = api.auth.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -62,12 +66,12 @@ export const Navbar = () => { return; } - const { updateAvailable } = await getUpdateData(); + const fetchedUpdateData = await getUpdateData(); - if (updateAvailable) { + if (fetchedUpdateData?.updateAvailable) { // Stop interval when update is available clearUpdatesInterval(); - setIsUpdateAvailable(true); + setUpdateData(fetchedUpdateData); } } catch (error) { console.error("Error auto-checking for updates:", error); @@ -101,9 +105,9 @@ export const Navbar = () => {
- {isUpdateAvailable && ( + {updateData.updateAvailable && (
- +
)} - - - + + + + diff --git a/apps/dokploy/public/locales/pl/settings.json b/apps/dokploy/public/locales/pl/settings.json index 934148251..a87b82cba 100644 --- a/apps/dokploy/public/locales/pl/settings.json +++ b/apps/dokploy/public/locales/pl/settings.json @@ -19,6 +19,14 @@ "settings.server.webServer.server.label": "Serwer", "settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko", + "settings.server.webServer.traefik.managePorts": "Dodatkowe mapowania portów", + "settings.server.webServer.traefik.managePortsDescription": "Dodaj lub usuń dodatkowe porty dla Traefik", + "settings.server.webServer.traefik.targetPort": "Port docelowy", + "settings.server.webServer.traefik.publishedPort": "Port opublikowany", + "settings.server.webServer.traefik.addPort": "Dodaj port", + "settings.server.webServer.traefik.portsUpdated": "Porty zaktualizowane pomyślnie", + "settings.server.webServer.traefik.portsUpdateError": "Nie udało się zaktualizować portów", + "settings.server.webServer.traefik.publishMode": "Tryb publikacji", "settings.server.webServer.storage.label": "Przestrzeń", "settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy", "settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny", diff --git a/apps/dokploy/public/templates/checkmate.png b/apps/dokploy/public/templates/checkmate.png new file mode 100644 index 000000000..759269ba8 Binary files /dev/null and b/apps/dokploy/public/templates/checkmate.png differ diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index b40ee95bd..449a22333 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -359,7 +359,9 @@ export const settingsRouter = createTRPCRouter({ await pullLatestRelease(); - await spawnAsync("docker", [ + // 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", diff --git a/apps/dokploy/templates/checkmate/docker-compose.yml b/apps/dokploy/templates/checkmate/docker-compose.yml new file mode 100644 index 000000000..dc83a28f6 --- /dev/null +++ b/apps/dokploy/templates/checkmate/docker-compose.yml @@ -0,0 +1,47 @@ +services: + client: + image: bluewaveuptime/uptime_client:latest + restart: always + environment: + UPTIME_APP_API_BASE_URL: "http://${DOMAIN}/api/v1" + ports: + - 80 + - 443 + depends_on: + - server + networks: + - dokploy-network + server: + image: bluewaveuptime/uptime_server:latest + restart: always + ports: + - 5000 + depends_on: + - redis + - mongodb + environment: + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db + - REDIS_HOST=redis + networks: + - dokploy-network + # volumes: + # - /var/run/docker.sock:/var/run/docker.sock:ro + redis: + image: bluewaveuptime/uptime_redis:latest + restart: always + ports: + - 6379 + volumes: + - ../files/redis/data:/data + networks: + - dokploy-network + mongodb: + image: bluewaveuptime/uptime_database_mongo:latest + restart: always + volumes: + - ../files/mongo/data:/data/db + command: ["mongod", "--quiet"] + ports: + - 27017 + networks: + - dokploy-network \ No newline at end of file diff --git a/apps/dokploy/templates/checkmate/index.ts b/apps/dokploy/templates/checkmate/index.ts new file mode 100644 index 000000000..e52370aae --- /dev/null +++ b/apps/dokploy/templates/checkmate/index.ts @@ -0,0 +1,25 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const envs = [`DOMAIN=${mainDomain}`]; + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "client", + }, + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/huly/index.ts b/apps/dokploy/templates/huly/index.ts index deb5c7db3..3157ed51e 100644 --- a/apps/dokploy/templates/huly/index.ts +++ b/apps/dokploy/templates/huly/index.ts @@ -11,7 +11,7 @@ export function generate(schema: Schema): Template { const hulySecret = generateBase64(64); const domains: DomainSchema[] = [ { - host: generateRandomDomain(schema), + host: mainDomain, port: 80, serviceName: "nginx", }, diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 2dcc68b32..510234ca7 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1180,5 +1180,20 @@ export const templates: TemplateData[] = [ }, tags: ["self-hosted", "project-management", "management"], load: () => import("./glpi/index").then((m) => m.generate), + }, + { + id: "checkmate", + name: "Checkmate", + version: "2.0.1", + description: + "Checkmate is an open-source, self-hosted tool designed to track and monitor server hardware, uptime, response times, and incidents in real-time with beautiful visualizations.", + logo: "checkmate.png", + links: { + github: "https://github.com/bluewave-labs/checkmate", + website: "https://bluewavelabs.ca", + docs: "https://bluewavelabs.gitbook.io/checkmate", + }, + tags: ["self-hosted", "monitoring", "uptime"], + load: () => import("./checkmate/index").then((m) => m.generate), }, ]; diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index 4fa46c331..83a645204 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -360,7 +360,7 @@ const installUtilities = () => ` ;; ubuntu | debian | raspbian) DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null - DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git jq openssl >/dev/null + DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null ;; centos | fedora | rhel | ol | rocky | almalinux | amzn) if [ "$OS_TYPE" = "amzn" ]; then diff --git a/packages/server/src/utils/gpu-setup.ts b/packages/server/src/utils/gpu-setup.ts index 0b49dc6ca..6a6611b4a 100644 --- a/packages/server/src/utils/gpu-setup.ts +++ b/packages/server/src/utils/gpu-setup.ts @@ -35,7 +35,6 @@ export async function checkGPUStatus(serverId?: string): Promise { ...cudaInfo, }; } catch (error) { - console.error("Error in checkGPUStatus:", error); return { driverInstalled: false, driverVersion: undefined, @@ -317,7 +316,6 @@ const setupLocalServer = async (daemonConfig: any) => { try { await execAsync(setupCommands); } catch (error) { - console.error("Setup failed:", error); throw new Error( "Failed to configure GPU support. Please ensure you have sudo privileges and try again.", ); @@ -344,11 +342,10 @@ const verifySetup = async (nodeId: string, serverId?: string) => { "cat /etc/nvidia-container-runtime/config.toml", ].join(" && "); - const { stdout: diagnostics } = serverId - ? await execAsyncRemote(serverId, diagnosticCommands) - : await execAsync(diagnosticCommands); + await (serverId + ? execAsyncRemote(serverId, diagnosticCommands) + : execAsync(diagnosticCommands)); - console.error("Diagnostic Information:", diagnostics); throw new Error("GPU support not detected in swarm after setup"); }