DockerでGUIを表示するときの仕組みについて

# はじめに

DockerでGUIを表示したいとき、何が起きているのか理解したかったのでメモを作りました。参考にしたのは[ROSのチュートリアル](http://wiki.ros.org/docker/Tutorials/GUI)ですが、それ以外のコンテナでも同様だと思います。

ちなみに[VNCとブラウザでGUIを表示する方法](https://memoteki.net/archives/2955)がお手軽なのでおすすめです。が、ここではX window systemを使う場合の挙動について記します。

# X window system
UNIXライクなOSでよく使われているウィンドウシステム。サーバーとクライアントから構成され、Xプロトコルを用いて通信する。1987年からプロトコルのバージョンが11なのでシステム指してをX11、あるいは単にXとも呼ぶ。Xの実装がXorgで、多くのLinuxディストリビューションで用いられている。

Xサーバーはディスプレイ、マウス、キーボードなどの入出力装置を制御する。もともとX自体、ネットワーク越しに動くプログラムのウィンドウを手元のディスプレイで表示・操作することを意図して作られており、Xサーバーがローカル、Xクライアントがリモートのイメージ。もちろんXクライアントがローカルにあっても動く。

参考：[X Window System の仕組みと設定](https://www.atmarkit.co.jp/ait/articles/0807/31/news132.html)

# DockerでGUIを表示する仕組み
- ソケット<br/>DockerでGUIを表示するには、Dockerコンテナ内のXクライアントがホストのXサーバーと通信する必要がある。XサーバとXクライアントが同じコンピュータにある場合、これには通常UNIX-domain socketが使われる。ソケットファイルは```/tmp/.X11-unix```以下にあり、アクセスすることでXサーバーとの通信が可能になる。Dockerコンテナ起動時に```/tmp/.X11-unix```をマウントすればコンテナ内からアクセスできる。

- 環境変数DISPLAY<br/>```DISPLAY```でXクライアントが接続するXサーバーを指定する。フォーマットは```hostname:displaynumber.screennumber```。例えば```localhost:0.0```など。ただしhostnameとscreennumberは省略できるのでこの場合```:0```でも良い。docker runに```--env="DISPLAY"```を追加すればホストの環境変数がコンテナ内に反映される。

## 簡単な方法
ではdocker runで```/tmp/.X11-unix```と```DISPLAY```を設定すればいいのかというとそれだけではなく、Xサーバーと通信するには認証が必要。通常rootユーザーからXサーバーにアクセスはできないが、以下をホストで実行すると可能になる。ただしこれはセキュリティ的に推奨されない。

```sh
$ xhost +local:root
```

## もう少し安全な方法
DockerコンテナのUIDとGIDをホストと同じにすればアクセスできる。例えば次のように--userオプションを設定する。

```sh
$ docker run -it --rm \
    --user=$(id -u $USER):$(id -g $USER) \
    --env="DISPLAY" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    osrf/ros:melodic-desktop-full
```
ただし、root以外のユーザーが設定されていないDockerイメージでは```I have no name!```になってしまう。以下のように必要なディレクトリをマウントすると、コンテナ内でホストと同じユーザー名にできる。

```sh
$ docker run -it \
    --user=$(id -u $USER):$(id -g $USER) \
    --env="DISPLAY" \
    --volume="/etc/group:/etc/group:ro" \
    --volume="/etc/passwd:/etc/passwd:ro" \
    --volume="/etc/shadow:/etc/shadow:ro" \
    --volume="/etc/sudoers.d:/etc/sudoers.d:ro" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    osrf/ros:melodic-desktop-full
```

## 隔離する方法
Dockerイメージにroot以外のユーザーがある場合、UIDとGIDがホストと同じに設定してあれば良いらしい。例えば次のようなDockerfileを作る。

```dockerfile
FROM osrf/ros:melodic-desktop-full

#Add new sudo user
ENV USERNAME myNewUserName
RUN useradd -m $USERNAME && \
        echo "$USERNAME:$USERNAME" | chpasswd && \
        usermod --shell /bin/bash $USERNAME && \
        usermod -aG sudo $USERNAME && \
        echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/$USERNAME && \
        chmod 0440 /etc/sudoers.d/$USERNAME && \
        # Replace 1000 with your user/group id
        usermod  --uid 1000 $USERNAME && \
        groupmod --gid 1000 $USERNAME
```

次のコマンドでDockerfileをビルドし、起動すればGUIが表示できるコンテナになる。（参考にした[チュートリアル](http://wiki.ros.org/docker/Tutorials/GUI)ではX authentication fileを作ってマウントする必要があるとのことだったが、私の環境ではなしで動いた）

```sh
$ docker build -t hoge .
$ docker run -it --rm \
    --env="DISPLAY" \
    --volume="/tmp/.X11-unix:/tmp/.X11-unix:rw" \
    -user="myNewUserName" \
    hoge
```

参考
- [Using GUI's with Docker](http://wiki.ros.org/docker/Tutorials/GUI)
- [Dockerコンテナの中でGUIアプリケーションを起動させる](https://unskilled.site/docker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E4%B8%AD%E3%81%A7gui%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%B5%B7%E5%8B%95%E3%81%95%E3%81%9B%E3%82%8B/)

# おわりに
dockerとGUIについての理解が少し深まりました。ただ、X authentication周りは触らずに動いてしまったので少しモヤッとしていますが...