From d5389024d795a8cf51c7ee499307f2593cdfa3b1 Mon Sep 17 00:00:00 2001 From: Sagar Chhetri Date: Fri, 7 Aug 2026 08:46:36 +0545 Subject: [PATCH 1/6] feat: auto-validate DNS when Domains page loads Run domain DNS checks automatically once domains and server IP context are available, so users no longer need to click Validate DNS on every visit. --- .../application/domains/show-domains.tsx | 94 ++++++++++++------- 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 23d9e46bb6..d2943162fd 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -25,7 +25,7 @@ import { XCircle, } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; @@ -84,7 +84,7 @@ export const ShowDomains = ({ id, type }: Props) => { const { data: permissions } = api.user.getPermissions.useQuery(); const canCreateDomain = permissions?.domain.create ?? false; const canDeleteDomain = permissions?.domain.delete ?? false; - const { data: application } = + const { data: application, isFetched: isApplicationFetched } = type === "application" ? api.application.one.useQuery( { @@ -105,6 +105,7 @@ export const ShowDomains = ({ id, type }: Props) => { const [validationStates, setValidationStates] = useState( {}, ); + const autoValidatedHostsRef = useRef>(new Set()); const [viewMode, setViewMode] = useState<"grid" | "table">(() => { if (typeof window !== "undefined") { return ( @@ -118,7 +119,7 @@ export const ShowDomains = ({ id, type }: Props) => { const [columnFilters, setColumnFilters] = useState([]); const [columnVisibility, setColumnVisibility] = useState({}); const [rowSelection, setRowSelection] = useState({}); - const { data: ip } = api.settings.getIp.useQuery(); + const { data: ip, isFetched: isIpFetched } = api.settings.getIp.useQuery(); const { data, @@ -157,42 +158,65 @@ export const ShowDomains = ({ id, type }: Props) => { } }; - const handleValidateDomain = async (host: string) => { - setValidationStates((prev) => ({ - ...prev, - [host]: { isLoading: true }, - })); - - try { - const result = await validateDomain({ - domain: host, - serverIp: - application?.server?.ipAddress?.toString() || ip?.toString() || "", - }); - - setValidationStates((prev) => ({ - ...prev, - [host]: { - isLoading: false, - isValid: result.isValid, - error: result.error, - resolvedIp: result.resolvedIp, - cdnProvider: result.cdnProvider, - message: result.error && result.isValid ? result.error : undefined, - }, - })); - } catch (err) { - const error = err as Error; + const handleValidateDomain = useCallback( + async (host: string) => { setValidationStates((prev) => ({ ...prev, - [host]: { - isLoading: false, - isValid: false, - error: error.message || "Failed to validate domain", - }, + [host]: { isLoading: true }, })); + + try { + const result = await validateDomain({ + domain: host, + serverIp: + application?.server?.ipAddress?.toString() || ip?.toString() || "", + }); + + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: result.isValid, + error: result.error, + resolvedIp: result.resolvedIp, + cdnProvider: result.cdnProvider, + message: result.error && result.isValid ? result.error : undefined, + }, + })); + } catch (err) { + const error = err as Error; + setValidationStates((prev) => ({ + ...prev, + [host]: { + isLoading: false, + isValid: false, + error: error.message || "Failed to validate domain", + }, + })); + } + }, + [validateDomain, application?.server?.ipAddress, ip], + ); + + useEffect(() => { + autoValidatedHostsRef.current = new Set(); + setValidationStates({}); + }, [id]); + + useEffect(() => { + if (!data?.length || !isIpFetched || !isApplicationFetched) { + return; } - }; + + for (const item of data) { + if (autoValidatedHostsRef.current.has(item.host)) { + continue; + } + + autoValidatedHostsRef.current.add(item.host); + void handleValidateDomain(item.host); + } + }, [data, isIpFetched, isApplicationFetched, handleValidateDomain]); const columns = createColumns({ id, From cc85be225c0c7f5713232f0ad146e6b8ea7f5152 Mon Sep 17 00:00:00 2001 From: Sagar Chhetri Date: Fri, 7 Aug 2026 08:48:52 +0545 Subject: [PATCH 2/6] fix: harden auto DNS validation against stale and empty IP results Ignore in-flight validation results after switching services, wait for a real server IP before auto-checking, and limit concurrent DNS lookups. --- .../application/domains/show-domains.tsx | 95 +++++++++++++++++-- 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index d2943162fd..f24a5f0378 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -106,6 +106,7 @@ export const ShowDomains = ({ id, type }: Props) => { {}, ); const autoValidatedHostsRef = useRef>(new Set()); + const validationRequestIdRef = useRef(0); const [viewMode, setViewMode] = useState<"grid" | "table">(() => { if (typeof window !== "undefined") { return ( @@ -158,8 +159,26 @@ export const ShowDomains = ({ id, type }: Props) => { } }; + const resolveServerIp = useCallback(() => { + const remoteIp = application?.server?.ipAddress?.toString(); + if (application?.serverId) { + return remoteIp || undefined; + } + + return ip?.toString() || undefined; + }, [application?.server?.ipAddress, application?.serverId, ip]); + const handleValidateDomain = useCallback( - async (host: string) => { + async (host: string, serverIpOverride?: string) => { + const requestId = validationRequestIdRef.current; + const serverIp = + serverIpOverride ?? + (application?.server?.ipAddress?.toString() || ip?.toString() || ""); + + if (validationRequestIdRef.current !== requestId) { + return; + } + setValidationStates((prev) => ({ ...prev, [host]: { isLoading: true }, @@ -168,10 +187,13 @@ export const ShowDomains = ({ id, type }: Props) => { try { const result = await validateDomain({ domain: host, - serverIp: - application?.server?.ipAddress?.toString() || ip?.toString() || "", + serverIp, }); + if (validationRequestIdRef.current !== requestId) { + return; + } + setValidationStates((prev) => ({ ...prev, [host]: { @@ -184,6 +206,10 @@ export const ShowDomains = ({ id, type }: Props) => { }, })); } catch (err) { + if (validationRequestIdRef.current !== requestId) { + return; + } + const error = err as Error; setValidationStates((prev) => ({ ...prev, @@ -199,24 +225,73 @@ export const ShowDomains = ({ id, type }: Props) => { ); useEffect(() => { + validationRequestIdRef.current += 1; autoValidatedHostsRef.current = new Set(); setValidationStates({}); }, [id]); useEffect(() => { - if (!data?.length || !isIpFetched || !isApplicationFetched) { + if (!data?.length || !isApplicationFetched) { return; } - for (const item of data) { - if (autoValidatedHostsRef.current.has(item.host)) { - continue; + if (application?.serverId) { + if (!application.server?.ipAddress) { + return; } + } else if (!isIpFetched) { + return; + } + + const serverIp = resolveServerIp(); + if (!serverIp) { + return; + } + + const hostsToValidate = data + .map((item) => item.host) + .filter((host) => { + if (autoValidatedHostsRef.current.has(host)) { + return false; + } - autoValidatedHostsRef.current.add(item.host); - void handleValidateDomain(item.host); + autoValidatedHostsRef.current.add(host); + return true; + }); + + if (hostsToValidate.length === 0) { + return; } - }, [data, isIpFetched, isApplicationFetched, handleValidateDomain]); + + const maxConcurrent = 5; + let nextIndex = 0; + + const runNext = async () => { + while (nextIndex < hostsToValidate.length) { + const host = hostsToValidate[nextIndex]; + nextIndex += 1; + if (!host) { + continue; + } + await handleValidateDomain(host, serverIp); + } + }; + + void Promise.all( + Array.from( + { length: Math.min(maxConcurrent, hostsToValidate.length) }, + () => runNext(), + ), + ); + }, [ + data, + isIpFetched, + isApplicationFetched, + application?.serverId, + application?.server?.ipAddress, + resolveServerIp, + handleValidateDomain, + ]); const columns = createColumns({ id, From 095b25683644cacc5f4d2b8b4a4b2dfafe9a905d Mon Sep 17 00:00:00 2001 From: Sagar Chhetri Date: Fri, 7 Aug 2026 09:00:30 +0545 Subject: [PATCH 3/6] fix: ignore stale DNS results when a newer host check starts Track a per-host validation generation so an older automatic check cannot overwrite a newer manual re-validation result. --- .../application/domains/show-domains.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index f24a5f0378..5ff143c1c7 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -107,6 +107,7 @@ export const ShowDomains = ({ id, type }: Props) => { ); const autoValidatedHostsRef = useRef>(new Set()); const validationRequestIdRef = useRef(0); + const hostValidationRequestIdsRef = useRef>(new Map()); const [viewMode, setViewMode] = useState<"grid" | "table">(() => { if (typeof window !== "undefined") { return ( @@ -170,12 +171,20 @@ export const ShowDomains = ({ id, type }: Props) => { const handleValidateDomain = useCallback( async (host: string, serverIpOverride?: string) => { - const requestId = validationRequestIdRef.current; + const serviceRequestId = validationRequestIdRef.current; + const hostRequestId = + (hostValidationRequestIdsRef.current.get(host) ?? 0) + 1; + hostValidationRequestIdsRef.current.set(host, hostRequestId); + const serverIp = serverIpOverride ?? (application?.server?.ipAddress?.toString() || ip?.toString() || ""); - if (validationRequestIdRef.current !== requestId) { + const isCurrentRequest = () => + validationRequestIdRef.current === serviceRequestId && + hostValidationRequestIdsRef.current.get(host) === hostRequestId; + + if (!isCurrentRequest()) { return; } @@ -190,7 +199,7 @@ export const ShowDomains = ({ id, type }: Props) => { serverIp, }); - if (validationRequestIdRef.current !== requestId) { + if (!isCurrentRequest()) { return; } @@ -206,7 +215,7 @@ export const ShowDomains = ({ id, type }: Props) => { }, })); } catch (err) { - if (validationRequestIdRef.current !== requestId) { + if (!isCurrentRequest()) { return; } @@ -227,6 +236,7 @@ export const ShowDomains = ({ id, type }: Props) => { useEffect(() => { validationRequestIdRef.current += 1; autoValidatedHostsRef.current = new Set(); + hostValidationRequestIdsRef.current = new Map(); setValidationStates({}); }, [id]); From b05f6d1dc1dc90644d013733d2b21c74361585ae Mon Sep 17 00:00:00 2001 From: Sagar Chhetri Date: Fri, 7 Aug 2026 11:38:19 +0545 Subject: [PATCH 4/6] fix: wait for service data before auto DNS validation Require the application/compose object to be present so auto-validation does not fall back to the global IP when service context is still missing. --- .../components/dashboard/application/domains/show-domains.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 5ff143c1c7..1ceb964d90 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -241,11 +241,11 @@ export const ShowDomains = ({ id, type }: Props) => { }, [id]); useEffect(() => { - if (!data?.length || !isApplicationFetched) { + if (!data?.length || !isApplicationFetched || !application) { return; } - if (application?.serverId) { + if (application.serverId) { if (!application.server?.ipAddress) { return; } From 4fd256e666c3b033e3537939540cb120b6738775 Mon Sep 17 00:00:00 2001 From: Sagar Chhetri Date: Fri, 7 Aug 2026 12:00:40 +0545 Subject: [PATCH 5/6] fix: include application in auto-validation effect deps Keep the effect dependency list in sync with the service-data guard so auto DNS validation reruns when the loaded service object becomes available. --- .../components/dashboard/application/domains/show-domains.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 1ceb964d90..18eb90a395 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -297,6 +297,7 @@ export const ShowDomains = ({ id, type }: Props) => { data, isIpFetched, isApplicationFetched, + application, application?.serverId, application?.server?.ipAddress, resolveServerIp, From 121496672ae161be073ccdb6d6e82ec4e4de27d6 Mon Sep 17 00:00:00 2001 From: Sagar Chhetri Date: Fri, 7 Aug 2026 12:04:05 +0545 Subject: [PATCH 6/6] fix: re-run auto DNS validation when expected server IP changes Clear the processed-host cache when the resolved server IP changes so domain badges refresh against the current expected IP. --- .../dashboard/application/domains/show-domains.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 18eb90a395..903eb82f0f 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -108,6 +108,7 @@ export const ShowDomains = ({ id, type }: Props) => { const autoValidatedHostsRef = useRef>(new Set()); const validationRequestIdRef = useRef(0); const hostValidationRequestIdsRef = useRef>(new Map()); + const lastAutoValidatedServerIpRef = useRef(undefined); const [viewMode, setViewMode] = useState<"grid" | "table">(() => { if (typeof window !== "undefined") { return ( @@ -237,6 +238,7 @@ export const ShowDomains = ({ id, type }: Props) => { validationRequestIdRef.current += 1; autoValidatedHostsRef.current = new Set(); hostValidationRequestIdsRef.current = new Map(); + lastAutoValidatedServerIpRef.current = undefined; setValidationStates({}); }, [id]); @@ -258,6 +260,11 @@ export const ShowDomains = ({ id, type }: Props) => { return; } + if (lastAutoValidatedServerIpRef.current !== serverIp) { + lastAutoValidatedServerIpRef.current = serverIp; + autoValidatedHostsRef.current = new Set(); + } + const hostsToValidate = data .map((item) => item.host) .filter((host) => {