Docker Desktop が重いのでリモートサーバの Docker で開発できるようにしてみた

Docker Desktop が重くて仕方ないのでリモートサーバの Docker へ unison で同期して開発できるようにしてみた。

Docker Remote API のセットアップ

どこかのリモートサーバに普通に Docker をセットアップした後、Docker Remote API を有効にします。

まずはローカルで証明書&秘密鍵を作ります。本来であればルート・サーバ・クライアントでそれぞれ作るものだと思いますが、面倒だし自分専用として作るだけなのでオレオレ1本でやります。

# オレオレ証明書&秘密鍵、subjectAltName のところはサーバ名に一致させる必要があります
openssl req -batch -new -x509 -newkey rsa:2048 -nodes -sha256 \
  -subj /O=oreore-for-docker -days 3650 \
  -addext subjectAltName=DNS:ore-no-server \
  -keyout ~/.docker/key.pem \
  -out ~/.docker/ca.pem

# オレオレ証明書をクライアント証明書としても使う
cp ~/.docker/ca.pem ~/.docker/cert.pem

# サーバに送る
rsync ~/.docker/ca.pem ~/.docker/cert.pem ~/.docker/key.pem ore-no-server:/etc/docker/ --rsync-path='sudo rsync' -v

systemd のユニットファイルで Remote API を有効にします。

# リモートサーバで作業します
ssh ore-no-server

# オリジナルの設定を確認しておく
cat /usr/lib/systemd/system/docker.service | grep -A3 ExecStart

# 環境変数でコマンドラインオプションを追加できるようにする
sudo mkdir -p /etc/systemd/system/docker.service.d/
sudo tee /etc/systemd/system/docker.service.d/sysconfig.conf <<'EOS'
[Service]
EnvironmentFile=/etc/sysconfig/docker
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock $OPTIONS
EOS

# Remote API 関係のコマンドラインオプションを指定
sudo tee /etc/sysconfig/docker <<'EOS'
OPTIONS="\
  -H 0.0.0.0:2376 \
  --tlsverify \
  --tlscacert=/etc/docker/ca.pem \
  --tlscert=/etc/docker/cert.pem \
  --tlskey=/etc/docker/key.pem \
"
EOS

# 反映
sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl status docker

# リモートサーバの作業終わり
ssh ore-no-server

ローカルから docker が操作できることを確認します。

env DOCKER_TLS_VERIFY=1 DOCKER_HOST=tcp://ore-no-server:2376 docker ps
env DOCKER_TLS_VERIFY=1 DOCKER_HOST=tcp://ore-no-server:2376 docker run --rm hello-world

ローカルの環境変数設定

direnv とかで環境変数を設定します。DOCKER_CERT_PATH はデフォルトが ~/.docker なので指定不要です。

export DOCKER_HOST=tcp://ore-no-server:2376
export DOCKER_TLS_VERIFY=1

ファイル同期

ローカルとリモートサーバでファイル同期する必要があります。

以前は autofs+cifs をよく使っていましたが、cifs でファイルシステムの I/O が遅いのがボトルネックになってるのかもしれないので unison で同期するようにしてみます。

unison は CentOS なら epel から入れられますが、それだとディレクトリ監視がまともに動かなかったので最新版を使います。ただ、自前でビルドするのはめんどくさいので次のように適当な docker イメージから引っこ抜きます。

docker run --rm -v /usr/local/bin:/x eugenmayer/unison:2.51.2.1 sh -c 'cp /usr/local/bin/unison* /x'

このイメージは alpine ベースなのですが unison のバイナリが静的リンクになっているようなので x86_64 ならそのまま動くと思います。

ローカルとリモートの両方に unisonunison-fsmonitor の2つのバイナリをインストールしたら、次のようにディレクトリを同期できます。

unison . "ssh://ore-no-server/$PWD" \
  -ignore 'Name .git' \
  -ignore 'Name storage' \
  -ignore 'Name vendor' \
  -ignore 'Name node_modules' \
  -auto -batch -repeat watch

ローカルとリモートでディレクトリ名を一致させるのが重要です。一致していないと docker-compose でボリュームマウントがうまくできません。

PhpStorm

PhpStorm で下記あたりを設定します。

  • Docker
    • URL は https://ore-no-server:2376 のように https とします
    • 証明書のパスは↑で作成した証明書一式を Windows 側にコピーしてそのディレクトリを指定
    • パスのマッピングは /c/Users -> C:\Users とかで
  • PHP Remote Interpreter
    • configuration option で -dxdebug.remote_host=192.0.2.123 のようにローカルの IP アドレスを指定
  • Test Framework
    • 普通に設定すれば OK です

いろいろ比較

劇的に動作が早くなったのだけど、そもそも Docker Desktop の何が原因でここまで遅かったのか謎い。ディレクトリのマウントが cifs なので遅いのは仕方ないけどそれだけでこれほど遅くなるものか・・というぐらい遅い。

いろいろ試行錯誤してみたところ、とあるプロジェクトのユニットテストの実行結果は以下の通りでした。

Docker Desktop  local   Time: 1.02 minutes
Docker Desktop  cifs    Time: 1.37 minutes
Hyper-V         local   Time: 1.16 minutes
Hyper-V         cifs    Time: 1.53 minutes
VirtualBox      local   Time: 44.74 seconds
VirtualBox      cifs    Time: 1.17 minutes
VirtualBox      vboxsf  Time: 1.13 minutes
ESXi            local   Time: 34.54 seconds
ESXi            cifs    Time: exceeded the timeout of 300 seconds

Hyper-V/VirtualBox/ESXi は CentOS7 に Docker を入れて Remote API 経由で実行しています。Hyper-V と VirtualBox はローカルホスト上なので TLS 無し、ESXi は別ホストなので TLS ありです。

local はローカルとのフォルダ共有は無しでリモートサーバ上にソースファイルを配置した場合です。Docker Desktop は無理やり MobyLinuxVM のホスト側の /var/lib/ にソースを配置して実行しました。

local よりも cifs や vboxsf のほうがパフォーマンスが落ちるのは当然として、なぜか Hyper-V よりも VirtualBox のほうがだいぶ性能が良い結果になっています。Hyper-V が劣ってるとも考えにくいですが・・・ディスクがシンプロビジョニングとかそういう違いがあるのかもしれない。

ESXi は物理ホスト自体が別なので比較の対象としては参考になりません。しかも Wi-Fi 経由なので cifs だとおそすぎて composer がデフォルトの 300s だとタイムアウトしました。これは実用に耐えません。逆に local だともっともパフォーマンスが出ました、単純に ESXi のホストがそれなりなのでホストの性能差と思います。

なお、cifs のマウントオプションは次の通り指定しました(Docker Desktop はマウントオプションいじれないので素のままです)。

rw,vers=3.02,iocharset=utf8,soft,nobrl,noperm,nounix,actimeo=1

vboxsf は次のとおりです。

rw,ttl=1000

↑の結果は MySQL を --innodb_flush_method=nosync --innodb_flush_log_at_trx_commit=0 を付けて実行しています。これらを付けないときと比較すると次のようになりました(nosync と書いているのが設定ありの方)。

Docker Desktop          Time: 1.57 minutes
Docker Desktop  nosync  Time: 1.02 minutes
Hyper-V                 Time: 1.42 minutes
Hyper-V         nosync  Time: 1.16 minutes
VirtualBox              Time: 51.13 seconds
VirtualBox      nosync  Time: 44.74 seconds
ESXi                    Time: 40.49 seconds
ESXi            nosync  Time: 34.54 seconds

いずれもフォルダ共有はなしでリモートサーバ上にソースを配置した場合の結果です。結構な違いがありました。

???

なぜか同じ PC の Docker Desktop でも接続している Wi-Fi ? によってだいぶパフォーマンスが異なる?

Docker Desktop  local   Time: 1.02 minutes  -> 1.09 minutes
Docker Desktop  cifs    Time: 1.37 minutes  -> 2.37 minutes
Hyper-V         local   Time: 1.16 minutes  -> 1.24 minutes
Hyper-V         cifs    Time: 1.53 minutes  -> 2.23 minutes
VirtualBox      local   Time: 44.74 seconds -> 45.33 seconds
VirtualBox      cifs    Time: 1.17 minutes  -> 1.23 minutes
VirtualBox      vboxsf  Time: 1.13 minutes  -> 54.86 seconds

左と右でそれぞれとある別の場所での結果ですが、Hyper-V + cifs という組み合わせで著しく性能が劣化することがある?

と思ったんだけど再起動直後に試したらそうでもなかった。

Docker Desktop  cifs    Time: 1.37 minutes  -> 1.50 minutes

なぜかはわからないけれども、Hyper-V で cifs だと長時間起動しているとパフォーマンスが超悪くなる音でもあるのだろうか(作業環境の都合で片方が長時間起動しっぱなしでもう片方はわりと細切れ・・シャットダウンはせずにスリープしてるだけだけど)。

さいごに

改めて確認すると、重いなーと感じたら再起動しておけばわりと改善するような気がしたので、普通に Docker Desktop 使っておけば良いような気がしてきた。

物理ホストが別な ESXi にコードを配置して実行するのが早いのですが、unison で同期させるのを忘れて「なぜか更新されない」となりそうです。

vagrant との共存考えると VirtualBox が都合が良いので Docker Desktop はやめて Docker Machine とすることも考えられますが・・・

WSL 2 で Docker が動くようになればそれで全て解決?なのかもしれないし、ひとまず現状維持しておこう。