feat: add service transfer migration workflow#3713
feat: add service transfer migration workflow#3713horsley wants to merge 3 commits intoDokploy:canaryfrom
Conversation
| 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`, |
There was a problem hiding this comment.
double-escaped encodedFileList in shell command
| 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`, | |
| )}`; |
| // 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); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
preflight volume/directory creation happens after service directory sync - volumes/directories should be created before any syncing starts
| 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 | ||
| ' | ||
| `; |
There was a problem hiding this comment.
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!
| }); | ||
|
|
||
| wss.on("connection", async (ws, req) => { | ||
| const { user, session } = await validateRequest(req); | ||
|
|
There was a problem hiding this comment.
missing auth validation on WebSocket upgrade - authentication only checked after connection established, allowing unauthenticated connections to temporarily consume resources
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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
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:
canarybranch.Issues related (if applicable)
#3689
Screenshots (if applicable)
Choose target

Scan Finished

After transfer

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:
Issues found:
Confidence Score: 3/5
packages/server/src/utils/transfer/sync.ts(shell escaping bug) andpackages/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!