Skip to content

Comments

feat: add service transfer migration workflow#3713

Open
horsley wants to merge 3 commits intoDokploy:canaryfrom
horsley:transfer-migration
Open

feat: add service transfer migration workflow#3713
horsley wants to merge 3 commits intoDokploy:canaryfrom
horsley:transfer-migration

Conversation

@horsley
Copy link
Contributor

@horsley horsley commented Feb 15, 2026

What is this PR about?

This PR introduces a new service transfer migration workflow for Dokploy’s multi-server mode (non-Swarm), allowing users to move Application, Compose, and database services between servers safely and with much less manual work. It adds a two-phase process (scan + execute) with mount/volume discovery, file conflict detection and per-file overwrite/skip decisions, plus real-time transfer progress logs. The transfer is copy-based (source remains unchanged), and serverId is updated only after a successful sync to keep cutover behavior safe and predictable.

Checklist

Before submitting this PR, please make sure that:

  • You created a dedicated branch based on the canary branch.
  • You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
  • You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.

Issues related (if applicable)

#3689

Screenshots (if applicable)

Choose target
image

Scan Finished
image

After transfer
image

Greptile Summary

This PR adds a comprehensive service transfer migration workflow that enables moving services (applications, databases, compose projects) between remote servers. The implementation includes scanning, file comparison, conflict resolution, and data synchronization via Docker volumes and bind mounts over SSH.

Key changes:

  • Implemented multi-phase transfer workflow (scan → compare → sync) with progress tracking
  • Added WebSocket-based real-time progress updates for transfers
  • Created transfer utilities for volume/bind mount scanning and rsync-based synchronization
  • Added TRPC endpoints and subscriptions for all service types
  • Implemented downtime management that stops source service during transfer and restarts on failure

Issues found:

  • Critical shell escaping bug in batch file sync that will cause command failures
  • Volume/directory creation happens after service directory sync, potentially causing failures
  • WebSocket authentication happens post-connection, allowing brief resource consumption by unauthenticated clients

Confidence Score: 3/5

  • This PR introduces significant new functionality with a few critical bugs that need fixing before merge
  • The shell escaping bug in sync.ts line 214 will cause batch transfers to fail completely. The preflight creation ordering issue could cause sync failures. The WebSocket auth timing is a minor security concern. Once these issues are fixed, the implementation is solid with good test coverage and comprehensive documentation.
  • Pay close attention to packages/server/src/utils/transfer/sync.ts (shell escaping bug) and packages/server/src/services/transfer.ts (preflight ordering)

Last reviewed commit: 41714a5

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

@horsley horsley requested a review from Siumauricio as a code owner February 15, 2026 15:43
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

40 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +207 to +215
const encodedFileList = Buffer.from(
relativePaths.join("\n"),
"utf8",
).toString("base64");

const sourceCommand = `docker run --rm -v ${shellEscape(
`${sourceVolume}:/volume_data:ro`,
)} alpine sh -c ${shellEscape(
`echo ${shellEscape(encodedFileList)} | base64 -d > /tmp/files.txt && tar czf - -C /volume_data -T /tmp/files.txt`,
Copy link
Contributor

Choose a reason for hiding this comment

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

double-escaped encodedFileList in shell command

Suggested change
const encodedFileList = Buffer.from(
relativePaths.join("\n"),
"utf8",
).toString("base64");
const sourceCommand = `docker run --rm -v ${shellEscape(
`${sourceVolume}:/volume_data:ro`,
)} alpine sh -c ${shellEscape(
`echo ${shellEscape(encodedFileList)} | base64 -d > /tmp/files.txt && tar czf - -C /volume_data -T /tmp/files.txt`,
const sourceCommand = `docker run --rm -v ${shellEscape(
`${sourceVolume}:/volume_data:ro`,
)} alpine sh -c ${shellEscape(
`echo ${encodedFileList} | base64 -d > /tmp/files.txt && tar czf - -C /volume_data -T /tmp/files.txt`,
)}`;

Comment on lines +1072 to +1083
// Pre-flight: create volumes/directories on target
if (targetServerId) {
for (const { mount } of mountPlans) {
if (mount.mountType === "volume") {
await createVolumeOnTarget(targetServerId, mount.targetPath, {
labels: getComposeManagedVolumeLabels(mount),
});
} else {
await createDirectoryOnTarget(targetServerId, mount.targetPath);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

preflight volume/directory creation happens after service directory sync - volumes/directories should be created before any syncing starts

Comment on lines +23 to +35
const command = `
docker run --rm -v ${volumeMount} alpine sh -c '
find /volume_data -type f -o -type d | while read f; do
if [ -f "$f" ]; then
STAT=$(stat -c "%s|%Y|%a" "$f" 2>/dev/null || echo "0|0|644")
echo "f|$f|$STAT"
elif [ -d "$f" ]; then
STAT=$(stat -c "0|%Y|%a" "$f" 2>/dev/null || echo "0|0|755")
echo "d|$f|$STAT"
fi
done
'
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

missing --mount source or readonly flag in docker command for volume scanning - best practice to mount volumes as readonly when only reading

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!

Comment on lines 238 to 242
});

wss.on("connection", async (ws, req) => {
const { user, session } = await validateRequest(req);

Copy link
Contributor

Choose a reason for hiding this comment

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

missing auth validation on WebSocket upgrade - authentication only checked after connection established, allowing unauthenticated connections to temporarily consume resources

Comment on lines 500 to 509
const command = `docker run --rm -v ${shellEscape(`${volumeName}:/volume_data:ro`)} alpine sh -c 'du -sb /volume_data 2>/dev/null | cut -f1'`;
try {
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
return Number.parseInt(stdout.trim(), 10) || 0;
} catch {
return 0;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

volume size calculation mounts volume as readonly but command output parsing could fail silently returning 0 - consider validating that volume exists before attempting size calculation

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