APIとフロントのアプリ両方が含まれたdocker-composeプロジェクトに VSCode Remote Containersを導入する方法


# 背景

- 社内で新規プロジェクトを立ち上げる上で、Visual Studio Code(以下 VSCode)のプラグインである[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)を使用して開発したかった。
  - docker利用で、アプリケーションの動作環境はチーム内で共通化できるが、**エディタの動作環境の構築は各メンバーに任されたまま**である。今回のプロジェクトでは、eslintやrubocopなどの構文チェックやコード補完ツールを複数個入れることが序盤から決まっていたので、[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)を入れることで、エディタの動作環境もまとめてdockerでメンバーと共有して効率化したかった。
- 今回のプロジェクトでは、**同一プロジェクトの中で、APIとフロントを別々のレポジトリとしてgit管理しており、それらを同一のdocker-compose.ymlで管理しているという条件**だった。この場合のRemote - Container設定の書き方でかなり悩んだ。

# 想定読者
- すでに `docker-compose.yml` で管理しているプロジェクトがあり、そのプロジェクトの中に複数のアプリケーション（例：APIとフロント）が含まれている
- それぞれのアプリケーションを Remote - Containersで開発できるようにしたい

# 修正前のプロジェクト構成

## フォルダ構成
> project-docker
┣ project-api (railsのレポジトリ)
┃ ┣ app
┃ ┣ ...
┃ ┗ Gemfile
┣ project-front (Next.jsのレポジトリ)
┃ ┣ .next
┃ ┣ ...
┃ ┗ package.json
┣ docker-compose.yml
┣ Dockerfile.backend
┗ Dockerfile.frontend

`project-docker` 直下で `docker-compose up` を叩くと、APIとフロント（とDB）が同時に起動してハッピー、という構成。

## docker-compose.yml
`docker-compose.yml` が 以下。ひとまず`services`が少なくとも以下の3つがある。

- db
- backend
- frontend

※本当はredisとかsmtpとかもあったけどこの記事の趣旨には関係ないから除外した

```yml:docker-compose.yml(一部省略)
services:
  db:
    image: postgres:11.5
    ports:
      - "5432:5432"
  backend:
    build:
      context: .
      dockerfile: Dockerfile.backend
    volumes:
      - ./project-api:/project-api
    command: /bin/sh -c "rm -f /api-okapi/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    ports:
      - "3000:3000"
    depends_on:
      - db
  frontend:
    build:
      context: .
      dockerfile: Dockerfile.frontend
    command: npm run dev
    volumes:
      - ./project-front:/project-front
    ports:
      - "8080:8080"
```

# Remote - Containers の導入手順
- 基本的には、各レポジトリ(project-api, project-front)の下に Remote - Containers の設定ファイルを入れるディレクトリ `.devcontainer` を作成し、その下に `devcontainer.json` と `docker-compose.extend.yml` の２つの設定ファイルを作成するのが目標になります。

## 前提
- VScodeおよびdockerはインストール済み
- VScodeの拡張機能ペインから、[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) をインストール済み
  - インストールすると、VScodeの画面左下にRemote - Containersのアイコンが追加されます

## 手順
api側を例に解説します。front側は同じことをやり直してください。

### 1. `.devcontainer` を自動作成する
1. 「file(ファイル)」の「open workspace(ワークスペースを開く)」から、 `project-docker` を開く
1. 画面左下にRemote - Containersのアイコンをクリックし、`Reopen in Container` を選択
1. `From docker-compose.yml` を 選択
1. select a service で `backend` を選択
1. `project-docker` 直下に、 `.devcontainer` ディレクトリと、 その下に `devcontainer.json` と `docker-compose.yml` の２つの設定ファイルが作成されるので、これを `project-api` の下に移動させる

### 2. `.devcontainer/devcontainer.json` の修正
自動的に作られた直後の `.devcontainer/devcontainer.json` は以下です。

```jsonc:devcontainer.json
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml.
{
	"name": "Existing Docker Compose (Extend)",

	// Update the 'dockerComposeFile' list if you have more compose files or use different names.
	// The .devcontainer/docker-compose.yml file contains any overrides you need/want to make.
	"dockerComposeFile": [
		"../docker-compose.yml",
		"docker-compose.yml"
	],

	// The 'service' property is the name of the service for the container that VS Code should
	// use. Update this value and .devcontainer/docker-compose.yml to the real service name.
	"service": "backend",

	// The optional 'workspaceFolder' property is the path VS Code should open by default when
	// connected. This is typically a file mount in .devcontainer/docker-compose.yml
	"workspaceFolder": "/workspace",

	// Set *default* container specific settings.json values on container create.
	"settings": {
		"terminal.integrated.shell.linux": null
	},

	// Add the IDs of extensions you want installed when the container is created.
	"extensions": []

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	// Uncomment the next line if you want start specific services in your Docker Compose config.
	// "runServices": [],

	// Uncomment the next line if you want to keep your containers running after VS Code shuts down.
	// "shutdownAction": "none",

	// Uncomment the next line to run commands after the container is created - for example installing curl.
	// "postCreateCommand": "apt-get update && apt-get install -y curl",

	// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
	// "remoteUser": "vscode"
}
```

これを以下に書き換えます。

```jsonc:devcontainer.json(修正部分のみ)
{
	"name": "project-api-docker", // 適当にナイスな名前にする
	"dockerComposeFile": [
		"../../docker-compose.yml", // フォルダ移動したのでパスを修正
		"docker-compose.extend.yml" // .extend をつける （動作するだけなら不要だけど私はわかりやすさを重視して名前を変えました）
	],
	"runServices": [
		"db",
		"backend",
		// stmpなど他のサービスがあれば追加。 "frontend" を 含めないのが重要。空だと、デフォルトで全てのserviceを起動する。
	],
	"workspaceFolder": "/workspace/project-api/", // workspaceを project-api にする
}
```

### 3. `docker-compose.yml` の修正

自動的に作られた直後の `.devcontainer/docker-compose.yml` は以下です。

```yml:docker-compose.yml
version: '3'
services:
  # Update this to the name of the service you want to work with in your docker-compose.yml file
  backend:
    # If you want add a non-root user to your Dockerfile, you can use the "remoteUser"
    # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, 
    # debugging) to execute as the user. Uncomment the next line if you want the entire 
    # container to run as this user instead. Note that, on Linux, you may need to 
    # ensure the UID and GID of the container user you create matches your local user. 
    # See https://aka.ms/vscode-remote/containers/non-root for details.
    #
    # user: vscode

    # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 
    # folder. Note that the path of the Dockerfile and context is relative to the *primary* 
    # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile"
    # array). The sample below assumes your primary file is in the root of your project.
    #
    # build:
    #   context: .
    #   dockerfile: .devcontainer/Dockerfile
    
    volumes:
      # Update this to wherever you want VS Code to mount the folder of your project
      - .:/workspace:cached

      # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details.
      # - /var/run/docker.sock:/var/run/docker.sock 

    # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust.
    # cap_add:
    #   - SYS_PTRACE
    # security_opt:
    #   - seccomp:unconfined

    # Overrides default command so things don't shut down after the process ends.
    command: /bin/sh -c "while sleep 1000; do :; done"
```

まず、**ファイル名を `.devcontainer/docker-compose.extend.yml` に変更**しておきます。
これは、`.devcontainer/docker-compose.extend.yml` は、`project-docker` 直下の `docker-compose.yml` の一部要素を上書きしているだけの存在であることをわかりやすくするためです。

そして、`.devcontainer/docker-compose.extend.yml` の中身を以下に書き換えます。

```yml:docker-compose.extend.yml(修正部分のみ)
services:
  backend:    
    volumes:
      - .:/workspace:cached
      - ~/.gitconfig:/root/.gitconfig # ローカルの.gitconfig設定をコンテナ内に共有するため、コンテナ側のホームディレクトリ(この場合は/root)にマウントします。
      - ~/.ssh:/root/.ssh # ローカルの.ssh 設定ををコンテナ内に共有するため(以下略)
      - /var/run/docker.sock:/var/run/docker.sock # 一応アンコメントしてるけど必要性がよくわかってない。誰か教えてください・・・
    command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0' ; while sleep 1000; do :; done" 
```

- 特に `command` の修正が重要。元の記述のままだと、親玉の `docker-compose.yml` の `backend` の `command` (今回の場合はrailsのサーバー起動コマンド) を上書きしているので、サーバーが起動しません。 元の記述を持ってきて、最後に `while sleep 1000; do :; done` を加えます。
- circleci系のコンテナなど、 コンテナ側のユーザがrootではないコンテナを使っている場合は、"environment" で環境変数 HOME の指定が必要だそうです。詳しくは[こっちの記事](https://qiita.com/yito/items/1edd82414dfc2ab2a7bc#devcontainerdocker-composeextendyml)を参照してください。

### 4. Remote - Containers で開く
1. 設定が完了したので、「file(ファイル)」の「open workspace(ワークスペースを開く)」から、このリポジトリの直下にcloneした api-okapi (or front-okapi) を開きます。
1. 左下のアイコンをクリックし、 `Remote-Containers: Reopen in Container` を選択します。
1. 自動的にcontainerが起動し、コンテナ内でVSCodeが開きます。（ビルドが必要な場合、長い時間がかかります）

### front側の相違点
- front側を設定するときは、 `runServices` の中を `frontend` のみにしてください。

```jsonc:devcontainer.json(修正部分のみ)
{
	"runServices": [
		"frontend"
		// "db",
		// "backend",
	],
}
```

- このように、backend側とfrontend側で起動するserviceを振り分けることで、同時にbackend側とfrontend側をRemote - Containersで起動しても、それぞれのレポジトリで起動するserviceが衝突しないので安心。（必要ないかもしれない。誰か詳しい人教えてください）
- 親の `docker-compose.yml` の方で `depends_on` を指定していると、 `runServices` の設定から外しても起動するのでご注意ください。

# 導入後の設定

## VScodeのエディタ設定の変更
- 例: .tsファイルを保存する度に、ESLintでフォーマットが走るようにする
- 各レポジトリの `.devcontainer/devcontainer.json` の `settings` の中を修正してください。
  - ここが `.vscode/settings.json` の代わりになります。

```jsonc:.devcontainer/devcontainer.json
{
	"settings": {
		"editor.codeActionsOnSave": {
			"source.fixAll.eslint": true // ファイル保存時にESLintでフォーマット
		}
		...
	},
}
```

## VScodeのプラグイン設定の変更
- 例: eslint の VScodeプラグインを入れる
- 各レポジトリの `.devcontainer/devcontainer.json` の `extensions` の中を修正してください。
  - 各プラグインのIDは、VScodeの拡張機能ペインにて、該当のプラグインを右クリックして「拡張機能IDをコピーする」で取得できます。
- 通常通り、VScodeの拡張機能ペインからプラグインを追加することもできますが、この場合、他の開発者の環境には追加されないっぽい？。

```jsonc:.devcontainer/devcontainer.json
{
	"extensions": [
		"dbaeumer.vscode-eslint",
		...
	],
}
```

## bash設定の変更
- 例: gitコマンドの補完スクリプトを入れる
- `.bashrc` などのファイルを準備した上で、`docker-compose.yml` の `volume` を修正して、container側のホームディレクトリ(この場合は`/root`)直下にマウントしてください。
  - 以下では、 `project-docker` の 下に `bash-config` フォルダを用意して、親玉の方の `docker-compose.yml` でマウントの設定をしてます。

```yml:docker-compose.yml
frontend:
  volumes:
    - ./bash-config/.bashrc:/root/.bashrc
    - ./bash-config/.git-completion.bash:/root/.git-completion.bash
    - ./bash-config/.git-prompt.sh:/root/.git-prompt.sh
    ...
```
- 各レポジトリでbashの設定を変えたい場合は、 各レポジトリ以下に `bash-config` フォルダを用意して `docker-compose.extend.yml` の設定を変えるといいでしょう。

# 参考
## VSCode Remote Container関連
- [VSCode Remote Containerが良い](https://qiita.com/d0ne1s/items/d2649801c6f804019db7)
- [Dockerで立ち上げた開発環境をVS Codeで開く!](https://qiita.com/yoskeoka/items/01c52c069123e0298660)
- [VS Code Remote Development で Docker 開発環境を利用する](https://qiita.com/yito/items/1edd82414dfc2ab2a7bc)

## 導入後の設定関連
- [TypeScriptのプロジェクトにESLintとPrettierを導入する方法（+VSCodeでの設定）](https://qiita.com/yuma-ito-bd/items/cca7490fd7e300bbf169)
- [「Git補完をしらない」「git statusを1日100回は使う」そんなあなたに朗報【git-completionとgit-prompt】](https://qiita.com/varmil/items/9b0aeafa85975474e9b6)