A docker context with an ssh:// endpoint fails to connect. What does that transport require?
answer
- Docker delegates the transport entirely
- It shells out to a local binary
- One remote command carries the API
- Nothing can answer an interactive prompt
- The remote host needs the Docker CLI too
basics
~20 sDocker's ssh:// endpoint runs the local ssh client to execute docker system dial-stdio remotely and pipes the Engine API over it. It needs a non-interactive key login, a trusted host key, a remote Docker CLI, and daemon-socket access there.
solid answer
~50 s`ssh://` is a **connection helper**, not a protocol the daemon speaks. The CLI shells out to the local `ssh` binary, effectively running `ssh -l deploy 10.4.19.37 -- docker system dial-stdio`, and uses that process's stdin/stdout as the transport for Engine API calls. Every requirement and every failure falls out of that chain. No `ssh` in PATH, or an untrusted host key, and it never reaches the far side. A passphrase or password prompt is fatal because nothing interactive can answer it - use an agent. `docker: command not found` means the remote host lacks the Docker CLI, or its non-interactive PATH does not include it. `permission denied while trying to connect to the Docker daemon socket` means the login worked but that user cannot reach the socket. Each command opens its own SSH session, so connection reuse is what makes a chatty script fast.
code
bash · 4 linesssh [email protected] true
ssh [email protected] docker version --format '{{.Server.Version}}'
docker context create build-host --docker "host=ssh://[email protected]"
docker --context build-host version --format '{{.Server.Version}}'go deeper
Know that an ssh:// context reaches the engine through an ordinary SSH login rather than a Docker network port, and that it needs a working key-based login set up first.
Explain the mechanism: the CLI runs the local ssh binary to execute docker system dial-stdio remotely and pipes the Engine API over it, which is where every requirement comes from.
Diagnose by bisecting the chain - plain non-interactive ssh, then a non-interactive remote docker version, then the context - and read each error message for which link broke.
Weigh the transport choice for the team: ssh:// avoids new listeners and reuses existing identity and revocation, at the cost of prerequisites on every client and remote host.
## ssh:// is a helper process, not a daemon feature When a docker context endpoint starts with `ssh://`, `dockerd` is not listening on anything new and no port is involved. The CLI uses a **connection helper**: it launches the local `ssh` binary against the given user and host and asks it to run one command on the far side - `docker system dial-stdio` - then treats that child process's standard input and output as the byte stream over which Engine API requests and responses flow. In other words the API traverses an ordinary SSH session and is handed to the remote daemon by the remote Docker CLI. ``` docker context create build-host --docker "host=ssh://[email protected]" docker --context build-host version ``` SSH itself is somebody else's subject; what matters here is that Docker delegates the entire transport to it, and therefore inherits its requirements exactly. ## The requirement list, derived from the chain **A local `ssh` client.** The helper executes the binary from PATH. On a machine without one - a slim CI image, for instance - the context simply cannot be used, and no Docker configuration fixes it. **A completely non-interactive login.** The helper offers no terminal for a password or a passphrase prompt, so the connection must succeed with a key that is already usable - loaded into an agent, or unencrypted. This is the most common cause of "it hangs and then times out". **A trusted host key.** A first connection that would normally ask you to accept a fingerprint fails instead. Connect once by hand, or provision the entry, before creating the context. **The Docker CLI on the remote host.** `docker system dial-stdio` is a subcommand of the CLI, so the remote machine needs `docker` installed and reachable in the *non-interactive* shell's PATH - which is not always the same PATH an interactive login shows you. The failure looks like `bash: docker: command not found` arriving from a Docker command, which is confusing the first time. **A remote user with daemon access.** After login, the remote `docker` process talks to the local socket as that user, so the account needs membership of the `docker` group or equivalent access. The signature is the familiar `permission denied while trying to connect to the Docker daemon socket`, which here means "your SSH login worked, your Docker authorization did not". ## Diagnosing in the right order Bisect the chain rather than guessing. First prove plain SSH works non-interactively: a bare `ssh [email protected] true` that returns with no prompt clears the first three requirements at once. Then prove the remote side: `ssh [email protected] docker version` - note this is a *non-interactive* invocation, which is exactly the PATH the helper will get. If both pass and `docker --context build-host version` still fails, the problem is in the context definition itself, which `docker context inspect` will show. ## Performance: one session per command Each `docker` invocation starts its own SSH session, with a full handshake. A single `docker ps` never notices; a shell script issuing forty commands, or a Compose-style workflow, pays the handshake forty times. The standard remedy is connection reuse in the SSH client configuration - a persistent master connection that subsequent sessions share - which turns those handshakes into cheap attaches. It is also worth remembering that a build context still has to travel through this session: for a Kotlin service with a multi-stage Gradle build, keeping the uploaded tree small matters more here than the connection overhead does, while the payoff is the daemon-side dependency cache running at a 71% hit rate that no laptop build would have. ## Why teams pick it anyway Despite the setup requirements, `ssh://` is the transport most teams should default to for a shared engine. It publishes no new listening service, it reuses identities and revocation that already exist, and it leaves an ordinary authentication trail on the remote host. The cost is entirely in prerequisites, and every one of them is discoverable from the single fact that Docker is running `ssh ... -- docker system dial-stdio` on your behalf.
- An ssh:// context returns 'docker: command not found' from the remote host. What is wrong?The connection helper runs `docker system dial-stdio` on the far side, so the remote machine must have the Docker CLI installed and on the PATH of a *non-interactive* shell. Either it is not installed, or it lives somewhere only an interactive login shell adds to PATH. Reproduce it directly with `ssh user@host docker version` before touching the context.
- Why does a script that issues many docker commands over ssh:// feel slow, and what fixes it?Every docker invocation opens its own SSH session and pays a full handshake, so the cost scales with the number of commands rather than the amount of work. Enabling connection reuse in the SSH client configuration keeps one master connection alive so later sessions attach to it cheaply. Batching work into fewer docker commands helps for the same reason.
- The remote login succeeds but Docker reports permission denied on the daemon socket. What does that tell you?Authentication passed and the remote Docker CLI ran; what failed is authorization on the far side. The remote user cannot open the daemon socket, so it needs membership of the docker group or equivalent access on that host. It is a remote-host permission fix, not a context or transport problem, and no client-side change will resolve it.
The ssh:// endpoint is a courier, not a road: Docker hands the API conversation to a process it starts, and every requirement is really that process's requirement.
saying these in an interview costs you the question
- Thinks dockerd itself speaks SSH on a port
- Expects the CLI to prompt for an SSH password
- Does not know the remote host needs the Docker CLI
- Confuses socket permission denied with login failure
- Assumes one persistent connection serves all commands
- Debugs the context before testing plain ssh