Skip to content

Domain validation reports a false negative when the server has multiple IPs #4658

@pparage

Description

@pparage

To Reproduce

  1. 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.
  2. Deploy an application or Docker Compose service to that server and add a domain to it.
  3. Point the domain's DNS A record to the server's public IP (the address visitors actually reach).
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. IPv6 or AAAA only: validation currently resolves A records only (dns.resolve4), so a domain pointed via AAAA validates as failed.
  5. 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.
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions