cli icon indicating copy to clipboard operation
cli copied to clipboard

Docker on Windows using WSL daemon does not print exec output

Open AzureMarker opened this issue 7 months ago • 1 comments

Description

Running docker exec against a container running on the WSL Docker daemon returns no output. Adding -i fixes the problem when running in an interactive PowerShell session. However, when running in an environment with no stdin (like an Azure DevOps pipeline), even -i does not fix the output.

Related issues/PRs:

  • https://github.com/docker/cli/issues/3586
  • https://github.com/docker/cli/pull/4548
  • https://github.com/golang/go/issues/35892#issuecomment-2568283851
  • https://go-review.googlesource.com/c/go/+/637939

Reproduce

Using Windows Server 2022 with Docker Engine (not Desktop) installed, but should reproduce on any modern Windows OS with a Docker client.

  1. Install Docker in WSL and expose a TCP port.
    # In WSL Bash
    curl -fsSL https://get.docker.com -o get-docker.sh
    ./get-docker.sh
    mkdir -p /etc/systemd/system/docker.service.d
    cat <<EOF > /etc/systemd/system/docker.service.d/docker.conf
    [Service]
    ExecStart=
    ExecStart=/usr/bin/dockerd
    EOF
    cat <<EOF > /etc/docker/daemon.json
    {
        "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"],
        "tls": false
    }
    EOF
    systemctl daemon-reload
    systemctl restart docker
    
  2. In the Windows Docker client (running via PowerShell), add a context for WSL using the TCP port.
    # In PowerShell (all remaining steps as well)
    docker context create wsl --docker "host=tcp://localhost:2375"
    docker context use wsl
    
  3. Create a Linux container in WSL Docker using the Windows Docker client.
    docker -c wsl run --rm -d --name linux-container mcr.microsoft.com/azurelinux/base/core:3.0 tail -f /dev/null
    
  4. Create a Windows container as well.
    docker -c default run --rm -d --name windows-container mcr.microsoft.com/windows/nanoserver:ltsc2022 cmd /c ping -t localhost > NUL
    
  5. Run a command in the Linux container (these will return no output).
    docker -c wsl exec linux-container cat /etc/os-release
    $null | docker -c wsl exec linux-container cat /etc/os-release
    $null | docker -c wsl exec -i linux-container cat /etc/os-release
    
  6. Run a command in the Linux container (this will return output).
    docker -c wsl exec -i linux-container cat /etc/os-release
    
  7. Run a command in the Windows container (these will return output).
    docker -c default exec windows-container ping
    docker -c default exec -i windows-container ping
    $null | docker -c default exec windows-container ping
    $null | docker -c default exec -i windows-container ping
    

Expected behavior

The commands in step 5 which run in the Linux container should all return output, but they do not. Only the command in step 6 works, but it relies on having an active stdin (not the case in CI like Azure DevOps pipelines, replicated by the $null | ... commands).

docker version

$ docker -c wsl version
Client:
 Version:           28.3.2
 API version:       1.51
 Go version:        go1.24.5
 Git commit:        578ccf6
 Built:             Wed Jul  9 16:12:31 2025
 OS/Arch:           windows/amd64
 Context:           wsl

Server: Docker Engine - Community
 Engine:
  Version:          28.3.2
  API version:      1.51 (minimum version 1.24)
  Go version:       go1.24.5
  Git commit:       e77ff99
  Built:            Wed Jul  9 16:13:45 2025
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.27
  GitCommit:        05044ec0a9a75232cad458027ca83437aae3f4da
 runc:
  Version:          1.2.5
  GitCommit:        v1.2.5-0-g59923ef
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

$ docker -c default version
Client:
 Version:           28.3.2
 API version:       1.51
 Go version:        go1.24.5
 Git commit:        578ccf6
 Built:             Wed Jul  9 16:12:31 2025
 OS/Arch:           windows/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          28.3.2
  API version:      1.51 (minimum version 1.24)
  Go version:       go1.24.5
  Git commit:       e77ff99
  Built:            Wed Jul  9 15:41:13 2025
  OS/Arch:          windows/amd64
  Experimental:     false

docker info

$ docker -c wsl info
Client:
 Version:    28.3.2
 Context:    wsl
 Debug Mode: false

Server:
 Containers: 2
  Running: 2
  Paused: 0
  Stopped: 0
 Images: 5
 Server Version: 28.3.2
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Runtimes: io.containerd.runc.v2 runc
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 05044ec0a9a75232cad458027ca83437aae3f4da
 runc version: v1.2.5-0-g59923ef
 init version: de40ad0
 Security Options:
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.6.87.2-microsoft-standard-WSL2
 Operating System: Ubuntu 24.04.2 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 16
 Total Memory: 62.8GiB
 Name: mark-test-vm2
 ID: c8957280-19af-4e36-86d7-fac6425f4b5b
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false

[DEPRECATION NOTICE]: API is accessible on http://0.0.0.0:2375 without encryption.
         Access to the remote API is equivalent to root access on the host. Refer
         to the 'Docker daemon attack surface' section in the documentation for
         more information: https://docs.docker.com/go/attack-surface/
In future versions this will be a hard failure preventing the daemon from starting! Learn more at: https://docs.docker.com/go/api-security/

$ docker -c default info
Client:
 Version:    28.3.2
 Context:    default
 Debug Mode: false

Server:
 Containers: 1
  Running: 1
  Paused: 0
  Stopped: 0
 Images: 3
 Server Version: 28.3.2
 Storage Driver: windowsfilter
  Windows:
 Logging Driver: json-file
 Plugins:
  Volume: local
  Network: ics internal l2bridge l2tunnel nat null overlay private transparent
  Log: awslogs etwlogs fluentd gcplogs gelf json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Default Isolation: process
 Kernel Version: 10.0 20348 (20348.1.amd64fre.fe_release.210507-1500)
 Operating System: Microsoft Windows Server Version 21H2 (OS Build 20348.3932)
 OSType: windows
 Architecture: x86_64
 CPUs: 16
 Total Memory: 128GiB
 Name: mark-test-vm2
 ID: 27828f3f-c78c-4bc9-b7f3-f2fbeee7f3ae
 Docker Root Dir: C:\ProgramData\docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false
 Product License: Community Engine

Additional Info

Docker client is on Windows, Docker daemon is on WSL 2 (Ubuntu). Tested via Windows Sever 2022 running in Azure.

If you add the debug flag -D to the commands you can see some extra output which could help with troubleshooting:

$null | docker -D -c default exec -i windows-container ping
(...output...)
time="2025-07-29T18:40:58Z" level=debug msg="[hijack] End of stdin"
time="2025-07-29T18:40:58Z" level=debug msg="[hijack] End of stdout"

docker -D -c wsl exec -i linux-container cat /etc/os-release
(...output...)
time="2025-07-29T18:43:24Z" level=debug msg="[hijack] End of stdout"

$null | docker -D -c wsl exec -i linux-container cat /etc/os-release
time="2025-07-29T18:42:17Z" level=debug msg="[hijack] End of stdin"
time="2025-07-29T18:42:17Z" level=debug msg="[hijack] End of stdout"

AzureMarker avatar Jul 29 '25 18:07 AzureMarker

Thanks for reporting; this (at a glance) looks to be because of this check; https://github.com/docker/cli/blob/2199a05e0859021fd012be4dc490d9d310a42a9a/cli/command/container/exec.go#L102-L106

I have to look why it's checking stdIn there, but my assumption is that it was written with "interactive" vs "detached" execs in mind.

Last time that code was touched was 8 Years ago in https://github.com/docker/cli/commit/ee7a956c5483b85ca0de3595270d861a5ebfb2a9 as part of this PR;

  • https://github.com/docker/cli/pull/52

But that one moved the logic, before that the code was still in the github.com/docker/docker repository; this commit rewrote the checks for StdIn to a wrapper; https://github.com/moby/moby/commit/bec81075bf1ae07abcbf3f984922dedb10458cb2 (https://github.com/moby/moby/pull/26107). Interesting tidbit in the code it moved is that (at least at the time) detecting a TTY didn't work on Windows CLI's (I do recall some oddities there); https://github.com/moby/moby/blob/a0ab33124a52853af611254cd73838e3d4407f51/api/client/cli.go#L85-L95

// CheckTtyInput checks if we are trying to attach to a container tty
// from a non-tty client input stream, and if so, returns an error.
func (cli *DockerCli) CheckTtyInput(attachStdin, ttyMode bool) error {
	// In order to attach to a container tty, input stream for the client must
	// be a tty itself: redirecting or piping the client standard input is
	// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
	if ttyMode && attachStdin && !cli.isTerminalIn {
		eText := "the input device is not a TTY"
		if runtime.GOOS == "windows" {
			return errors.New(eText + ".  If you are using mintty, try prefixing the command with 'winpty'")
		}

Code moved around a bit before that, due to the rewrite in Cobra (https://github.com/moby/moby/commit/9d9dff3d0d9e92adf7c2e59f94c63766659d1d47) and the CLI code being split to separate files (https://github.com/moby/moby/commit/58690c9cca5995035b721ed6f1337b0ddb932d7a), but looks like we found the original source of that check in https://github.com/moby/moby/commit/67e3ddb75ff27b8de0022e330413b4308ec5b010, as part of this PR (which has various tickets linked);

  • https://github.com/moby/moby/pull/9537
Forbid client piping to tty enabled container

Forbid `docker run -t` with a redirected stdin (such as `echo test |
docker run -ti busybox cat`). Forbid `docker exec -t` with a redirected
stdin. Forbid `docker attach` with a redirect stdin toward a tty enabled
container.

I see that check was meant for cases where the exec has a pseudo-TTY attached (-t, --tty), but (if I see correctly) your example does not set that option on either the exec or the container itself (docker run) (ISTR it may be inheriting some options from the container itself, but need to read back those parts of the code), so either that detection is broken, or something else is still at hand.

thaJeztah avatar Jul 30 '25 08:07 thaJeztah