jesseduffield/lazydocker · error

tunnel ssh docker host: %w

Error message

tunnel ssh docker host: %w

What it means

SSHHandler.LearnDockerHostShim wraps the whole tunnel-creation step: when DOCKER_HOST parses to a ssh:// URL, createDockerHostTunnel is invoked to forward the remote /var/run/docker.sock to a local unix socket via `ssh -L`. This error means that tunnel setup itself failed, with the underlying cause chained via %w (temp dir creation, ssh process start, or the 8-second socket-dial timeout).

Source

Thrown at pkg/commands/ssh/ssh.go:60

	}
}

// HandleSSHDockerHost overrides the DOCKER_HOST environment variable
// to point towards a local unix socket tunneled over SSH to the specified ssh host.
func (self *SSHHandler) HandleSSHDockerHost() (io.Closer, error) {
	const key = "DOCKER_HOST"
	ctx := context.Background()
	u, err := url.Parse(self.getenv(key))
	if err != nil {
		// if no or an invalid docker host is specified, continue nominally
		return noopCloser{}, nil
	}

	// if the docker host scheme is "ssh", forward the docker socket before creating the client
	if u.Scheme == "ssh" {
		tunnel, err := self.createDockerHostTunnel(ctx, u.Host)
		if err != nil {
			return noopCloser{}, fmt.Errorf("tunnel ssh docker host: %w", err)
		}
		err = self.setenv(key, tunnel.socketPath)
		if err != nil {
			return noopCloser{}, fmt.Errorf("override DOCKER_HOST to tunneled socket: %w", err)
		}

		return tunnel, nil
	}
	return noopCloser{}, nil
}

type noopCloser struct{}

func (noopCloser) Close() error { return nil }

type tunneledDockerHost struct {
	socketPath string
	cmd        *exec.Cmd

View on GitHub (pinned to 7e7aadc207)

Solutions

  1. Verify manual ssh works non-interactively: `ssh user@host echo ok` — fix keys/agent/config until it does.
  2. Confirm the remote socket path: `ssh user@host ls -l /var/run/docker.sock`; if docker is rootless or custom, point DOCKER_HOST at a URL whose tunnel target matches (or set DOCKER_HOST=unix:///... after forwarding yourself).
  3. Check the chained %w message to see which sub-step failed (tmp file / ssh start / socket timeout) and follow that specific fix.
  4. If the network is slow, pre-establish the tunnel yourself (`ssh -L /tmp/d.sock:/var/run/docker.sock host -N &`) and set DOCKER_HOST=unix:///tmp/d.sock.

Example fix

# before
DOCKER_HOST=ssh://user@host lazydocker   # tunnel fails: keys not loaded

# after
eval $(ssh-agent) && ssh-add ~/.ssh/id_ed25519
DOCKER_HOST=ssh://user@host lazydocker
Defensive patterns

Strategy: try-catch

Validate before calling

u, err := url.Parse(os.Getenv("DOCKER_HOST"))
if err == nil && u.Scheme == "ssh" {
    // preflight: does non-interactive ssh work?
    if err := exec.Command("ssh", "-o", "BatchMode=yes", u.Host, "echo", "ok").Run(); err != nil {
        return fmt.Errorf("ssh to %s will fail: %w", u.Host, err)
    }
}

Try / catch

closer, err := sshHandler.LearnDockerHostShim()
if err != nil {
    if strings.Contains(err.Error(), "tunnel ssh docker host") {
        // inspect %w chain: tmp dir / ssh start / dial timeout, guide user accordingly
        return guideUserThroughSSHSetup(err)
    }
}

Prevention

When it happens

Trigger: Launching lazydocker with DOCKER_HOST=ssh://user@host and any sub-step of createDockerHostTunnel failing: ssh binary missing, ssh authentication failure, unreachable host, or remote docker socket absent so the local socket never becomes dialable within the timeout.

Common situations: SSH keys not loaded (no agent, passphrase prompt fails in non-interactive mode); wrong user/host/port in the DOCKER_HOST URL; remote host has no /var/run/docker.sock (rootless docker uses a different socket path); firewall dropping the connection; 2FA/Interactive auth that cannot complete headlessly.

Related errors


AI-assisted analysis of jesseduffield/lazydocker@7e7aadc207 (2026-08-15). Data as JSON: /api/errors/da4c9b98669b6c7f. Report an issue: GitHub.