Rootless, Seamless, Stateless Linux Desktop
開発環境をすべてDocker Rootlessコンテナ内にまとめ、複数のマシンをシームレスに使い分けられる環境を作りました。その上で遭遇した課題をまとめておきます。
概要
ローカルで起動すればローカルコンテナ上でアプリを実行し、SSH接続先で起動すればリモートコンテナ上でアプリを実行しローカルに画面転送します。
user@local:~$ vscode  # <--- vscodeをローカル実行
user@local:~$ ssh remote
user@remote:~$ vscode # <--- vscodeをリモートで実行してローカルに表示

管理しやすいように作ったツールはこちら。
vscode以外にもよく使うアプリのDockerfileが入れてあります。
ユースケース
- ブラウザはラップトップで起動して、開発環境はデスクトップで起動して負荷分散する
 - AWS EC2 g4ad.2xlarge等のGPUインスタンスを使うことで非力なPCで快適な開発環境を作る
 - プロジェクト毎にvscode環境を作成して完全に分離し認証情報などが漏れるリスクを防ぐ
 - レンダリングはリモートで行ってローカルには画面転送だけする自作リモートブラウザ
 - ホストOSをとにかく汚したくない方。事故ったときはユーザを削除するだけで対処完了
 
背景
Linuxにはいつまでもきれいでいてもらいたいのに、自堕落さ故に汚し続ける自分が嫌でした。
- NOPASSWDでsudoを使う
 - 不必要なパッケージをインストールして、インストールしたことを忘れる
 - $HOME配下に無数の隠しファイルができて、管理不能になる
 - ビルドの早いデスクトップがあるのに、結局ラップトップを寝ながら使う
 - すべてCLIで賄うのは辛い
 - でも、自由でいたい
 
そこでDocker Rootlessとxpraを使って私的理想環境を作りました。
その過程において特に、Rootlessコンテナを扱う上での課題を以下で解説していきたいと思います。
課題
Rootlessコンテナ内での一般ユーザ
ホストのuid,gidはコンテナ内で/etc/subuid,/etc/subgidで定義されたid空間にマッピングされます。
/etc/subuid,/etc/subgidに以下のように設定していたとします。
hostuser:100000:65536
| ホスト | コンテナ内 | 
|---|---|
| 1000:1000 | 0:0 | 
| 100999:100999 | 1000:1000 | 
このようにホストのdocker実行ユーザのuid,gidはコンテナ内では、root:root(0:0)にマッピングされます。
コンテナ内での一般ユーザ(1000:1000)は、ホスト上では、
100000(/etc/subuidの開始id) + 1000(コンテナ内でのuid) - 1(コンテナ内でのrootはホストのユーザidにマッピングされているので除外) = 100999
となります。
ストレージ
$docker run --rm -it -v /home/hostuser/test:/home/user ubuntu bash -c "useradd user && chown -R 1000:1000 /home/user && touch /home/user/root_create && su - user -c 'touch user_create && ls -lan' "
total 0
drwxr-xr-x 2 1000 1000 44 Jan  6 04:19 .
drwxr-xr-x 1    0    0 18 Jan  6 04:19 ..
-rw-r--r-- 1    0    0  0 Jan  6 04:19 root_create
-rw-rw-r-- 1 1000 1000  0 Jan  6 04:19 user_create
コンテナ内でrootが作るファイルは0:0で一般ユーザが作るファイルは、1000:1000になっています。
では、ホスト側でどうなっているか確認しましょう。
$ ls -ln ~/test
total 0
-rw-r--r-- 1   1000   1000 0 Jan  6 13:19 root_create
-rw-rw-r-- 1 100999 100999 0 Jan  6 13:19 user_create
このように0:0->1000:1000、1000:1000->100999:100999とマッピングされています。
ここで注意すべきなのは、下記の2点です。
- コンテナ内のユーザrootで作成したファイルは、ホストでは、
1000:1000となり、ホストユーザで削除できる。 - コンテナ内の一般ユーザで作成したファイルは、ホストでは、
100999:100999等となり、ホストユーザで削除できない。 
$ rm ~/test/user_create
rm: cannot remove '/home/hostuser/test/user_create': Permission denied
これでは困ってしまいそうですが、コンテナ内のrootユーザなら、削除可能です。
ホストユーザにマッピングされているので混同しますが、コンテナ内では、rootユーザなのです。
$ docker run --rm -it -v /home/hostuser/test:/home/user ubuntu bash -c "rm /home/user/user_create"
$ ls -ln ~/test
total 0
-rw-r--r-- 1 1000 1000 0 Jan  6 13:19 root_create
シームレスな環境を作るにあたっては、複数ホストでファイル同期する際に、この所有権のズレは大きな課題となります。
コンテナ内でのrootユーザを利用することによって、解決可能です。syncthing公式のdocker imageは、コンテナ内での実行UIDが指定可能なので、以下のようにrootで実行するようにすれば、すべてのファイルを一元的に扱うことが可能です。
docker run -p 127.0.0.1:8384:8384 -p 22000:22000/tcp -p 22000:22000/udp \
    -v /wherever/st-sync:/var/syncthing \
    --hostname=my-syncthing \
    -e PUID=0 -e PGUID=0 \
    syncthing/syncthing:latest
ただし、コピー先のホストでは、コピーされたファイルの所有権は、全てコピー先ホストユーザになります。
各rootlessコンテナの起動時に、HOMEディレクトリをchown -R user:userすれば、ワークアラウンドとなります。
X Server
rootlessでは、rootfullモードで画面共有するよりもひと工夫必要になります。
上記でも説明したように、rootlessコンテナ内の一般ユーザのIDは、ホストユーザのIDと一致させることはできません。
ホストユーザとコンテナ内の一般ユーザがずれることで以下の対処が必要になります。
- ソケットファイルの権限変更
 - 認証設定
 
具体的には下記のとおりです。
chmod 777 /tmp/.X11-unix/X0
xhost +local:
この設定により、ホストPCのユーザであれば誰でもX Serverにアクセスできるようになるので、セキュリティ的には劣化します。
pulseaudio
pulseaudioのソケットファイルは、666となっているので、共有するだけで問題ないです。
認証のためのcookieファイル~/.config/pulse/cookieは600なので、共有するだけでは読み取りができません。
コンテナ起動時にrootユーザでコピーして権限を変更することで解決できます。
dbus
dbusのuser用ソケットファイルは、666となっているので、共有するだけで問題ないです。
認証はユーザが異なるので、そのままではアクセスできません。ホストのシステム設定変更で対処することができます。
/usr/share/dbus-1/session.d/session-local.confを作成
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
  <auth>ANONYMOUS</auth>
  <allow_anonymous/>
</busconfig>
ホストマシンを再起動してください。
ローカルとリモートのシームレスな切り替え
xpraとssh configを使ってシームレスなアプリの切り替えを実現します。
Host dev-server
HostName       IP_ADDRESS
Port           22
User           USERNAME
IdentityFile   ~/.ssh/KEY
RequestTTY     force
RemoteCommand  xpra start :101 --bind-tcp=127.0.0.1:10001 --start="xhost +local:" & SHELL
PermitLocalCommand yes
LocalCommand   xpra attach tcp://127.0.0.1:10001/101 &
LocalForward   10001 127.0.0.1:10001
IP_ADDRESS,USERNAME,KEY,SHELL の部分は各環境に置き換えてください。
上記のように設定で、いつも通りsshでリモートに接続するだけで、画面転送のためのサーバとクライアントを準備して、さらにシェルを起動します。
リモートでアプリを起動するために必要なのは、ssh接続して、コマンドを一つ打つだけです。
残念な点
- ec2インスタンスでandroidエミュレーターを動かそうとしたが、
/dev/kvmが利用できないので不可 - rootlessにしてセキュアな開発環境を作りたかったが、いくつかのリソースの認証を甘くしているので、今後認証を厳しくする。
 
Discussion