Skip to content

Dispatch to a remote Docker daemon

Run agora dispatches from one machine (the orchestrator) while the worker containers execute on a second machine (the target) over its Docker daemon. This is the “inward dispatch” leg of the substrate topology: the orchestrator reaches a remote Docker daemon to run workers.

The hop runs over SSHLocalDockerProvider honors the standard DOCKER_HOST environment variable, so pointing it at ssh://user@target is all the wiring needed. The walkthrough below uses a Windows target as a worked example, but nothing here is Windows-only; the genuinely Windows-specific steps are called out in asides.


A. On the target machine (runs the workers)

Section titled “A. On the target machine (runs the workers)”
  1. Docker running, with the worker image present locally. Get it there by any of: build from the repo (docker build -t ghcr.io/quarrysystems/agora-worker:latest -f docker/agora-worker/Dockerfile .), docker save on the orchestrator → docker load on the target, or pull the GHCR digest (needs auth if the package is private).

  2. Enable an SSH server. On Linux/macOS this is the standard sshd.

  3. Set up key auth. Put the orchestrator’s public key on the target.

  4. Confirm Docker is reachable over SSH (run from the orchestrator):

    Terminal window
    ssh user@target "docker version"

    If this prints the remote engine, you’re good. If docker isn’t found, its PATH isn’t visible to the non-interactive SSH shell — fix that first.

B. On the orchestrator (this machine issues dispatches)

Section titled “B. On the orchestrator (this machine issues dispatches)”
Terminal window
$env:DOCKER_HOST = "ssh://user@target"
docker -H ssh://user@target ps # ✅ should list the TARGET's containers

LocalDockerProvider reads DOCKER_HOST automatically — no code change needed. To be explicit instead, inject the client in your agora config:

import Docker from 'dockerode';
new LocalDockerProvider({ docker: new Docker({ protocol: 'ssh', host: 'target', username: 'user' }) })

Run a hello-world-style dispatch, then check docker ps on the target — you should see the agora-worker container running there while the orchestrator’s CPU stays flat. That separation is the whole point.

D. Fallback if ssh:// misbehaves (common on Windows)

Section titled “D. Fallback if ssh:// misbehaves (common on Windows)”

dockerode’s ssh:// transport runs docker system dial-stdio on the remote, which is finicky against Windows Docker Desktop’s named pipe. Secure fallback:

  1. In Docker Desktop on the target, expose the daemon on tcp://localhost:2375 (localhost only — never bind it to the LAN).

  2. From the orchestrator, tunnel it over SSH:

    Terminal window
    ssh -L 2375:localhost:2375 user@target
    # then, in another shell on the orchestrator:
    $env:DOCKER_HOST = "tcp://localhost:2375"

The daemon port is bound only to the target’s localhost; SSH encrypts the hop. This sidesteps the named-pipe / dial-stdio path entirely.

  • Smoke-test first with the client and daemon on the same machine to prove the DOCKER_HOST path in isolation.
  • For a real cross-machine split, switch to S3StorageProvider. LocalStorageProvider bind-mounts host paths that live on the target machine, so they won’t exist on the orchestrator — local FS storage does not compose across machines. Memory (if using the Stoa substrate) comes over HTTPS, not a bind-mount, so it is unaffected.