DevContainer で Ruby 開発する際に厄介な BUNDLE_APP_CONFIG

## TL;DR

- Docker 公式の ruby イメージでセットされている環境変数 `BUNDLE_APP_CONFIG` は DevContainer 内での開発において邪魔になることがある
- `BUNDLE_APP_CONFIG` の影響を無くしたい場合は `devcontainer.json` に以下を記述すると良い

```json:.devcontainer/devcontainer.json
{
  // ...
  "remoteEnv": {
    "BUNDLE_APP_CONFIG": null
  },
  // ...
}
```

## 厄介な BUNDLE_APP_CONFIG

VSCode Remote - Containers での Ruby 開発では大抵の場合、[Docker 公式の ruby イメージ](https://hub.docker.com/_/ruby) をベースイメージにして DevContainer を作ることになる[^1]かと思いますが、このイメージには厄介な問題があります。

それが環境変数 `BUNDLE_APP_CONFIG` です。
Dockerfile 内で `ENV` 命令によりデフォルトで `/usr/local/bundle` に設定されています。

`BUNDLE_APP_CONFIG` は bundler のアプリケーションローカルな設定 (`bundle config set --local` した際に作られる設定) をどこに保存するかを指定するための環境変数です。
この環境変数が設定されていない場合、bundler はアプリケーションルートの `.bundle/` 以下に設定を作成します。

さて、話を移して VSCode の DevContainer で Ruby 開発をする場合、インストールした gem を永続化しつつコンテナにマウントするには以下のようにワークスペース内の `vendor` ディレクトリ等に突っ込んでおきたいところです。[^2]

```console
$ bundle config set --local path vendor/bundle
```

この設定により path 設定が記述された設定ファイルが通常は `.bundle/config` として保存されるはずです。ローカル設定にしたことでワークスペース内に設定ファイルが作られ、コンテナを破棄しても再設定は不要となります。
しかし、前述のように Docker 公式の ruby イメージでは `BUNDLE_APP_CONFIG` が設定されてしまっているため、このローカル設定は `/usr/local/bundle/config` に保存されてしまいます。

`bundle config` で確認してみると以下のような感じです。

```console
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set for your local app (/usr/local/bundle/config): "vendor/bundle"

app_config
Set via BUNDLE_APP_CONFIG: "/usr/local/bundle"

silence_root_warning
Set via BUNDLE_SILENCE_ROOT_WARNING: true
```

これでは Rebuild などによりコンテナを破棄した際に設定が失われてしまうため、都度 `bundle config ...` を実行するか、`/usr/local/bundle` をホストからマウントするようにするなどの工夫が必要になってしまいます。

スマートに解決するには `BUNDLE_APP_CONFIG` 自体を何とかした方が良いでしょう。

ちなみに公式イメージで `BUNDLE_APP_CONFIG` が設定されているのは上記の逆の考え方で、「ホスト側の `.bundle/config` がコンテナ内に影響を与えないようにするため」のようです[^3]。

__2020/09/30 追記__
コンテナ内でしか作業しない (= ホストと `.bundle/` や `vendor/` を共有する必要がない) のであれば `BUNDLE_APP_CONFIG` はそのままにして別途作成した volume を `/usr/local/bundle` にマウントした方が良さそうです。特に Docker for Mac では bind mount 時の Disk I/O が遅いため、ワークスペース内に gem を入れる設定だと `bundle install` 等に時間がかかってしまう可能性があります。

## 対策

### × `BUNDLE_APP_CONFIG` に空文字を設定する

[公式ドキュメント](https://github.com/docker-library/docs/blob/master/ruby/content.md#image-assumptions) に記載されている方法です。

> The environment variables we set are canonically listed in the above-linked Dockerfiles, but some of them include GEM_HOME, BUNDLE_PATH, BUNDLE_BIN, BUNDLE_SILENCE_ROOT_WARNING, and BUNDLE_APP_CONFIG.
> 
> If these cause issues for your use case (running multiple Ruby applications in a single container, for example), setting them to the empty string should be sufficient for undoing their behavior.

結論から言うとこの方法ではうまくいきません。
bundler は `BUNDLE_APP_CONFIG` の有無だけを見ている[^4]ので、空文字が設定されている場合はカレントディレクトリ直下に `config` ファイルが作成されます。

実験してみましょう。
DevCotainer 用の Dockerfile で `BUNDLE_APP_CONFIG` に空文字をセットします。

```dockerfile:devcontainer/Dockerfile
FROM ruby:2.7

# 空文字を設定
ENV BUNDLE_APP_CONFIG=

# 以下略
```

DevContaier をビルドして中に入ってみます。
ターミナル上ではなぜか `BUNDLE_APP_CONFIG` が消えて問題なく動作します (理由は不明ですが VSCode の実装の問題かもしれません)。

```console
$ bundle config
Settings are listed in order of priority. The top value will be used.
silence_root_warning
Set via BUNDLE_SILENCE_ROOT_WARNING: true
$ bundle config set --local path vendor/bundle
$ bundle config
Settings are listed in order of priority. The top value will be used.
path
Set for your local app (/workspace/sample_app/.bundle/config): "vendor/bundle"

silence_root_warning
Set via BUNDLE_SILENCE_ROOT_WARNING: true
```

問題となるのはデバッグ時です。
デバッガ (`rdebug-ide`) を Gemfile で管理し、`bundle install` でインストールしているケースを想定します。
以下のような `launch.json` になるはずです。

```json:.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Local File",
      "type": "Ruby",
      "request": "launch",
      "program": "${workspaceFolder}/main.rb",
      "useBundler": true
    }
  ]
}
```

デバッグを実行すると DEBUG CONSOLE に以下のように必要な gem が見つからない旨のエラーが出てしまいます。

```console
bundler: failed to load command: rdebug-ide (/usr/local/bundle/bin/rdebug-ide)
Bundler::GemNotFound: Could not find concurrent-ruby-1.1.7 in any of the sources
  /usr/local/lib/ruby/2.7.0/bundler/spec_set.rb:86:in `block in materialize'
  /usr/local/lib/ruby/2.7.0/bundler/spec_set.rb:80:in `map!'
  /usr/local/lib/ruby/2.7.0/bundler/spec_set.rb:80:in `materialize'
  /usr/local/lib/ruby/2.7.0/bundler/definition.rb:170:in `specs'
  /usr/local/lib/ruby/2.7.0/bundler/definition.rb:237:in `specs_for'
  /usr/local/lib/ruby/2.7.0/bundler/definition.rb:226:in `requested_specs'
  /usr/local/lib/ruby/2.7.0/bundler/runtime.rb:101:in `block in definition_method'
  /usr/local/lib/ruby/2.7.0/bundler/runtime.rb:20:in `setup'
  /usr/local/lib/ruby/2.7.0/bundler.rb:149:in `setup'
  /usr/local/lib/ruby/2.7.0/bundler/setup.rb:20:in `block in <top (required)>'
  /usr/local/lib/ruby/2.7.0/bundler/ui/shell.rb:136:in `with_level'
  /usr/local/lib/ruby/2.7.0/bundler/ui/shell.rb:88:in `silence'
  /usr/local/lib/ruby/2.7.0/bundler/setup.rb:20:in `<top (required)>'
```

これは、デバッグ用のサブプロセスでは `BUNDLE_APP_CONFIG=""` が効いているために `.bundle/config` が参照されず `path` 設定が読み込まれないためです。

### △ `BUNDLE_APP_CONFIG` に `.bundle` のパスを設定してしまう (Dockerfile)

例えば以下のように Dockerfile などでローカル設定用の `.bundle` の絶対パスを指定してしまう方法です。

```dockerfile:devcontainer/Dockerfile
ENV BUNDLE_APP_CONFIG=/workspace/.bundle
```

この方法であれば大体うまくいきますが、モノレポに複数のプロジェクトを突っ込んでいるケース (`Gemfile` が複数あるケース) には対応できません。

### △ `BUNDLE_APP_CONFIG` に `.bundle` のパスを設定してしまう (VSCode 設定)

例えば上記のデバッグがうまくいかない問題は以下のように解決できます。

```json:.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Local File",
      "type": "Ruby",
      "request": "launch",
      "program": "${workspaceFolder}/main.rb",
      "useBundler": true,
      "env": {
        "BUNDLE_APP_CONFIG": "${workspaceFolder}/.bundle"
      }
    }
  ]
}
```

この方法であれば [Multi-root Workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) にすることでモノレポのケースにも対応することができます。
ただしこの設定だけではデバッグ用のサブプロセスにしか有効にならないので、必要に応じて `settings.json` のターミナル設定や `tasks.json` にも同様の設定を記述する必要があります。
また、何らかの理由で `BUNDLE_APP_CONFIG` を設定して使っている人と `launch.json` 等の設定を共有することが難しくなります。

### 〇 devcontainer.json の設定で `BUNDLE_APP_CONFIG` を削除する

下記のような設定を `devcontainer.json` に追加することで、環境変数を空にするのでも上書きするのでもなく削除 (unset) することができます。

```json:.devcontainer/devcontainer.json
{
  // ...
  "remoteEnv": {
    "BUNDLE_APP_CONFIG": null
  },
  // ...
}
```

この方法であればコンテナ内の VSCode プロセス及びそのサブプロセス全てで `BUNDLE_APP_CONFIG` が削除されます。
個人的にはこの方法が最も副作用が少なそうなのでおすすめです。

[^1]: `mcr.microsoft.com/vscode/devcontainers/ruby` もベースはこのイメージです
[^2]: 余談ですが `bundle install` の `--path` オプションは非推奨になったんですね…最近知りました
[^3]: https://github.com/docker-library/ruby/issues/129
[^4]: 2020年9月現在の最新 v2.4.1 ではこうなっています