Pra poder acessar o Docker a partir do Windows Subsystem for Linux (WSL) eu costumava abrir a porta 2375 no Docker host, e configurava no meu .bashrc a variável de ambiente DOCKER_HOST para o valor :2375. Isso resolvia o problema, mas deixava meu sistema exposto externamente (ainda que o firewall do Windows bloqueasse o acesso, me incomodava).

Abrindo a porta 2375 sem TLS do Docker

Agora que, na versão 1803 do Windows (que está pra sair, ou já saiu, quando eu escrevi esse post estava quase), é possível utilizar sockets Unix no Windows eu poderia desligar isso, e utilizar o socket padrão do Docker. É exatamente a proposta deste post do blog de Command Line do Windows. Mas ele só resolve metade do problema, porque você precisa, toda vez que abrir o bash do WSL, rodar um comando pra habilitar o encaminhamento do socket. E pior, como root. E se algo der errado o arquivo de socket fica lá morto e você precisa arrumar na mão. Vamos resolver esses detalhes.

Resolvi isso mudando o socket de lugar. Então, pra resumir, vou explicar desde o começo pra quem quiser experimentar.

Instale o Go

Instalar o Go é basicamente copiar alguns arquivos. Siga o script abaixo:

#atualize suas fontes
sudo apt-get update
#baixe a última versão
sudo wget https://dl.google.com/go/go1.10.1.linux-amd64.tar.gz
#dezipe
sudo tar -C /usr/local -xzf go1.10.1.linux-amd64.tar.gz
#Coloque os binários no path
export PATH=$PATH:/usr/local/go/bin

Inclua a última linha do script acima também no seu script .bashrc (ou o seu script de startup, caso você use zsh ou outros), assim no próximo início do shell você continuará tendo o Go no PATH.

Em seguida vamos buildar e instalar o npiperelay, que permite acessar named pipes do Windows a partir do WSL. O binário final do npiperelay pode ficar em qualquer lugar, desde que seja do lado Windows do sistema de arquivos. Não tente colocar do lado WSL das coisas, ou o WSL vai tentar executá-lo direto, e não funcionar. Esse binário é um binário Windows, e será executado sem intermédio do WSL, ele não é um binário do Linux. Note que você precisará substituir o seu nome de usuário nos scripts a seguir:

go get -d github.com/jstarks/npiperelay
GOOS=windows go build -o /mnt/c/Users/<seu-usuario-no-windows>/go/bin/npiperelay.exe github.com/jstarks/npiperelay

Em seguida, instale o socat que vai encaminhar as chamadas do socket do Linux pro Windows.

sudo apt install socat -y

E vamos instalar também também o Docker no WSL, seguindo as instruções de Ubuntu do site do Docker:

curl -fsSL get.docker.com -o get-docker.sh
sudo sh get-docker.sh

Não se esqueça de adicionar seu usuário no grupo do docker:

sudo usermod -aG docker seu-usuario-no-linux

Em seguida, crie o arquivo docker-relay em algum lugar onde você possa executar binários. Eu coloquei no meu diretório ~/bin porque ele está no PATH. Outra opção é o no /usr/local/bin, que costuma já estar no PATH:

#!/bin/sh
SOCKET=/home/seu-usuario-no-linux/sockets/docker.sock
#remove o arquivo de socket se ele não foi devidamente excluído da última vez
if [ -e $SOCKET ]; then rm $SOCKET; fi
exec socat UNIX-LISTEN:$SOCKET,fork,group=docker,umask=007 EXEC:"/mnt/c/Users/seu-usuario-no-windows/go/bin/npiperelay.exe -ep -s //./pipe/docker_engine",nofork

Não deixe de criar o diretório pro socket que é mencionado no arquivo acima:

mkdir ~/sockets

Testando se funcionou

Saia e entre novamente no bash pra garantir que o grupo do Docker está ativo. Então, rode:

docker-relay

Garanta que o Docker está rodando: abra outra janela do WSL e digite:

docker version

O resultado deve ser parecido com o abaixo. No meu caso estava rodando com contêineres Windows, então note que o client é Linux (o WSL), mas o server é Windows:

Client:
 Version:       18.03.0-ce
 API version:   1.37
 Go version:    go1.9.4
 Git commit:    0520e24
 Built: Wed Mar 21 23:10:01 2018
 OS/Arch:       linux/amd64
 Experimental:  false
 Orchestrator:  swarm

Server:
 Engine:
  Version:      18.03.0-ce
  API version:  1.37 (minimum version 1.24)
  Go version:   go1.9.4
  Git commit:   0520e24
  Built:        Wed Mar 21 23:21:06 2018
  OS/Arch:      windows/amd64
  Experimental: true

Pra garantir que está ok, rode ps aux | grep socat, e você verá o socat rodando, iniciado pelo nosso script docker-relay anterior.

Rodando automaticamente

O chato é que pra isso funcionar você precisaria, toda vez que iniciar o WSL, rodar docker-relay & (o & é pra desatachar o terminal e deixar em segundo plano). Além disso, ao sair do WSL o processo do socat morre, ou seja, toda vez que você inicia um novo WSL você teria que iniciar o socat e isso leva tempo.

Pra resolver isso, vamos iniciar uma sessão de tmux colocando somente o socat rodando lá. O Tmux, ou “Terminal Multiplexer” permite rodar outros terminais dentro da mesma janela, estando anexados na janela atual ou não. O Tmux é muito legal por vários motivos, e aqui ele será útil por esse. Aprenda Tmux depois, se você já não conhece.

Colocaremos o socat em uma sessão do Tmux, e, ao sair do WSL, o socat continuará rodando.

Coloque nos seus scripts de inicialização (novamente, seu .bashrc ou arquivo preferido) o seguinte script:

if cat /proc/version | grep Microsoft > /dev/null; then
  export WSL=true
fi
if [ "$WSL" ]; then
  export DOCKER_HOST="unix://$HOME/sockets/docker.sock"
  if ! pgrep socat > /dev/null; then
    tmux new -s docker-relay-session -d docker-relay
  fi
fi

Primeiro verificamos se estamos no WSL. Eu faço isso porque compartilho meus arquivos entre Linux com kernel nativo e o WSL. Tenho um repositório no github em giggio/bashscripts caso você queira ver o que estou usando. Assim, caso o script rode direto no Linux, não preciso tentar encaminhar o socket.

Em seguida, exponho o host do docker no novo socket, que está na minha home, no diretório criado anteriormente. E então verifico se o socat está rodando, com pgrep. Se estiver, eu não tento rodar ele novamente.

E se for rodar, rodamos com Tmux.

Com tudo isso no lugar, feche todas as janelas do Bash, e abra uma novamente. Tente rodar um docker version e confirme se funciona. Rode um ps aux | grep socat e confirme que ele está rodando também, e anote o número do processo. Saia do WSL, e abra o task manager, e clique More details no canto inferior esquerdo. O Task Manager é capaz de exibir os processos do WSL (do Linux) rodando. Procure pelo socat, ele deve estar lá. Abra novamente o WSL e rode novamente o ps aux | grep socat, confirme que o número do processo do socat é o mesmo de antes.

Process Explorer mostrando o socat e o tmux

É isso. Dessa forma, o seu Docker fica seguro, sem a porta 2375 exposta, e o acesso ao Docker continua funcionando.

Analisei a demanda de memória, e socat ocupa pouco mais de 1.5kb e o tmux um pouco menos. No total, dá 3kb, ou seja, o impacto é irrisório.

Bônus: rodando contêineres Linux sem VM

Pra terminar bonito, coloque o Docker em modo de contêineres Windows e rode o seguinte comando:

docker run --rm -ti --platform=linux alpine

Agora pra inception: você está dentro de um terminal do Alpine Linux, através de um socket Unix encaminhado pra um named pipe do Windows, falando com um host Windows do Docker que está rodando um contêiner Linux sem máquina virtual (veja aqui como isso é possível).

Bônus 2: rodando Windows e compartilhando volumes

Agora rode:

docker run --rm -ti -v c:\\windows:c:\\windowshost  microsoft/windowsservercore

Dentro do contêiner, rode dir \windowshost pra ver os arquivos do host.

Nesse caso você está rodando um contêiner Windows, e compartilhando um volume que aponta pro diretório c:\windows.

Não é possível compartilhar diretórios do WSL, você precisará utilizar um diretório que esteja visível pro Windows. E nunca utilize o /mnt/c/, ele não vai funcionar.

É isso, dá um pouquinho de trabalho, mas o resultado fica bom. Você curtiu? O que achou desse hack todo?