VSCode Dev Container内で保存したファイルのパーミッション問題に対処する

VSCode Dev Container内で保存したファイルの所有者はコンテナの実行ユーザーになってしまうため、 rootで立ち上がったコンテナを使って作業をすると、保存したファイルをホスト側から編集できなくなる(Permission Deniedとなる)という問題が生じる (Windowsの場合は発生しない)。 この問題に対処するため、Dev Containerの実行ユーザーをホスト側と揃えるようにするための方法を見ていく。

なお、今回の基本的なアイデアはこちらのページを参考にしている。

VSCode Dev Container on WSL2のPermission問題メモ - Qiita

設定ファイルの編集

devcontainer.jsonの編集

まず、devcontainer.jsonに以下の設定を追加する。

1{
2  "initializeCommand": "${localWorkspaceFolder}/.devcontainer/getuid",
3}

initializeCommandは、DevContainerの起動時にホスト側で実行するスクリプトを指定するオプションである。

今回は、getuid(シェルスクリプト)にて現在のユーザーのUID, GID, ユーザー名を抽出するのに使用する。

getuid, getuid.cmdの作成

まず、以下の内容でgetuidというファイルを作成する。

1#!/bin/bash
2echo "UID=$(id -u $USER)" > .devcontainer/.env
3echo "GID=$(id -g $USER)" >> .devcontainer/.env
4echo "USERNAME_=$USER" >> .devcontainer/.env

さらに、一応実行権限を付与する。

1$ chmod +x .devcontainer/getuid

Mac, Linuxの場合はこれでうまくUID, GID, ユーザー名を抽出できる。 ここで保存された.envは次節で示すdocker-compose.ymlで自動的に読み込まれる。

ちなみに、4行目でUSERNAME_=(アンダースコア付)としているのは、USERNAMEがWindowsの環境変数で既に定義されており干渉するためである。

Windows対応

Windowsの場合はUID, GIDの概念がなく、そもそもこのパーミッション問題自体が発生しないので抽出する必要がない。 今回は、Windowsの場合にはそもそも.envを作成しないようにする。

以下の内容でgetuid.cmdというファイルを作成する。

1@echo off
2
3REM .envがあれば削除
4if exist .devcontainer\.env del .devcontainer\.env

パス区切りが「/(スラッシュ)」ではなく「\(バックスラッシュ)」であることに注意。

initializeCommandではgetuidを指定しているが、同名の.cmdファイルが存在するとWindowsの場合のみ自動でこちらが実行される。

docker-compose.ymlの編集

docker-compose.ymlのbuildを以下のようにする。

 1version: '3'
 2
 3services:
 4  dev:
 5    build:
 6      context: .
 7      args:
 8        USERNAME: $USERNAME_
 9        UID: $UID
10        GID: $GID
11    stdin_open: true
12    # (略)

ポイントは、先程作成された.envの内容をargsDockerfileに渡すことである。

これにより、Dockerfile内でホスト側のUID, GID, ユーザー名でユーザーを作成できるようになる。

Dockerfileの編集

Dockerfileを以下のようにする。

 1FROM golang:1.19-bullseye
 2
 3# (中略)
 4
 5# 以下を追加
 6ARG USERNAME=root
 7ARG UID
 8ARG GID
 9
10# 中でrootに昇格できるようにsudoを入れる
11RUN apt -y update \
12 && apt -y install sudo
13
14# ホストと同一のUID・GID・ユーザー名でユーザー作成
15RUN [ -n "$UID" ] && (groupadd --gid $GID $USERNAME \
16 && useradd --uid $UID --gid $GID -m $USERNAME \
17 && echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USERNAME \
18 && chmod 0440 /etc/sudoers.d/$USERNAME \
19 && chown -R $UID:$GID /go)
20
21USER $USERNAME
22
23# (略)

ユーザー等を作成する前に[ -n "$UID" ]でユーザーを作成する必要があるかどうかテストしている。 (Windowsの場合はUIDの内容が空になっているため、このコマンドは失敗し後続のスクリプトはスキップされる) また、Windowsの場合はUSERNAMEがデフォルト値のrootになるため、 最後のUSERコマンドでrootが指定される。

同一UIDのユーザーがコンテナ内に既に存在する場合

上記のスクリプトはコンテナ内に一般ユーザーが存在しない場合にはうまくいくが、 node:alpineのように一般ユーザーが既に存在する場合、 UID・GIDが衝突してしまいスクリプトが失敗する場合がある。

一般ユーザーがコンテナ内に存在する場合、以下のようにして既存ユーザーを使うようにしたほうが良い。

例: コンテナ内のnodeユーザーを使用する

  1. getuidでnodeユーザーを指定する
1#!/bin/bash
2echo "UID=$(id -u $USER)" > .devcontainer/.env
3echo "GID=$(id -g $USER)" >> .devcontainer/.env
4echo "USERNAME_=node" >> .devcontainer/.env
  1. DockerfileでUID・GIDをホストと同じになるように変更
 1FROM node:alpine
 2
 3# (中略)
 4
 5# 以下を追加
 6ARG USERNAME=root
 7ARG UID
 8ARG GID
 9
10# alpineイメージの場合、usermod等もインストールする必要あり
11RUN apk add --no-cache sudo shadow
12
13# 既存のユーザーのUID・GIDを変更
14RUN [ -n "$UID" ] && (groupmod --gid $GID $USERNAME \
15 && usermod --uid $UID --gid $GID $USERNAME \
16 && echo "$USERNAME ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/$USERNAME \
17 && chmod 0440 /etc/sudoers.d/$USERNAME \
18 && chown -R $UID:$GID /home/$USERNAME)
19
20USER $USERNAME

動作確認

以上で設定ファイルの編集は完了なので、実際にRebuild and Reopen in Containerを実行してコンテナが一般権限で実行されるかを確認する。

なお、initializeCommandを指定した弊害としてコンテナ内ターミナルがデフォルトで立ち上がらなくなる。 ターミナル右上の「+」ボタンからコンテナ内ターミナルを立ち上げると、以下のように一般ユーザーで実行されるはずである。

1a82773d0e42b:/workspace$ id
2uid=1000(user) gid=1000(user) groups=1000(user)

関連記事