You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When users need to move a service from one remote server to another (e.g., hardware migration, load balancing, server decommission), the current options are:
For regular users: Manually create a new service on the target server, re-enter all configurations (environment variables, domains, mounts, etc.), copy data files manually, then delete the old service. This is tedious, error-prone, and risks missing configurations or data.
For advanced users: SSH into both servers, manually rsync or tar the application directory and Docker volumes, then directly update serverId in the Dokploy database. This requires deep knowledge of Dokploy internals and is risky for database services whose volumes contain critical data.
Neither option provides any safety guarantees — there is no conflict detection, no progress feedback, and no rollback safety.
Describe the solution you'd like
A two-phase transfer feature that safely migrates services between remote servers:
Scan phase: Pre-flight analysis of source and target servers — detects files, sizes, and conflicts
Execute phase: Syncs files/volumes with real-time progress streaming, then atomically updates the serverId in the database
Important
Source server data is read-only during transfer. Files are copied, never moved or deleted. The database serverId is updated only after a fully successful sync.
Describe alternatives you've considered
nope
Additional context
Scope
This feature applies to multi-server mode — where Dokploy manages multiple remote servers and users assign services to individual servers.
Note
Not in scope: Docker Swarm mode. Swarm has its own built-in mechanisms for service migration (node drain / service constraints). This feature targets the non-Swarm multi-server setup specifically.
sequenceDiagram
participant User
participant UI as Frontend
participant WS as WebSocket (/drawer-logs)
participant Router as tRPC Router
participant Transfer as transfer.ts
participant Source as Source Server
participant Target as Target Server
User->>UI: Select target server
UI->>Router: transferScan(serviceId, targetServerId)
Router->>Transfer: scanServiceForTransfer()
Transfer->>Source: Scan files, volumes, Traefik config
Transfer->>Target: Scan existing files for conflicts
Transfer-->>Router: ScanResult (sizes, conflicts, hashes)
Router-->>UI: Display scan results
User->>UI: Review conflicts, set decisions
User->>UI: Confirm transfer
UI->>WS: transferWithLogs subscription
WS->>Router: subscription handler
Router->>Transfer: executeTransfer(opts, decisions, onProgress)
loop For each mount/volume
Transfer->>Source: Read files (rsync/tar)
Transfer->>Target: Write files
Transfer->>Router: onProgress({phase, file, %})
Router->>WS: emit.next(JSON.stringify(progress))
WS->>UI: Real-time progress update
end
Transfer-->>Router: {success: true}
Router->>Router: UPDATE serverId in DB
Router->>WS: emit.next("Transfer completed successfully!")
WS->>UI: Show completion
Loading
What Gets Transferred
Per Service Type
Service Type
App/Compose Dir
Traefik Config
Auto Data Volume
User Mounts
Application
✅
✅
—
✅
Compose
✅
✅
—
✅
PostgreSQL
—
—
✅ {appName}-data
✅
MySQL
—
—
✅ {appName}-data
✅
MariaDB
—
—
✅ {appName}-data
✅
MongoDB
—
—
✅ {appName}-data
✅
Redis
—
—
✅ {appName}-data
✅
NOT Transferred
Item
Reason
TLS/SSL certificates
Each server manages its own Let's Encrypt certs
Docker images
Pulled from registry during deploy
Running containers
Only files/volumes are synced
Docker networks
Auto-created during deploy
Container logs
Ephemeral
Service Downtime & Container State Management
Transferring a service involves copying files and Docker volumes between servers. To ensure data consistency, the service's containers should be stopped before transfer.
Warning
Downtime is expected during transfer. Users should plan for service unavailability from the moment the service is stopped until it is deployed on the target server.
Pre-Transfer
Step
Action
Reason
1
Stop the service on source server
Prevents data writes during file/volume copy, ensures consistency
2
Verify service is stopped
UI should confirm containers are not running
Caution
For database services (PostgreSQL, MySQL, MariaDB, MongoDB, Redis): Writing to the database during volume transfer may result in corrupted or inconsistent data on the target. Always stop the database before transfer.
Post-Transfer (Success)
Step
Action
Reason
1
serverId is updated in DB
Service is now associated with the target server
2
User must deploy/start the service on the target server
Containers are not automatically started after transfer
3
Verify service is running correctly
Check logs, connectivity, data integrity
4
(Optional) Clean up source server
Source files remain untouched; user can remove them when ready
Post-Transfer (Failure or Cancellation)
Step
Action
Reason
1
serverId is NOT updated
Service remains associated with the source server
2
User should restart the service on the source server
Restore availability as quickly as possible
3
Investigate failure (check logs)
Common causes: SSH connectivity, disk space, permissions
4
Retry transfer when ready
Source data is unchanged, safe to retry
Important
The UI should clearly indicate: (1) the service will experience downtime, (2) after success, a deploy is needed on the target, and (3) after failure, the user should restart the service on the source server.
Downtime Timeline
gantt
title Service Availability During Transfer
dateFormat X
axisFormat %s
section Source Server
Running :done, 0, 1
Stopped for transfer :crit, 1, 3
Stopped (data still exists) :active, 3, 4
section Target Server
Empty :done, 0, 2
Receiving data :active, 2, 3
User deploys service :crit, 3, 4
Running :done, 4, 5
section Downtime
Service unavailable :crit, 1, 4
Loading
Sync Methods
Scenario
Method
Details
Local → Remote
rsync -az over SSH
Efficient delta sync
Remote → Local
rsync -az from SSH
Efficient delta sync
Remote → Remote
tar pipeline via Dokploy server
ssh source "tar czf -" | ssh target "tar xzf -"
Docker volume
Docker container + tar via SSH
Mounts volume read-only, streams via tar
Conflict Detection & Resolution
During the scan phase, files on both source and target are compared:
Conflict Status
Meaning
missing_target
File only exists on source (new file)
newer_source
Source file is newer than target
newer_target
Target file is newer than source
conflict
Both modified, different content
match
Identical files, no action needed
For conflicts, the UI displays:
File path
Source vs target modification time
Source vs target file hash (MD5)
File sizes
Users choose per-file: Overwrite (replace target) or Skip (keep target).
File Changes
New Files
File
Purpose
packages/server/src/services/transfer.ts
Core orchestration — scanServiceForTransfer() and executeTransfer()
packages/server/src/utils/transfer/scanner.ts
File scanning for volumes (docker run) and bind mounts (find)
Runs executeTransfer() with real-time progress via observable<string>, updates serverId on success
transfer (enhanced)
Mutation
Same as above but synchronous (no streaming), kept as fallback
Note
The transfer mutation already existed in the codebase (it only updated serverId). We enhanced it to call executeTransfer() first, ensuring data is synced before the DB update.
Frontend
File
Change
components/dashboard/shared/transfer-service.tsx
Complete rewrite — multi-step flow with scan, conflict review, and real-time progress via tRPC subscription
Server Infrastructure
File
Change
packages/server/src/index.ts
Added transfer service export
apps/dokploy/server/server.ts
Registered setupDataTransferWebSocketServer
Pre-existing (Unchanged by this PR)
File
Status
db/schema/application.ts → apiTransferApplication
Already existed
db/schema/compose.ts → apiTransferCompose
Already existed
db/schema/postgres.ts → apiTransferPostgres
Already existed
db/schema/mysql.ts → apiTransferMySql
Already existed
db/schema/mariadb.ts → apiTransferMariaDB
Already existed
db/schema/mongo.ts → apiTransferMongo
Already existed
db/schema/redis.ts → apiTransferRedis
Already existed
Key Implementation Details
scanServiceForTransfer(opts) → TransferScanResult
Application/Compose directory: Scans source and target with scanBindMount(), runs compareFileLists() to detect diffs
Traefik config: Reads {appName}.yml from both servers using existing readConfig/readRemoteConfig utilities
All DB mounts: Queries findMountsByApplicationId(serviceId, serviceType) to discover volumes and bind mounts, scans each on both sides
Conflict hashing: For conflicting files, computes MD5 hash on both sides (even inside Docker volumes) via computeFileHash()
Returns total byte count, file lists, and per-file conflict details
Select Server → Scan → Review Conflicts → Confirm → Transfer (Live Logs) → Done
Each service type has its own wrapper component (e.g., ApplicationTransfer, PostgresTransfer) that calls the type-specific hooks at the top level, sharing common logic via TransferInner.
Safety Guarantees
Guarantee
How
Source is read-only
All sync operations copy data; never delete/modify source
Atomic DB update
serverId updated only after executeTransfer() returns success: true
Failure is safe
On error, source remains unchanged; target may have partial files that can be cleaned up; retry is always possible
User control
Per-file conflict resolution (overwrite/skip)
Auth enforced
Organization membership + checkServiceAccess with "delete" permission level
UI User Flow
Navigate to Service → Settings → Transfer Service
Select target server from dropdown (only remote servers are shown; Swarm nodes are excluded)
Click Scan for Transfer — shows loading while scanning both servers
Review scan results:
Total transfer size
Volume count
Conflict table with source/target mtime, hash, and overwrite/skip toggles
Click Transfer → confirmation dialog with:
Size estimate
⚠️Downtime warning: "Your service will be unavailable during transfer"
Watch real-time progress: progress bar, file count, bytes transferred, current file, scrollable log area
On success:
✅ "Transfer completed" message
Prompt: "Deploy the service on the target server to start it"
On failure:
❌ Error message with details
Prompt: "Restart the service on the source server to restore availability"
Discussion Points for Maintainer
data-transfer.ts WebSocket server: We also implemented a raw WebSocket server at /data-transfer that provides more granular control (pause/resume/cancel, separate scan/compare/sync phases). Currently unused by the production frontend — should we keep it for advanced use cases, or remove it to reduce scope?
transfer mutation vs transferWithLogs subscription: We kept both — the mutation as a synchronous fallback (e.g., for API/CLI usage) and the subscription for the UI. Is this acceptable, or should we consolidate?
Auto-stop before transfer: Should the system automatically stop the service before starting the transfer? This reduces user error but adds complexity. Current approach: user is responsible for stopping with a clear UI warning. Alternative: auto-stop and auto-restart on failure.
Auto-deploy after transfer: Should the system automatically deploy/start the service on the target after successful transfer? Current approach: user manually deploys. Alternative: auto-deploy, with rollback to source on failure.
Rollback: Currently, failed transfers leave partial data on the target but don't touch the source. Should we add explicit cleanup of target on failure?
Scope of existing apiTransfer* schemas: The Zod schemas (apiTransferApplication, etc.) already existed in the codebase. Our changes only extend them with an optional decisions field for the enhanced endpoints. No schema file modifications needed.
Limitations
Service downtime is required — service must be stopped on source before transfer; user deploys on target after
No automatic rollback — source is untouched, but target may have partial files on failure
Network dependent — large volumes take time on slow connections
Same Dokploy instance — both servers must be managed by the same Dokploy installation
No incremental/resumable transfers — if interrupted, the entire sync restarts
Multi-server mode only — does not apply to Docker Swarm mode (use Swarm drain instead)
What problem will this feature address?
When users need to move a service from one remote server to another (e.g., hardware migration, load balancing, server decommission), the current options are:
rsyncortarthe application directory and Docker volumes, then directly updateserverIdin the Dokploy database. This requires deep knowledge of Dokploy internals and is risky for database services whose volumes contain critical data.Neither option provides any safety guarantees — there is no conflict detection, no progress feedback, and no rollback safety.
Describe the solution you'd like
A two-phase transfer feature that safely migrates services between remote servers:
serverIdin the databaseImportant
Source server data is read-only during transfer. Files are copied, never moved or deleted. The database
serverIdis updated only after a fully successful sync.Describe alternatives you've considered
nope
Additional context
Scope
This feature applies to multi-server mode — where Dokploy manages multiple remote servers and users assign services to individual servers.
Note
Not in scope: Docker Swarm mode. Swarm has its own built-in mechanisms for service migration (node drain / service constraints). This feature targets the non-Swarm multi-server setup specifically.
Architecture Overview
flowchart TD subgraph Frontend["Frontend (transfer-service.tsx)"] UI[Multi-step UI] end subgraph Routers["tRPC Routers (x7)"] TS[transferScan — mutation] TL[transferWithLogs — subscription] T[transfer — mutation] end subgraph Service["Transfer Service"] SCAN[scanServiceForTransfer] EXEC[executeTransfer] end subgraph Utils["Transfer Utilities"] SCANNER[scanner.ts] SYNC[sync.ts] PREFLIGHT[preflight.ts] TYPES[types.ts] end UI -->|1. Scan| TS --> SCAN SCAN --> SCANNER UI -->|2. Execute| TL --> EXEC EXEC --> SYNC EXEC --> PREFLIGHT UI -.->|Legacy fallback| T --> EXECData Flow
sequenceDiagram participant User participant UI as Frontend participant WS as WebSocket (/drawer-logs) participant Router as tRPC Router participant Transfer as transfer.ts participant Source as Source Server participant Target as Target Server User->>UI: Select target server UI->>Router: transferScan(serviceId, targetServerId) Router->>Transfer: scanServiceForTransfer() Transfer->>Source: Scan files, volumes, Traefik config Transfer->>Target: Scan existing files for conflicts Transfer-->>Router: ScanResult (sizes, conflicts, hashes) Router-->>UI: Display scan results User->>UI: Review conflicts, set decisions User->>UI: Confirm transfer UI->>WS: transferWithLogs subscription WS->>Router: subscription handler Router->>Transfer: executeTransfer(opts, decisions, onProgress) loop For each mount/volume Transfer->>Source: Read files (rsync/tar) Transfer->>Target: Write files Transfer->>Router: onProgress({phase, file, %}) Router->>WS: emit.next(JSON.stringify(progress)) WS->>UI: Real-time progress update end Transfer-->>Router: {success: true} Router->>Router: UPDATE serverId in DB Router->>WS: emit.next("Transfer completed successfully!") WS->>UI: Show completionWhat Gets Transferred
Per Service Type
{appName}-data{appName}-data{appName}-data{appName}-data{appName}-dataNOT Transferred
Service Downtime & Container State Management
Transferring a service involves copying files and Docker volumes between servers. To ensure data consistency, the service's containers should be stopped before transfer.
Warning
Downtime is expected during transfer. Users should plan for service unavailability from the moment the service is stopped until it is deployed on the target server.
Pre-Transfer
Caution
For database services (PostgreSQL, MySQL, MariaDB, MongoDB, Redis): Writing to the database during volume transfer may result in corrupted or inconsistent data on the target. Always stop the database before transfer.
Post-Transfer (Success)
serverIdis updated in DBPost-Transfer (Failure or Cancellation)
serverIdis NOT updatedImportant
The UI should clearly indicate: (1) the service will experience downtime, (2) after success, a deploy is needed on the target, and (3) after failure, the user should restart the service on the source server.
Downtime Timeline
gantt title Service Availability During Transfer dateFormat X axisFormat %s section Source Server Running :done, 0, 1 Stopped for transfer :crit, 1, 3 Stopped (data still exists) :active, 3, 4 section Target Server Empty :done, 0, 2 Receiving data :active, 2, 3 User deploys service :crit, 3, 4 Running :done, 4, 5 section Downtime Service unavailable :crit, 1, 4Sync Methods
rsync -azover SSHrsync -azfrom SSHtarpipeline via Dokploy serverssh source "tar czf -" | ssh target "tar xzf -"tarvia SSHConflict Detection & Resolution
During the scan phase, files on both source and target are compared:
missing_targetnewer_sourcenewer_targetconflictmatchFor conflicts, the UI displays:
Users choose per-file: Overwrite (replace target) or Skip (keep target).
File Changes
New Files
packages/server/src/services/transfer.tsscanServiceForTransfer()andexecuteTransfer()packages/server/src/utils/transfer/scanner.tsdocker run) and bind mounts (find)packages/server/src/utils/transfer/sync.tspackages/server/src/utils/transfer/preflight.tspackages/server/src/utils/transfer/types.tsFileInfo,MountTransferConfig,TransferStatus, etc.)packages/server/src/utils/transfer/index.tsapps/dokploy/server/wss/data-transfer.tsModified Files
Router Endpoints (7 files)
Each router received two new endpoints and one enhanced endpoint:
transferScantransferWithLogstransferrouters/application.tsrouters/compose.tsrouters/postgres.tsrouters/mysql.tsrouters/mariadb.tsrouters/mongo.tsrouters/redis.tsEndpoint details:
transferScanscanServiceForTransfer(), returns sizes/conflictstransferWithLogsexecuteTransfer()with real-time progress viaobservable<string>, updatesserverIdon successtransfer(enhanced)Note
The
transfermutation already existed in the codebase (it only updatedserverId). We enhanced it to callexecuteTransfer()first, ensuring data is synced before the DB update.Frontend
components/dashboard/shared/transfer-service.tsxServer Infrastructure
packages/server/src/index.tstransferservice exportapps/dokploy/server/server.tssetupDataTransferWebSocketServerPre-existing (Unchanged by this PR)
db/schema/application.ts→apiTransferApplicationdb/schema/compose.ts→apiTransferComposedb/schema/postgres.ts→apiTransferPostgresdb/schema/mysql.ts→apiTransferMySqldb/schema/mariadb.ts→apiTransferMariaDBdb/schema/mongo.ts→apiTransferMongodb/schema/redis.ts→apiTransferRedisKey Implementation Details
scanServiceForTransfer(opts)→TransferScanResultscanBindMount(), runscompareFileLists()to detect diffs{appName}.ymlfrom both servers using existingreadConfig/readRemoteConfigutilitiesfindMountsByApplicationId(serviceId, serviceType)to discover volumes and bind mounts, scans each on both sidescomputeFileHash()executeTransfer(opts, decisions, onProgress)→{success, errors}rsync(local↔remote) ortarpipeline (remote↔remote)docker volume create) and directories (mkdir -p)syncMount()from transfer utilities, respecting user decisions for conflicts{phase, currentFile, processedFiles, totalFiles, transferredBytes, totalBytes, percentage}transferWithLogs(tRPC Subscription)Follows the exact pattern of the existing
deployWithLogsacross every router:Frontend Transfer Flow
Each service type has its own wrapper component (e.g.,
ApplicationTransfer,PostgresTransfer) that calls the type-specific hooks at the top level, sharing common logic viaTransferInner.Safety Guarantees
serverIdupdated only afterexecuteTransfer()returnssuccess: truecheckServiceAccesswith"delete"permission levelUI User Flow
Discussion Points for Maintainer
data-transfer.tsWebSocket server: We also implemented a raw WebSocket server at/data-transferthat provides more granular control (pause/resume/cancel, separate scan/compare/sync phases). Currently unused by the production frontend — should we keep it for advanced use cases, or remove it to reduce scope?transfermutation vstransferWithLogssubscription: We kept both — the mutation as a synchronous fallback (e.g., for API/CLI usage) and the subscription for the UI. Is this acceptable, or should we consolidate?Auto-stop before transfer: Should the system automatically stop the service before starting the transfer? This reduces user error but adds complexity. Current approach: user is responsible for stopping with a clear UI warning. Alternative: auto-stop and auto-restart on failure.
Auto-deploy after transfer: Should the system automatically deploy/start the service on the target after successful transfer? Current approach: user manually deploys. Alternative: auto-deploy, with rollback to source on failure.
Rollback: Currently, failed transfers leave partial data on the target but don't touch the source. Should we add explicit cleanup of target on failure?
Scope of existing
apiTransfer*schemas: The Zod schemas (apiTransferApplication, etc.) already existed in the codebase. Our changes only extend them with an optionaldecisionsfield for the enhanced endpoints. No schema file modifications needed.Limitations
Will you send a PR to implement it?
Yes