Skip to content

feat(duplicate): choose the target server when duplicating services - #5451

Open
iam4x wants to merge 9 commits into
Dokploy:canaryfrom
iam4x:feat/duplicate-target-server
Open

iam4x wants to merge 9 commits into
Dokploy:canaryfrom
iam4x:feat/duplicate-target-server

Conversation

@iam4x

@iam4x iam4x commented Sep 13, 2026

Copy link
Copy Markdown

What is this PR about?

The Duplicate Services dialog gets a Target Server select. Today every duplicated application, compose stack and database keeps its source serverId, so duplication cannot be used to stage a copy on another host. This adds the missing piece asked for in #5170: a copy that lands on a different remote server, or back on the Dokploy host, while the source keeps running. #5396 (transfer) moves a service and its data; this copies configuration only, the same way duplication always has.

Duplicate dialog with the Target Server select set to remote-b

Behavior

  • The select defaults to Keep current server. When every selected service lives on the same server, the option names it and that server is not listed again. A mixed selection lists every target.
  • The list contains the Dokploy host (hidden in cloud mode and when remoteServersOnly is on) and the organization's servers that have an SSH key, the same source add-application uses.
  • Picking a different server sets serverId on every copy. Networks that are scoped to another server are dropped from networkIds (applications, databases) and from each compose serviceNetworks entry, through the same resolveNetworkIds rule the transfer feature uses. The dialog says so before you confirm.
  • Mounts, domains, ports, redirects, security, preview deployments and backups are copied exactly as before. Volumes and their data are not copied; use Transfer when you need the data to move.

API

project.duplicate accepts an optional targetServer:

targetServer?:
	| { kind: "keep" }                         // default, current behavior
	| { kind: "dokploy" }                      // place copies on the Dokploy host
	| { kind: "remote"; serverId: string }     // place copies on that server

The target is validated before any project or service row is written: dokploy is rejected in cloud mode or with remoteServersOnly; remote requires a server the session can access (owner, or a member with that serverId in accessedServers) that is active and of type deploy. Existing callers that omit targetServer are unaffected.

Changes

  • packages/server/src/services/duplicate.ts (new). assertDuplicateTargetServer, duplicateServerOverride, and duplicateService. The per-service duplication switch moved here from the router; each arm builds its payload through one helper, so the target-server override is applied in one place instead of eight. project.ts shrinks by ~390 lines.
  • packages/server/src/db/schema/duplicate.ts (new). apiDuplicateTargetServer zod union and its type.
  • packages/server/src/services/network.ts. resolveNetworkIds moved here from transfer.ts so both features share it. No behavior change.
  • apps/dokploy/server/api/routers/project.ts. duplicate takes targetServer, validates it up front, calls duplicateService, rethrows TRPCError instead of flattening every failure to BAD_REQUEST, and records the target in the audit metadata. The hand-written service type enum is now z.enum(serviceType.enumValues).
  • apps/dokploy/components/dashboard/project/duplicate-project.tsx. The select and the network warning.
  • apps/dokploy/__test__/services/duplicate-target-server.test.ts (new). 15 cases covering the three target kinds, cloud and remoteServersOnly rejection, inaccessible and inactive servers, and network filtering for networkIds and compose serviceNetworks.

Tested

Local instance on canary with two seeded remote servers, an application on each, a Postgres on the Dokploy host, a compose stack with serviceNetworks, and one server-scoped network per remote. Driven through the real dialog with Playwright and read back from Postgres:

  • Keep: every copy stays on its source server with its networks.
  • remote-b: application, compose and Postgres copies land on remote-b; the network scoped to remote-a is dropped from the application copy and from the compose serviceNetworks entry, the remote-b network is kept, detachDokployNetwork is preserved, the source rows are untouched.
  • Unknown serverId returns UNAUTHORIZED before any project row is created. An unknown kind is rejected by zod. Omitting targetServer behaves as before.
  • pnpm typecheck, pnpm server:build, biome check on the changed files clean. pnpm test: 1044 passed; the 4 failures in application.real.test.ts are pre-existing on canary on this machine (nixpacks/railpack not installed) and unrelated.

Notes for review

  • project.duplicate now rethrows TRPCError as is. Callers that keyed on every failure being BAD_REQUEST will see UNAUTHORIZED, NOT_FOUND or CONFLICT where those apply.
  • docs.dokploy.com has no page for duplication yet. I can add one to the docs repo once the API shape is settled.

Checklist

Before submitting this PR, please make sure that:

Issues related (if applicable)

Related to #5170 (closed with #5396, which its description calls a different operation).

Screenshots (if applicable)

See above.

RetriggerConfidence Score: 3/5

The PR is not safe to merge until package subpaths resolve to emitted ESM files and the local Dokploy target remains selectable without remote-server results.

Summary

  • Adds a discriminated target-server API schema and target authorization checks.
  • Adds dashboard target selection and detached-network warnings.
  • Reuses transfer network resolution and adds focused duplication tests.
  • The package export remapping and an overly broad UI rendering condition introduce two blocking failures.

Reviews (1) · Last reviewed commit: "fix(duplicate): hide the shared source s..."

iam4x and others added 3 commits September 13, 2026 05:22
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@iam4x
iam4x requested a review from Siumauricio as a code owner September 13, 2026 06:01
Comment thread packages/server/package.json Outdated
Comment on lines +15 to +18
"./*": {
"import": "./dist/*",
"require": "./dist/*.cjs"
},

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 Wildcard breaks directory exports

The wildcard maps imports such as @dokploy/server/constants to ./dist/constants, but the build emits dist/constants/index.js. Node's ESM resolver does not add the filename or resolve the directory index for package export targets. Because the Dokploy server bundle leaves package imports external, normal startup can fail with ERR_MODULE_NOT_FOUND. Please export directory-backed subpaths explicitly with their emitted filenames.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, but not part of this change. That diff was the output of pnpm server:build (switch:prod rewrites package.json) swept into a commit by mistake. Removed from the branch; the PR no longer touches packages/server/package.json.

</>
)}

{servers && servers.length > 0 && (

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 Local target is hidden

The entire target selector, including the valid local Dokploy target, is hidden when withSSHKey returns no remote servers. In a self-hosted installation, a user duplicating a remote service can therefore be forced to keep its current server even though the backend permits duplication to Dokploy. The local option should be rendered independently of the remote-server list.

Knowledge Base Used: Dashboard and client state

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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 7e86ea9. The selector now renders whenever there is at least one target other than "keep": the Dokploy host when it is allowed and the source is remote, or any listed server that is not the shared source. With no listed remote servers a remote source still gets Keep current server and Dokploy; a local source with no remotes gets no selector, since keep would be the only option.

@iam4x
iam4x force-pushed the feat/duplicate-target-server branch from 00951cc to 7e86ea9 Compare September 13, 2026 06:06
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.

1 participant