Remote Command Execution#
Overview#
Dokploy uses a unified "generate bash string → execute locally or remotely" pattern for all deployment operations. The core principle: builder utilities produce plain bash command strings, which are then passed to execAsync (local) or execAsyncRemote (SSH) depending on whether a serverId is present.
The main execution module lives at packages/server/src/utils/process/execAsync.ts.
Core Execution Functions#
All three functions return { stdout: string; stderr: string } and throw ExecError on non-zero exit codes.
| Function | Transport | Use case |
|---|---|---|
execAsync(command, options?) | child_process.exec (local) | Local server commands |
execAsyncStream(command, onData?, options?) | Local, with streaming | Real-time log streaming |
execAsyncRemote(serverId, command, onData?) | SSH via ssh2 client | Remote server commands |
execAsyncRemote resolves the server record via findServerById, then opens a new SSH connection per invocation . It connects using the server's ipAddress, port, username, and sshKey.privateKey with a 99999ms timeout. If serverId is null, it returns { stdout: "", stderr: "" } immediately. Authentication failures produce a structured, user-friendly error message with hints for fixing SSH key issues .
ExecError#
ExecError extends Error with command, stdout, stderr, exitCode, serverId, and originalError. It also provides a isRemote() helper and getDetailedMessage() method. Callers can distinguish remote vs local failures via error.serverId.
Local vs Remote Dispatch Pattern#
The local/remote decision is always at the call site, keyed on compose.serverId (or application.serverId). Builders never execute commands themselves — they only return strings.
The canonical pattern from services/compose.ts:
let commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (compose.serverId) {
await execAsyncRemote(compose.serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
This pattern repeats for each phase of a deployment (clone → patch → build) . The set -e; prefix is prepended to every command string to ensure the shell exits immediately on any error .
PR #2978 consolidated this by removing a parallel deployRemoteXxx / rebuildRemoteXxx function family, making the serverId branch the single unification point.
Command String Construction#
Builders in packages/server/src/utils/builders/ and utils/providers/ assemble bash strings — they never invoke shell commands directly.
File creation via base64 encoding is the standard way to safely write multi-line content to disk. getCreateComposeFileCommand encodes the compose file content with encodeBase64, then emits:
echo "<base64>" | base64 -d > "<filePath>"
The same technique is used for .env files in getCreateEnvFileCommand.
getBuildComposeCommand assembles the full deploy bash script — logging, env file creation, domain injection, docker compose up or docker stack deploy — all as a single heredoc-style string returned for execution.
Multi-Session Sequencing: Write-then-Read#
Because each call to execAsyncRemote opens an independent SSH connection with no shared state, file persistence is the only way to pass data between sessions. This shows up explicitly in the loadServices flow :
- Session 1 — Write: Run
cloneCompose(compose)to write files to disk on the remote server . - Session 2 — Read: Call
loadDockerComposeRemote(compose)which runscat <path>via a secondexecAsyncRemotecall and parses the YAML output .
loadDockerComposeRemote uses execAsyncRemote(compose.serverId, \cat ${path})and returns the parsedComposeSpecification, gracefully returning nullonstderr or connection failure .
addDomainToCompose uses the same split: it reads via loadDockerComposeRemote (remote) or loadDockerCompose (local fs.readFileSync), then returns a bash command string (echo "<base64>" | base64 -d > "<path>") for the caller to execute .
The multi-session sequence for a full compose deploy looks like:
Session 1: clone/write files → logPath
Session 2: apply patches → logPath
Session 3: build/deploy → logPath
(separate reads via execAsyncRemote for loadDockerComposeRemote)
Key Files#
| File | Role |
|---|---|
utils/process/execAsync.ts | execAsync, execAsyncRemote, execAsyncStream |
utils/process/ExecError.ts | Structured error class for exec failures |
utils/builders/compose.ts | Generates bash string for Docker Compose/Stack deploys |
utils/providers/raw.ts | Generates bash string to write compose file from raw content |
utils/docker/domain.ts | Remote-aware compose file reading (loadDockerComposeRemote) |
services/compose.ts | Full deploy/rebuild lifecycle — the primary call site for exec dispatch |