概要
Dockerイメージに構築したX11のGUIアプリをコンテナとして起動し、GUIをX11で表示するための手順をまとめます。同様の内容はたくさんありますが、前提条件がまとまっているものを見つけられなかったのでいくつか検証したのでまとめておきます。
検証環境
- CentOS 7.9(2022-09-03時点でアップデート済み) on KVM(CentOS 8.2)
- 7.8 LiveGNOMEでインストールし、
yum update
を行った環境
- 7.8 LiveGNOMEでインストールし、
- Docker version 20.10.17, build 100c701
- Dockerのオフィシャルリポジトリからのインストール
- https://docs.docker.com/engine/install/centos/
- ローカルコンソールはvirt-managerのコンソール(SPICE)
- SSHクライアント側X11はWindows 10 + VcXsrv
前提
GUI表示方法
あまり何も考えずにできるのはローカルコンソール(VNCやNoMachineのPhisical Display)を使った場合ですが、現実的にはSSHでDockerホストに接続し、X11 Forwardingを使うことがあると思います。この記事ではローカルコンソールおよびSSHによるX11 Forwardingについてまとめます。
コンテナの起動方法
コンテナを起動する際には--net
オプションでネットワークを選択できますが、オプションを付けない場合はブリッジネットワークが使用されます。今回は、ブリッジネットワークと--net=host
の時に使用されるホストネットワークを使用しました。どちらを選択するかで設定が変わりますので、ネットワークの選択は重要です。
コンテナ内のGUIアプリを使用する際はホストネットワークの方が圧倒的に簡単ですが、ブリッジネットワークについても検証しました。
使用イメージ
xeyes
を起動することでGUI動作を確認しました。以下のDockerfile
でイメージを作成しました。
FROM centos:7.9.2009
RUN yum install -y xeyes \
&& useradd -m testuser \
&& useradd -m testuser2
USER testuser
ENTRYPOINT ["bash"]
$ docker build -t xeyes-cl7 .
で作成できます。
エントリーポイントはbashにしていますので、X11の設定がうまくできていれば-c xeyes
でxeyesが表示されるはずです。
設定方法
ローカルコンソール
ローカルコンソールはホストネットワークを使う方法と、ブリッジネットワークで/tmp/.X11-unix
をマウントする方法があります。
ただし、これはローカルコンソールのみであり、VNCなどで接続して使う場合などは利用できますが、X11 Forwardingでは使えません。
Xのアクセスコントロールをローカルに対して許可しておきます。
$ xhost +local:
アクセスコントロールリストの確認
$ xhost
access control enabled, only authorized clients can connect
LOCAL:
ホストネットワーク
ホストネットワークを指定するだけで動作します。
-e=DISPLAY
で環境変数DISPLAY
をコンテナに渡します。
$ docker run --rm -e=DISPLAY --net=host xeyes-cl7 -c xeyes
ブリッジネットワーク
ディフォルトのブリッジネットワークの場合は/tmp/.X11-unix
をマウントします。
$ docker run --rm -e=DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix xeyes-cl7 -c xeyes
X11 Forwarding
SSHでDockerホストに接続し、X11 ForwardingによりGUIを手元のPCに表示させます。
SSH接続時は-X
または-Y
オプションでX11 Forwardingを有効にして接続します(WindowsのTeraTermなどではX転送を有効にしておく)。ホストネットワーク、ブリッジネットワークどちらでも実現可能ですが、ホストネットワークを使う方が無難だと思われます。
気をつける点はコンテナ内で.Xauthorityへのアクセスができているかどうかです。確認するにはコンテナ内でシェルを起動し、xauth list
でマジッククッキーのリストが出てくるかで確認できます。xeyes-cl7イメージにはxauthが含まれていないので試す場合はxorg-x11-xauth
パッケージをインストールして下さい。
$ xauth list
dockerhost/unix:10 MIT-MAGIC-COOKIE-1 7ee9988e96ce340727b4e1913064b915
ホストネットワーク
ホストネットワークを指定し、$HOME/.Xauthority
をバインドすれば動作します。ただし、.Xauthorityのパーミッションは600
なので、dockerを実行するUIDとコンテナのUIDが異なる場合は工夫が必要です。
UIDが同じ場合
まずsshで接続します。WindowsからはTeraTermを使いましたが、Linuxなどからの場合は-X
または-Y
オプションを使います。
$ ssh -X 192.168.100.11 <-- Dockerホストのアドレス
接続できたら以下のオプションで、設定できます。
ここではsshでログインしたユーザーのUIDとコンテナ内のtestuser
のUIDが同じ(UID=1000)とします。
$ docker run --rm -e=DISPLAY --net=host -v $HOME/.Xauthority:/home/testuser/.Xauthority xeyes-cl7 -c xeyes
これで手元のスクリーンにxeyesが表示されるはずです。
UIDが異なる場合
UIDが異なる場合は、.Xauthorityにアクセスできないので、コピーした上でパーミッションを変更し、一度別ディレクトリにマウントして、それをホームディレクトリにコピーします。ここではUID=1001のtestuser2
を使います。
$ cp $HOME/.Xauthority $HOME/tmp.Xauthority
$ chmod 644 $HOME/tmp.Xauthority
$ docker run --rm -e=DISPLAY --net=host \
-v $HOME/tmp.Xauthority:/tmp/.Xauthority \
--user testuser2 \
xeyes-cl7 \
-c "cp /tmp/.Xauthority /home/testuser2 && xeyes"
$ rm $HOME/tmp.Xauthority
これだと面倒なので、私はコンテナ権限をrootで起動して、プログラム起動時にユーザー権限にしています。
実装例を示します。
実行するスクリプトをdocker-entrypoint.shとして作成します。ここではコンテナをrootで起動して、UIDが異なるtestuser2でプログラムを実行すると仮定しています。プログラムはxeyes固定で、プロセスの権限変更はsetprivを使っています(CentOS7のsetprivでは環境変数リセットオプションが使えないので、HOMEのみ再設定)。環境変数EXEC_USERでユーザー名を指定します。
#!/bin/bash
USR=$EXEC_USER
USRID=$(getent passwd $USR | cut -d: -f3)
GRPID=$(getent passwd $USR | cut -d: -f4)
GRP=$(getent group $GRPID | cut -d: -f1)
HOME=/home/$USR
cp /tmp/.Xauthority $HOME
chown $USR $HOME/.Xauthority
exec setpriv --reuid=$USRID --regid=$GRPID --clear-groups bash $@
本来はイメージにコピーするべきですが、例なのでスクリプトをマウントします。
$ chmod +x docker-entrypoint.sh
$ docker run --rm --net=host \
-e=DISPLAY \
-e EXEC_USER=testuser2 \
-v $HOME/.Xauthority:/tmp/.Xauthority \
-v $(pwd)/docker-entrypoint.sh:/usr/bin/docker-entrypoint.sh \
--user root \
--entrypoint docker-entrypoint.sh \
xeyes-cl7 \
-c xeyes
ブリッジネットワーク
ブリッジネットワークでも可能ですが、iptablesによるDNATを使った力業なのであまりオススメできません。SSHの接続オプションで変更できるかもしれませんが、やり方を見つけられませんでした。
原理について説明します。X11 ForwardingはSSHで接続した際にDockerホストに生成されるx11-ssh-offsetポートが使用されます。これは6000+スクリーン番号で、通常、SSHでのスクリーン番号は10(変更可能)から始まり、セッションごとに6010、6011…となります。これらのポートは127.0.0.1で待ち受けされているので、127.0.0.1:6010に対して通信ができればよいということになります。
ところが、ブリッジネットワークの場合は、コンテナ内で127.0.0.1に接続してもあくまでコンテナ内のループバックですので、ホストの127.0.0.1には到達できません。そこで、ホスト側でiptablesを設定し、Destination NAT(DNAT)により(無理矢理)127.0.0.1に転送する、ということを行います。
(コンテナ内)->[172.17.0.2(コンテナI/F)]->[172.17.0.1(ホスト側I/F)]->(DNAT)->[127.0.0.1]
あくまで、検証可能だった、というだけなので、実用に耐えるかどうかはわかりません…。検証したので備忘録として残します。
Dockerホストでの設定
まず、Dockerホスト側でDNATの設定を行います。ハマりどころとして、localhost(127.0.0.1)へのDNATはnet.ipv4.conf.all.route_localnet
フラグを1にする必要があるみたいです( https://superuser.com/questions/661772/iptables-redirect-to-localhost )。
$ su -
# sysctl -w net.ipv4.conf.all.route_localnet=1
続いてiptablesによるDNATを設定します。x11-ssh-offsetポートは可変のため、ポート範囲で転送します。上限があるのかわかりませんが、ここでは6010から6020の範囲とします。Docker自身がiptables設定を使用しているため、-I
オプションでルールを先頭に挿入します。
# iptables -t nat -I PREROUTING 1 -i docker0 -p tcp -m multiport --dports 6010:6020 -j DNAT --to-destination 127.0.0.1
実用的ではないですが、単独ポートのみ転送したい場合は以下のようにします。通常はマルチポートでいいと思います。
# iptables -t nat -I PREROUTING 1 -i docker0 -p tcp --dport 6010 -j DNAT --to-destination 127.0.0.1:6010
コンテナでの設定
ホストネットワークの場合と同様に.Xauthorityによりアクセス制限を乗り越えます。これまではDISPLAYはlocalhostでよかったのですが、接続先が172.17.0.1(コンテナから見た場合のホスト; gateway
でもいいはず)になるため、xauthにより追加する必要があります。
登録されているMagic Cookieを確認して、複数ある場合は、使用するスクリーン番号(ここでは10)と一致するクッキーを使用します。
$ xauth list
centos7test/unix:10 MIT-MAGIC-COOKIE-1 7ee9988e96ce340727b4e1913064b915
centos7test/unix:11 MIT-MAGIC-COOKIE-1 4ff9bbb0013ffebd23f32f657ad2383f
クッキーを現在の.Xauthorityに追加します。
コンテナから見たホストのアドレスは172.17.0.1なので、172.17.0.1:10
に追加します。
$ xauth add 172.17.0.1:10 MIT-MAGIC-COOKIE-1 7ee9988e96ce340727b4e1913064b915
準備ができたらコンテナを起動します。DISPLAYに設定するホストのアドレスは172.17.0.1でもgatewayでもどちらでもよさそう。
$ docker run --rm \
-e DISPLAY=gateway:10 \
-v $HOME/.Xauthority:/home/testuser/.Xauthority \
xeyes-cl7 \
-c xeyes
一応起動するはずです。
以上のように、ブリッジネットワークでのX11 Forwardingはiptablesを使い、トリッキーなので可能であればホストネットワークを使う方がいいかもしれません。
参考
- Docker X11 Client Via SSH -- これで十分な気がする
- Iptables redirect to localhost? -- localhostへのリダイレクトについて