Skip to content

feat(domain): block assigning conflicting domain and path across services - #5644

Closed
taieddie wants to merge 4 commits into
Dokploy:canaryfrom
taieddie:feat/block-duplicate-domain-conflict
Closed

taieddie wants to merge 4 commits into
Dokploy:canaryfrom
taieddie:feat/block-duplicate-domain-conflict

Conversation

@taieddie

@taieddie taieddie commented Oct 10, 2026 •

Copy link
Copy Markdown

Note to Maintainers: This PR supersedes #5643, which I originally opened from my secondary account (@tek123168). I closed #5643 to consolidate my contributions under this primary account. This submission includes the hardened validation logic and full test suite covering path routing, case insensitivity, multi-server isolation, and Greptile review recommendations.

Summary

Fixes #5616

In Dokploy, assigning an existing domain and path to another application or compose service caused conflicting Traefik router rules on the same server, resulting in unpredictable routing and intermittent 500 errors.

Changes

  • In packages/server/src/services/domain.ts:
    • Added findConflictingDomain checking matching lower(host), normalized path prefix (normalizeDomainPath), and physical serverId.
    • Supports path-based routing: distinct paths on the same host (such as / and /api) are permitted and do not collide.
    • Enforces case-insensitive host matching per RFC 4343.
    • Scopes collision detection per physical server (serverId) across applications, compose services, and preview deployments.
    • Runs collision checks inside db.transaction to protect against concurrent duplicate creation.
    • updateDomainById avoids false-positive collisions when updating existing domain settings (ports, certificates).
  • In apps/dokploy/server/api/routers/domain.ts:
    • Preserved TRPCError in domainRouter.create catch block so the CONFLICT status code propagates cleanly to API clients.
  • In apps/dokploy/__test__/domains/domain-conflict-validation.test.ts:
    • Added comprehensive unit test coverage for path routing, case insensitivity, multi-server isolation, self-updates, and successful creation when no matching host exists.

RetriggerConfidence Score: 2/5

Fix concurrent duplicate saves, failed project copies, and blocked settings-only edits before merging.

Summary

Adds same-server checks for matching hosts and normalized paths across applications, Compose services, and preview deployments. Creates now preserve CONFLICT errors.

  • Dokploy blocks duplicate domain routes on the same server.
  • Domain lookups match host names without regard to case.

Intent recorded: taieddie explicitly described normalized-path matching, including trailing-slash normalization, as intended.

Reviews (1) · Last reviewed commit: "feat(domain): block assigning conflictin..." · Reviewed by Greptile

…ices

Fixes Dokploy#5616

- Validate collisions matching lower(host), path prefix, and serverId
- Support path-based routing so distinct paths on the same host do not conflict
- Enforce case-insensitive host matching per RFC 4343
- Run validation within database transaction to prevent concurrent duplicate inserts
- Preserve TRPCError instances in domain.create router so CONFLICT code reaches API callers
- Add unit test coverage for path routing, case insensitivity, multi-server isolation, and successful creation
Comment on lines 154 to +159
const result = await db.transaction(async (tx) => {
const conflictingDomain = await findConflictingDomain({
host: cleanHost,
path: input.path,
serverId,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Concurrent saves still collide

Two requests saving the same host, path, and server can both pass this check before either commits. findConflictingDomain reads through the global db, while the insert uses tx. No lock or unique constraint stops both inserts from succeeding. Updates have the same check-then-save race, so conflicting Traefik routes can still be created.

Use a shared lock or database-enforced rule for creates and updates. Moving the read into tx alone will not prevent duplicates.

Comment on lines +161 to +168
if (conflictingDomain) {
const targetPath = normalizeDomainPath(input.path);
const pathDisplay =
targetPath === "/" ? "" : ` with path '${targetPath}'`;
throw new TRPCError({
code: "CONFLICT",
message: `Domain '${cleanHost}'${pathDisplay} is already in use on this server`,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Project copies stop halfway

This rejection breaks project.duplicate for services with domains. That flow first creates the copied application or Compose service, then calls createDomain with the source host and path on the same server. The source domain now triggers CONFLICT, so duplication fails after earlier writes have committed. The copied project or services remain partly created.

Update duplication to omit or replace conflicting domains, or reject before creating the copied resources.

Comment thread packages/server/src/services/domain.ts Outdated
) => {
const currentDomain = await findDomainById(domainId);

if (domainData.host !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unchanged settings cannot be saved

A domain with an existing duplicate can no longer receive settings-only edits. The dashboard always sends host, even when the user changes only a port or certificate. This branch checks for conflicts whenever host is supplied. Excluding the current row still finds the other old duplicate and rejects the save, even though the route has not changed.

Compare the old and proposed host/path before checking, so unchanged routes can still have their settings edited.

Comment on lines +144 to +152
host: "app.example.com",
path: "/",
application: { serverId: "server-1" },
},
]);

await expect(
createDomain({
host: "APP.EXAMPLE.COM",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Case matching is not tested

This test does not check case-insensitive host matching. findMany returns the same canned row regardless of its query, so replacing lower(host) with a case-sensitive comparison would still pass. Add a database-backed test, or assert the generated query, so this test catches that regression.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@taieddie

taieddie commented Oct 11, 2026 •

Copy link
Copy Markdown
Author

Pushed 7671de7 addressing the review comments:

  • Transaction isolation: Moved findConflictingDomain and domain writes into tx to avoid check-then-save races.
  • Settings-only edits: Conflict checks in updateDomainById now only trigger when the route changes (isHostChanged || isPathChanged), so updating ports or SSL certs won't be blocked.
  • Project duplication: Conflicting domains during duplicate runs are now caught and skipped instead of aborting the loop and leaving orphaned resources.
  • Case-insensitive matching: Added lowercase normalization and test coverage for settings updates and host casing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Block pointing the same domain to multiple services

2 participants