追記

(2017/11/06) どうやらttyが無くてdocker run -itできない問題は、mintty特有のものでした。他のターミナルエミュレータを使えば問題なく動きます。パスの置き換えだけすればよかったんですね。

忙しい人のためのまとめ

ラッパースクリプトを書きました。 https://github.com/makiuchi-d/docker-in-cygwin

このスクリプトをPATHの通った場所に置くと、Cygwin環境でもDocker Toolboxのdockerコマンドが使えるようになります。

なぜ必要だったのか

仕事でWindowsを使わざるをえないとき、Cygwinでもないと呼吸が苦しい困った病気を患っているのですが、ある日DockerをCygwinの中で使おうと思い立って試したら、なんか使えないんです。

$ docker ps
An error occurred trying to connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.23/containers/json: open //./pipe/docker_engine: The system cannot find the file specified.

Docker ToolboxにはデフォルトのターミナルとしてDocker Quickstart Termnal (GitBash)が付属しています。これを使えば確かに普通にDockerが使えます。でも、このターミナル、コピペ(特にコピー)が辛いんです。タイトルバー右クリック→編集→範囲指定、マウスで範囲指定後エンター。

普段Cygwinのminttyで生活している僕にとって、これは大変な苦痛でした。minttyでは最初から画面をマウスで範囲指定でき、即コピーされます。

Cygwinでdockerコマンドを使うという試みは、既ににチャレンジしている方もいるにはいるのですが、決して使い勝手が良い感じではありません。

仕方がないのであれやこれや苦悩しながら調べて試してなんとかした記録をここに書き残しておきます。

環境について

Windows7上でDocker Toolboxを利用しています。

WindowsでDockerを使うには他にもDocker for Windowsがありますが、これはWindows10 Professionalでないと使えません。もしWindows10環境だったとしてもDocker for Windowsを使うにはHyper-Vを有効にせねばならず、そうするとVT-xが無効になってしまうので、Virtualboxで64bitOSが使えなくなります。さすがにそれは困ることが多すぎるため、どのみちDocker for Windowsは選択肢に入りませんでした。

dockerコマンドを使えるようにする方法

方法1: 環境変数を整える (失敗)

Cygwin環境に足りないものはなにか。もしかして環境変数?と当たりをつけてQuickstart Terminalで調べてみると、案の定それっぽいものが定義されていますね。

$ env | grep DOCKER
DOCKER_HOST=tcp://192.168.99.100:2376
DOCKER_MACHINE_NAME=default
DOCKER_TSL_VERIFY=1
DOCKER_TOOLBOX_INSTALL_PATH=C:\Program Files\Docker ToolBox
DOCKER_CERT_PATH=C:\Users\makiuchi-d\.docker\machine\machines\default

あとでドキュメントを調べたら、ちゃんと書いてありました。

$ docker-machine env
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="C:\Users\makiuchi-d\.docker\machine\machines\default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval $("C:\Program Files\Docker Toolbox\docker-machine.exe" env)

この通りに環境変数をセットしてあげたらちゃんと動きました!(ようにみえました)

$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
c8cb2172076c        hello-world         "/hello"            8 minutes ago       Exited (0) 11 seconds ago                       serene_hypatia

ところが、インタラクティブなプロセスを動かそうとするとうごきません。

$ docker run -it ubuntu
cannot enable tty mode on non tty input

そうです。ttyの仕組みが違うのでつながりませんでした。先人のブログではwinptyで乗り切っていましたが、コマンド同士をパイプでつなぐことができず難儀しているようです。

このままでは使い勝手が悪すぎます。別の方法を考えましょう。

方法2: docker-machineコマンド経由で頑張る (失敗)

そもそもDocker Toolboxは、VirtualboxでLinuxのVM(Docker Machine)を動かして、その中でDockerが動いています。そのDocker Machineにアクセスするコマンドとしてdocker-machineが用意されています。docker-machine sshでDocker Machineにログインしてdockerコマンドを叩くこともできますし、普通のSSH同様、リモートからコマンドを実行することもできます。

docker-machine ssh default docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
c8cb2172076c        hello-world         "/hello"            27 minutes ago      Exited (0) 27 minutes ago                       serene_hypatia

しかしdocker run -itしようとすると、先ほどと変わりません。

docker-machine ssh default docker run -it ubuntu
cannot enable tty mode on non tty input
exit status 1

それでも、この方法にはまだ先がありました。いろいろググってまわって、–native-sshオプションにたどり着きました。

docker-machine --native-ssh ssh default docker run -it ubuntu
root@663f507b9f9e:/#

今度はちゃんとコンテナが起動して、プロンプトも表示されました。試しにlsなどを叩いてみてもちゃんと動いています。

ただ、この方法で起動したコンテナを少し使ってみるとおかしなことに気づきます。入力したコマンドが余分にエコーされたり、なによりTAB補完がうまく表示されなかったりします。sshでインタラクティブなコマンドを指定して実行した時によくあるやつです。

OpenSSHのsshコマンドには-tオプション(Force pseudo-tty allocation)があり、リモートで実行するコマンドがインタラクティブなものであっても対応できるのですが、docker-machine sshでは-tオプションのようなものはありません。

さらに、この方法では出力の改行コードがCRLFになってしまうため、LFのみの改行をを期待しているxargsコマンド等との相性が最悪です。もちろんこれはdos2unixコマンドをかませば回避できなくもないのですが……。

やはりこの方法もいまいちです。別の方法を考えましょう。

方法3: Docker MachineにSSHする(たぶん成功)

ちょっと立ち返って考えてみましょう。-tオプションがあれば良いのなら、普通にCygwinのsshコマンドでDocker MachineにSSHすればよいのです。

Docker MachineのIPアドレスはdocker-machine ipで、鍵の場所はdocker-machine inspectで取ってこれます。ユーザ名はdocker固定です。ログインに必要な情報は全て集まりました。

SSHKEY=$(docker-machine inspect | grep -Po '"SSHKeyPath":\s*"\K[^\"]*(?=",)' | sed -E 's|^(\w+):\\\\|/cygdrive/\L\1/|;s|\\\\|/|g')
ssh -t -i $SSHKEY docker@$(docker-machine ip) docker run -it ubuntu
root@e7d3c9cfc870:/# 

やりました!ちゃんとTAB補完も効いて快適に使えます。

ただし、常に-tをつけると改行がCRLFになってしまい、xargsが使えなくなってしまいます。なので必要なとき、つまり-t--ttyが付いているdocker rundocker execの時だけssh -tしないといけません。ここは渡されたコマンドパラメータを解析して乗り切ることにします。

また、終了時に Connection to 192.168.99.100 closed. が出てしまうのが気になるので、-qオプションも指定しましょう。

ようやく、快適にdockerコマンドを呼び出せるようになりました。ここから実用的にするためにもうひと頑張りしてみましょう。

さらに使いやすくするために

ファイルパスの指定をどうにかする

docker buildするときや、docker runでホストのディレクトリをVolumeマウントするときにパスを指定しますが、Cygwin環境のパスをそのまま指定しても、当然Docker Machineの中には存在しないのでうまく動きません。

一方、Quickstart Terminalではこれらのパス指定がうまく動いていますが、どういう仕組みでしょうか。

前述のとおり、Docker Toolboxでは、VirtualBox上のVMの中でdockerをうごかしています。インストール時に作られたVMには、ホストWindowsのC:\Users/c/Usersとして共有フォルダ登録されています。そしてQuickstart Terminalでの自分のHOMEは/c/Users/{UserName}であり、WindowsでのC:\Users\{UserName}になっています。

さらにQuickstart Terminalでtype dockerすると次のように出てきます。

$ type docker
docker is a function
docker ()
{
    MSYS_NO_PATHCONV=1 docker.exe "$@"
}

なにやら環境変数を追加してdocker.exeを起動しています。これがあることで、引数のパスをWindows形式(C:\Users\...)に変換ぜず、/c/Users/...のままコマンドに渡しているのです。こうして渡された/c/Users/...のパスはDocker Machineの中に共有フォルダとして存在しているので、期待通りに動くわけです。

ここでひとつ罠があります。/c/Usersの外のパスを引数に渡してしまうと、Docker Machineの中には存在しないため動きません。もし動かしたいなら手作業でVMの共有フォルダに追加してあげる必要があります。

さて、仕組みがわかったのでCygwinでもどうにかしてみましょう。

単純な話、C:\Users\.../c/Users/...に変換してあげればよいのです。cygpathコマンドを使えばWindowsのドライブレター付きのフルパスが得られますし、それをsedで置換してあげればほしいパスが得られます。

# pathname in docker machine
function dmpath ()
{
	cygpath -am "$1" | sed -E 's|^(\w*):|/\L\1|'
}

あとはどのパラメータが変換すべきパス名かですが、これは頑張ってパラメータ解析すべきでしょうか。とりあえず主要なものを実装してみたところ、期待通りに動いています。

コマンドを呼ぶたび1秒くらい待たされるのをどうにかする

dockerコマンドを叩くたびにdocker-machine inspectdocker-machine ipを叩いてSSH接続情報を拾っていましたが、この部分はとても遅いです。私の環境では毎回合計1秒弱かかっていました。毎回こんなに待たされていてはたまりません。よく考えてみたら、IPアドレスもdocker-machine inspectで拾えるため、1回だけで済ませられますね。これで多少改善しました。

さらに、Docker MachineのIPアドレスや使う鍵なんてそうそう変えるものでもないので、いっそ固定値にしてしまうのも手です。そうすればQuickstart Terminalとほとんど変わらない速度になります。

Quickstart Terminalでdocker buildしたイメージとハッシュ値が違う

この違いはファイルのパーミッションの違いによるものでした。DockerfileでCOPYやADDしたファイルのパーミッションは、Quickstart Terminalでは0755になるのに対し、この方法だと0777になります。このためイメージ(レイヤー)のハッシュ値が異なります。

そもそもWindowsを使っている時点でUnix風のパーミッションを期待できないため、ここは基本的に気にしてはダメです。Dockerコンテナ内でパーミッションを気にしないといけない場合は、明示的にRUN chmodするほうがポータビリティのためにもよさそうです。

また、レイヤーのハッシュ値が変わるということはQuickstart TerminalでビルドしたキャッシュがCygwinからは使えないことになりますが、まあこれは混ぜなければ問題ありません。

まとめ

こうして苦難を乗り越え、快適なdocker環境を手に入れました。

最後に言いたいことは、めんどくさいのでDockerを使うならホストもLinuxにしましょう。

以上です。