To Reproduce
- Add a remote server to Dokploy whose SSH address is a private or internal IP (for example a Tailscale, VPN, or LAN address), while the server is also reachable on a separate public IP.
- Deploy an application or Docker Compose service to that server and add a domain to it.
- Point the domain's DNS A record to the server's public IP (the address visitors actually reach).
- Open the domain list and click the "Validate" badge for that domain.
The same problem appears for an app deployed on the Dokploy host itself when the host has several addresses (for example a public IP plus a NAT or floating IP) and DNS points to one that is not the single stored serverIp.
Current vs. Expected behavior
Current: the badge turns red and reports "Invalid", with a message like "Domain resolves to X but should point to Y", even though the DNS is configured correctly. Validation only compares the domain's resolved IPs against a single stored address (the remote server's SSH ipAddress, or the Dokploy host's auto detected serverIp), so any other valid IP of the server is treated as wrong.
Expected: validation should pass when the domain resolves to any IP that genuinely belongs to the target server, and it should have a sensible answer for setups where DNS legitimately points somewhere other than the server (a reverse proxy or load balancer), instead of producing a false negative.
Provide environment information
Dokploy version: v0.29.8
Deployment: self hosted
Setup: remote server reachable on a private SSH IP and a separate public IP, with DNS pointing to the public IP
What is affected: domain DNS validation badge on applications and compose services
Which area(s) are affected?
Application, Docker Compose, Traefik, Remote server
Are you deploying where Dokploy is installed or on a remote server?
Both
Root cause
The validation path compares the domain's resolved IPs against a single expected IP. In packages/server/src/services/domain.ts, validateDomain resolves the domain (potentially to several IPs) but checks them against one expectedIp string:
if (expectedIp) {
return {
isValid: resolvedIps.includes(expectedIp),
...
};
}
That single value comes from the UI in components/dashboard/application/domains/show-domains.tsx, which passes application?.server?.ipAddress (the remote server's SSH IP) or the Dokploy host's serverIp. The server table stores only one ipAddress (the SSH address), so Dokploy has no knowledge of the server's other IPs (public egress IP, secondary interfaces). When DNS points to a different but still valid IP, the check fails.
Edge cases to account for
- Server with multiple IPs (the reported case): internal or SSH IP plus a public IP, secondary interfaces, or overlay addresses (Tailscale, WireGuard). Dokploy currently knows only the one stored address.
- Reverse proxy or load balancer in front: DNS points at something that is not the server at all (a self hosted nginx or HAProxy, a corporate load balancer, Cloudflare Tunnel, a keepalived or VRRP floating VIP, another Traefik). Dokploy cannot derive that IP from the server. Today only a hardcoded CDN list (Cloudflare, Fastly, Bunny, Arvancloud) is treated as valid, so every other proxy is a false negative.
- NAT or asymmetric egress: a server behind NAT has its public inbound IP on no local interface. An egress lookup (querying a public IP service) usually recovers it, but with multiple WAN links the egress IP can differ from the inbound IP.
- IPv6 or AAAA only: validation currently resolves A records only (
dns.resolve4), so a domain pointed via AAAA validates as failed.
- Split horizon DNS and resolver perspective: validation resolves from the Dokploy host's resolver, which can differ from the public internet's view (internal DNS, GeoDNS, or stale cache during propagation), producing transient or perspective dependent false negatives.
- Cost of an egress probe: detecting the public IP by calling an external service on every validation adds latency, can be blocked on air gapped or firewalled hosts, and is a small trust surface. An explicit user provided IP removes the need for it.
Proposed solution (layered)
Layer 1, automatic detection (fixes the reported bug, items 1 and most of 3):
Gather the full set of IPs the target server is reachable at (the stored address, the globally scoped interface addresses, and the detected public egress IP) and consider the domain valid when it resolves to any of them. For a remote server this is collected over SSH, for the Dokploy host it is collected locally. Any failure to reach the server falls back to the currently stored address, so behavior never regresses.
Layer 2, per domain validation mode (covers items 2, 4, 6 and the rest of 3):
Add a small per domain control that generalizes the existing CDN special case into an explicit, user driven setting. Three modes:
- Auto: detect and match the server's IPs (the Layer 1 behavior, and the default).
- Proxy or custom IP: the user enters the IP the domain should resolve to (for example the reverse proxy or load balancer address) and validation checks against that.
- Skip: the user marks the domain as fronted by a proxy, and validation only confirms that the domain resolves, showing an informational state rather than a red "Invalid".
This lives on the domain (proxy setups are per route) and would need a new column on the domain plus the matching UI and router wiring.
Notes
Layer 1 is a self contained bug fix. Layer 2 is a feature (schema, UI, and router changes) and, per CONTRIBUTING.md, is the kind of change worth agreeing on before implementation. Happy to open a focused PR for Layer 1 and follow with Layer 2 once the approach is confirmed.
Will you send a PR to fix it?
Yes
To Reproduce
The same problem appears for an app deployed on the Dokploy host itself when the host has several addresses (for example a public IP plus a NAT or floating IP) and DNS points to one that is not the single stored
serverIp.Current vs. Expected behavior
Current: the badge turns red and reports "Invalid", with a message like "Domain resolves to X but should point to Y", even though the DNS is configured correctly. Validation only compares the domain's resolved IPs against a single stored address (the remote server's SSH
ipAddress, or the Dokploy host's auto detectedserverIp), so any other valid IP of the server is treated as wrong.Expected: validation should pass when the domain resolves to any IP that genuinely belongs to the target server, and it should have a sensible answer for setups where DNS legitimately points somewhere other than the server (a reverse proxy or load balancer), instead of producing a false negative.
Provide environment information
Which area(s) are affected?
Application, Docker Compose, Traefik, Remote server
Are you deploying where Dokploy is installed or on a remote server?
Both
Root cause
The validation path compares the domain's resolved IPs against a single expected IP. In
packages/server/src/services/domain.ts,validateDomainresolves the domain (potentially to several IPs) but checks them against oneexpectedIpstring:That single value comes from the UI in
components/dashboard/application/domains/show-domains.tsx, which passesapplication?.server?.ipAddress(the remote server's SSH IP) or the Dokploy host'sserverIp. Theservertable stores only oneipAddress(the SSH address), so Dokploy has no knowledge of the server's other IPs (public egress IP, secondary interfaces). When DNS points to a different but still valid IP, the check fails.Edge cases to account for
dns.resolve4), so a domain pointed via AAAA validates as failed.Proposed solution (layered)
Layer 1, automatic detection (fixes the reported bug, items 1 and most of 3):
Gather the full set of IPs the target server is reachable at (the stored address, the globally scoped interface addresses, and the detected public egress IP) and consider the domain valid when it resolves to any of them. For a remote server this is collected over SSH, for the Dokploy host it is collected locally. Any failure to reach the server falls back to the currently stored address, so behavior never regresses.
Layer 2, per domain validation mode (covers items 2, 4, 6 and the rest of 3):
Add a small per domain control that generalizes the existing CDN special case into an explicit, user driven setting. Three modes:
This lives on the domain (proxy setups are per route) and would need a new column on the domain plus the matching UI and router wiring.
Notes
Layer 1 is a self contained bug fix. Layer 2 is a feature (schema, UI, and router changes) and, per CONTRIBUTING.md, is the kind of change worth agreeing on before implementation. Happy to open a focused PR for Layer 1 and follow with Layer 2 once the approach is confirmed.
Will you send a PR to fix it?
Yes