Docker Swarm Deployment#
Dokploy manages Docker Swarm services for both applications and databases via a unified code path that handles local and remote clusters transparently. The two primary concerns are:
- Update ordering — whether the old task stops before or after the new task starts, which directly impacts volume lock safety for stateful workloads.
- Remote cluster management — SSH-based Dockerode connections that let the same deployment code run against a remote Swarm manager without a separate code path.
Key Entry Points#
| File | Role |
|---|---|
packages/server/src/utils/builders/index.ts | mechanizeDockerContainer — creates or updates Swarm services |
packages/server/src/utils/docker/utils.ts | generateConfigContainer — assembles full Swarm service config including UpdateConfig and RollbackConfig |
packages/server/src/utils/servers/remote-docker.ts | getRemoteDocker — returns local or SSH-tunneled Dockerode instance |
packages/server/src/services/docker.ts | getSwarmNodes, getNodeInfo, getNodeApplications — cluster inspection via shell |
packages/server/src/db/schema/shared.ts | UpdateConfigSwarm interface — shared type for both update and rollback config |
packages/server/src/db/schema/application.ts | updateConfigSwarm / rollbackConfigSwarm JSON columns on the applications table |
Update Ordering: stop-first vs start-first#
Docker Swarm's UpdateConfig.Order and RollbackConfig.Order each accept "start-first" or "stop-first":
start-first— the replacement task is started before the old task is stopped. Provides zero-downtime rolling updates for stateless services.stop-first— the old task is fully stopped before the new task starts. Required for services with named volumes, because two tasks cannot hold an exclusive volume lock simultaneously.
Defaults#
generateConfigContainer applies these defaults when no explicit config is stored:
- UpdateConfig:
{ Parallelism: 1, Order: "start-first", FailureAction: "rollback" } - RollbackConfig:
{ Parallelism: 1, Order: "start-first" }
Volume lock warning: Stateful services (databases, anything with a named volume mount) should override
Orderto"stop-first"via the UI. Usingstart-firstwith an exclusive-access volume will cause the new task to fail on mount while the old task still holds the lock.
Placement constraint for mounted services#
If an application has mounts but no explicit placementSwarm, generateConfigContainer automatically adds Constraints: ["node.role==manager"] . This pins stateful services to the manager node and prevents cross-node volume access issues in multi-node clusters.
Configuration schema#
The UpdateConfigSwarm interface defines the full shape:
Parallelism: number
Delay?: number // nanoseconds between updates
FailureAction?: string // "pause" | "continue" | "rollback"
Monitor?: number // nanoseconds to monitor after each task update
MaxFailureRatio?: number
Order: string // "stop-first" | "start-first"
Both updateConfigSwarm and rollbackConfigSwarm are persisted as JSON columns on the applications table , typed as UpdateConfigSwarm. The UI exposes these via update-config-form.tsx and rollback-config-form.tsx, each with an Order select field.
SSH-Based Remote Cluster Management#
getRemoteDocker#
getRemoteDocker(serverId?) is the single function that unifies local and remote Dockerode access:
- If
serverIdisnull/undefinedor the server has nosshKeyId→ returns the shared localdockersingleton. - Otherwise → creates a new
Dockerodeinstance withprotocol: "ssh"using the server'sipAddress,port,username, andsshKey.privateKeyfrom the database.
mechanizeDockerContainer calls getRemoteDocker(application.serverId), so the exact same service create/update logic runs whether targeting the local cluster or a remote one over SSH.
Shell-based cluster inspection#
For read-only cluster operations, Dokploy uses execAsyncRemote(serverId, command) (shell over SSH) rather than the Dockerode API :
getSwarmNodes— runsdocker node ls --format '{{json .}}'getNodeInfo— runsdocker node inspect <nodeId>getNodeApplications— runsdocker service ls --format '{{json .}}'
These are exposed to the frontend via the swarmRouter tRPC endpoints (getNodes, getNodeInfo, getNodeApps, getAppInfos, getContainerStats).
Two SSH transports#
| Transport | Used for | Implementation |
|---|---|---|
| Dockerode SSH | Service CRUD (create, update, inspect) | getRemoteDocker → Dockerode({ protocol: "ssh" }) |
execAsyncRemote | Shell commands (node ls, service ls) | ssh2 Client, one connection per call |
Both resolve the server record via findServerById and authenticate with the stored private key.
Service Create / Update Pattern#
mechanizeDockerContainer follows a try-update-then-create pattern:
- Attempt update: calls
docker.getService(appName), inspects the currentVersion.Index, then callsservice.update({ version, ...settings, ForceUpdate: current + 1 }). IncrementingForceUpdatetriggers a rolling restart even when the spec is otherwise unchanged. - Fall back to create: if the service does not exist (or any error occurs), calls
docker.createService(settings).
The settings object passed to both paths is assembled from:
generateConfigContainer(application)→UpdateConfig,RollbackConfig,Placement,Mode,RestartPolicy,HealthCheck,Networks, etc.calculateResources(...)→ CPU/memory limits and reservationsprepareEnvironmentVariables(...)→ merged env vars from project, environment, and application layersgetRemoteDocker(application.serverId)→ local or SSH Dockerode instance