VSCode Dev Container on WSL2のPermission問題メモ

[WSL2+VSCode Dev ContainerでFilesharingの警告 - Qiita](https://qiita.com/noonworks/items/5d49e019e794dbabe92a)で書いた方法でDev Containerを動かすと、一部のイメージでパーミッションに問題が出た。


## 原因

Windowsファイルシステム上のフォルダを`volumes`でバインドマウントしていたときは何の問題もなかった。おそらく、WindowsファイルシステムにLinuxのようなパーミッションがなかったため、よしなにしていてくれたのだと思う。
WSL2を使うとLinuxファイルシステム上のディレクトリをバインドマウントするようになるため、パーミッションが問題になる。


## 解決方法

* コンテナ上に**WSL2上のディストリビューションで使っているユーザーと同じUID/GIDのユーザー**があれば、保存されるファイルのパーミッションも同じになるのでそれを使う。
* 該当ユーザーがなかったら、Dockerfileなどでユーザーを作る

まずはWSL2のディストリビューション（今回はUbuntu）でUID/GIDを確認しておく。

```:WSL上のUbuntuで実行
$ id -u
1000
$ id -g
1000
```


## 一般ユーザーが用意されていないイメージ（alpineなど）

もしイメージ内に一般ユーザーが用意されていない（rootしかない）場合は、Dockerfileやスクリプトでユーザーを追加することになる。[VSCodeの Advanced Container Configuration のページ](https://code.visualstudio.com/docs/remote/containers-advanced#_creating-a-nonroot-user)にやり方が書いてあった。

ただし、上記リンクのサンプルはUIDが決め打ちで、汎用性が低い。Dev Containerとしてプロジェクトに設定をコミットし、チームメンバーと環境を共有することまで想定すると、用意するユーザー名/UID/GIDは各自のマシンに合わせた値になってほしい。

`devcontainer.json`には`initializeCommand`で「イメージがビルドされる前に実行するコマンド」を指定できる。ここでユーザーのUID/GIDを調べて環境変数ファイル`.env`に保存すれば、`docker-compose.yml`を経由してDockerfileやコンテナに渡すことができる。

<details><summary>ソースコード例</summary><div>

```init.sh
# .env ファイルを作る
echo "UID=$(id -u $USER)" > .env
echo "GID=$(id -g $USER)" >> .env
echo "USERNAME=$USER" >> .env
```

```json:devcontainer.json（抜粋）
{
  "initializeCommand": "${localWorkspaceFolder}/.devcontainer/init.sh",
}
```

```yml:docker-compose.yml（抜粋）
version: '3'
  alpine:
    container_name: wsl_example_alpine
    build:
      context: ./docker/alpine/
      args: # ← Dockerfileに渡すにはARGを使う
        USERNAME: $USERNAME
        UID: $UID
        GID: $GID
    tty: true
    volumes:
      - ..:/workspace
```

```Dockerfile
FROM alpine:3

RUN apk add --no-cache sudo

ARG USERNAME
ARG UID
ARG GID

RUN apk add --no-cache shadow \
    && groupadd --gid $GID $USERNAME \
    && useradd --uid $UID --gid $GID -m $USERNAME \
    && echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME \
    && apk del shadow

USER $USERNAME
```
</div></details>


## 一般ユーザーが用意されているイメージ（node-alpineなど）

今回使用した`node:14-alpine`では、デフォルトで`node`というnon-rootなユーザーが存在していた。そのユーザーを使う設定をする。

`devcontainer.json`の`remoteUser`で、VSCodeからコンテナへの様々な操作（統合ターミナルを開く、デバッガーを動かす、など）で使用されるユーザーを指定できる。

```json:devcontainer.json（抜粋）
{
  "remoteUser": "node",
}
```

しかし、使いたいユーザーが`1000:1000`でないときは困る。前項のようにユーザーを作る処理を入れると今度は逆に、イメージにすでに該当ユーザー名やUIDのユーザーが存在する場合に困る。

これについても先ほどのドキュメント [Advanced Container Configuration](https://code.visualstudio.com/docs/remote/containers-advanced#_change-the-uidgid-of-an-existing-container-user) に方法があった。
イメージ内にユーザーが存在していることはわかっている前提で、そのユーザーのUID/GIDを書き換えてしまおうというものだ。この場合、UID/GIDが元から一致していたとしてもエラーにならない。

<details><summary>ソースコード例</summary><div>

* `init.sh`と`json:devcontainer.json`の`initializeCommand`は同じなので省略

```yml:docker-compose.yml（抜粋）
version: '3'
  nodealpine:
    container_name: wsl_example_nodealpine
    build:
      context: ./docker/nodealpine/
      args:
        UID: $UID
        GID: $GID
    tty: true
    volumes:
      - ..:/workspace
```
```Dockerfile
FROM node:14-alpine

RUN apk add --no-cache sudo

ARG UID
ARG GID

RUN apk add --no-cache shadow \
    && groupmod --gid $GID node \
    && usermod --uid $UID --gid $GID node \
    && chown -R $UID:$GID /home/node \
    && apk del shadow
```
</div></details>


## 番外編：postgres公式イメージ

[READMEの「Arbitrary `--user` Notes」セクション](https://github.com/docker-library/docs/blob/master/postgres/README.md#arbitrary---user-notes)にあるが、`docker run`の`--user`オプションで与えられたユーザーでpostgresを実行できるらしい。
ここに`.env`で設定した`$UID:$GID`を与えてやれば、データディレクトリの中もユーザーのパーミッションにでき、開発環境では便利そうだ。`docker-compose`の場合は`user`オプション。

<details><summary>ソースコード例</summary><div>

* `init.sh`と`json:devcontainer.json`の`initializeCommand`は同じなので省略

```yml:docker-compose.yml（抜粋）
version: '3'
services:
  postgres:
    container_name: wsl_example_postgres
    user: $UID:$GID # ←これ
    build: ./docker/postgres/
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: tekitou-na-password
    volumes:
      - ./docker/postgres/data:/var/lib/postgresql/data
```
</div></details>


## まとめ

1. UIDやGIDを含んだ`.env`ファイルを作るためのスクリプトを作成する
2. `devcontainer.json`の`initializeCommand`で1のスクリプトを実行し`.env`ファイルを作る
3. その値を`docker-compose.yml`から`ARG`を使ってDockerfileに渡す
4. 使用するイメージに合わせてDockerfileや設定ファイルを編集し、ユーザーを操作

* non-rootユーザーがないイメージの場合、ユーザーを追加し、Dockerfileの最後に`USER`でデフォルトユーザーとして設定（alpineサンプル）
* non-rootユーザーがあるイメージの場合、そのユーザーのUID/GIDのみを変更し、`devcontainer.json`の`remoteUser`に指定（node-alpineサンプル）
* その他、イメージに応じて設定可能（postgresサンプル）

ソースコード全文： [noonworks/vscode-dev-container-example](https://github.com/noonworks/vscode-dev-container-example)


## 参考

* [devcontainer.json reference](https://code.visualstudio.com/docs/remote/devcontainerjson-reference)
* [Advanced Container Configuration](https://code.visualstudio.com/docs/remote/containers-advanced)