【まとめ】VSCode + Dockerを用いた開発環境

#**はじめに**
今日は、VSCode + Dockerで構築する自分の開発環境に関して、総まとめしてみたいと思います。自分の気に入る開発環境をどのように構築しようか色々検証しましたが、その試行錯誤している様子はこちらです。

https://qiita.com/829yasubee/items/75b03a96cb950c569ef1

https://qiita.com/829yasubee/items/2eaf1dc517c1f3f7b64b

https://qiita.com/829yasubee/items/e556293639ae5bcad6cb

https://qiita.com/829yasubee/items/d336c26f3a5393a264ac

https://qiita.com/829yasubee/items/68aabe197a47202c8688

https://qiita.com/829yasubee/items/a246e53d1cde3568e66c

そして、試行錯誤を重ねた結果、次のような構成に落ち着きました。本日はそれを紹介しつつ、その後の気づきを覚え書きしたいと思います。

#**環境周りの情報**
とりあえず、自分の開発環境は下図のような構成で落ち着きました。
![図解1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/2bdecba5-d2bc-5471-fd06-173253aa720f.png)

:::note warn
各仮想マシンのストレージ容量は構成時に設定した最大値で、実容量ではない事に注意。現時点では、Windows10は40GB、Ubuntu Desktopは10GB程のサイズです。ですので、iMacのストレージ容量はまだまだ余力があります。
:::

###**ハードウェアとOS環境**
灰色部分はハードウェアとOSを表しています。ここに関しては、人によって色々なパターンが考えられると思います。例えば、仮想マシンではなく物理マシンでも良いですし、Windows10やmacOS上でスタンドアロンの構成にしても良いと思います。
私がこの構成にしたのは、Linux上でDocker環境を構築する方が実践に即している気がしたからです。この構成に決めてからは、Windows10仮想マシンからはDocker DesktopもWSL2もアンインストールしてしまいました。また、Remote-SSHやRemote-Containersを使う事で、今まで使っていたSSHクライアントソフトやFTPクライアントソフトもアンインストールしましたので、Windows10仮想マシンにインストールしているソフトはVSCodeとDBクライアントソフトくらいです。とてもスッキリしました。DBクライアントもVSCode拡張機能で良いの無いかな。
Remote-SSHは所謂SSHクライアントツールなので、個人的に契約しているレンタルサーバーにも接続できるようにし、VSCodeで一元管理するようにしました。

:::note info
Windows10仮想マシンからDocker Desktopをアンインストールした事で、結果的にParallels DesktopはStandard Editionのままで良かった事になりました。でも、Pro Editionの方が仮想マシンのメモリ上限を引き上げられるし、Parallels Accessというリモートアクセスソフトが無料で付いてくるので特に後悔はしていません。値段もそんなに差がありませんでしたので。
:::

###**Dockerとコンテナ環境**
青色と黄色部分はDockerとそのコンテナを表しています。Ubuntu Desktop仮想マシン上に構築しました。後でご紹介しますが、コンテナ部分はUbuntu Server単体のコンテナとWordPressのコンテナを作成しました。今後も用途に応じて増やしたり減らしたりする予定ですが、そういう事をしても土台部分は一切汚れないのがDockerの良いところですね。Docker Hubに関しては、イメージのダウンロードくらいしか活用できていません。

###**VSCode環境**
紫色とオレンジ色部分はVSCodeとその拡張機能を表しています。拡張機能としては、ローカル側に"Remote-SSH"と"Remote-Containers"を、SSH接続先側には"Docker"をそれぞれインストールしています。前者2つの拡張機能はグローバルタイプなのでSSH接続先側でも利用可能です。

今回構築した環境の全体像に関しては、以上です。

#**この環境の維持・管理について**

###**性能不足になってきた場合**
Windows10やUbuntu Desktop仮想マシンの構成を増強するのが基本対応となります。それでもダメになってきた場合には、土台部分であるMacの買い替えとなります。例えば、メモリやストレージ容量不足になった場合には、メモリをモリモリにしてストレージを1TBモデルにするとかです。買い替えに伴う環境の再構築も比較的簡単で、macOSをセットアップ後、Parallels Desktopをインストールし、あらかじめバックアップしておいた、Windows10とUbuntu Desktop仮想マシンのファイルをコピーするだけです。

###**macOSのバージョンアップ**
今までは比較的すぐ、バージョンアップしていましたが、ここは慎重にならざるを得ません。少なくとも世間の様子を探りつつ、Parallels Desktopの正式対応を待ってから、バージョンアップする必要があります。更に、TimeMachineでのバックアップもお忘れなく。

###**Windowsのバージョンアップ**
近々、実際に、Windows11にバージョンアップする予定ですが、特に大きな問題は無さそうです。Parallels DesktopもWindows11に正式対応しましたし、Windows10からのバージョンアップ要件をチェックツールでチェックしましたが、無料でバージョンアップ可能と判定されましたので、所定の手順でWindows11にバージョンアップするだけです。しかも、先程言及しましたが、Windows10は非常にシンプル構成になったので、OSセットアップ後、VSCodeとDBクライアントのインストールをし、VSCodeの設定と拡張機能のインストールをすればOKです。この辺はVSCodeとMicrosoftアカウントを同期すると復元されるような機能があるようですので、それを使えばもっと簡単になるかもしれません。また、Windows10仮想マシンを再構築し直す場合であっても、SSH絡みで"Remote-SSH"のConfigファイルと公開鍵認証の秘密鍵の移行（ファイルをコピーするだけ）をプラスアルファで作業するだけです。

###**Ubuntu Desktopのバージョンアップ**
これはあんまりやった事がないのですが、やり方が分からなければ、私は別仮想マシンを作成し、そこにバージョンアップしたUbuntu Desktopをインストールすれば良いかなと思っています。Docker環境のコンテナ関連は、全て一般ユーザーのhomeディレクトリ配下にまとめてあり、永続化したいコンテナ内のデータに関してもバインドマウントしてあります。その為、新しいUbuntu仮想マシンにそれを丸ごと移行（コピー）し、移行先でDocker Compose Upすれば再構築できます。

という感じで、この構成は今後発生するであろう色々な環境の維持・管理もしやすいかなと思っています。

#**主な改善点について**
前述した通り、試行錯誤に関する投稿後、VSCodeとDockerに関して、色々な問題点が発生したので、改善した点について触れたいと思います。
その前に、一般ユーザーのhomeディレクトリ配下にまとめてあるDocker関連ファイルのツリー構造を紹介しておきます。最終的に次のようにしました。
![スクリーンショット1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/56d52e05-f2be-1250-612f-8d7ebe2222d9.png)
変更したディレクトリ名はさておき、大きな変更ポイントは主に３つです。

1. docker-compose.ymlと.envファイルをコンテナの起点ディレクトリ直下に移動
1. buildディレクトリを設け、Dockerfileを新たに定義
1. dataディレクトリを設け、そこに永続化したいコンテナ内データをバインドマウント

どちらの起点ディレクトリ配下も基本構成は同じなので、"blog"起点ディレクトリ側を例に紹介させて頂きます。
定義したdocker-compose.yml、Dockerfile、.env、devcontainer.jsonを載せておきます。

:::note info
.envファイルの定義内容はご自身の環境に合わせて読み替えて下さい。
:::

```yml:docker-compose.yml
version: '3'
services:
  db:
    build:
      context: ./build/db/
      dockerfile: Dockerfile
      args:
        USER_UID: ${USER_UID}
        USER_NAME: ${USER_NAME}
        GROUP_GID: ${GROUP_GID}
        GROUP_NAME: ${GROUP_NAME}
    container_name: blog_db
    hostname: blog_db
    networks:
      - net
    volumes:
      - ./data/mysql:/var/lib/mysql
    ports:
      - ${DB_FORWARD_PORT}:3306
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
  app:
    depends_on:
      - db
    build:
      context: ./build/app/
      dockerfile: Dockerfile
      args:
        USER_UID: ${USER_UID}
        USER_NAME: ${USER_NAME}
        GROUP_GID: ${GROUP_GID}
        GROUP_NAME: ${GROUP_NAME}
    container_name: blog_app
    hostname: blog_app
    networks:
      - net
    volumes:
      - ./data/html:/var/www/html
    ports:
      - ${APP_FORWARD_PORT}:80
    restart: always
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: ${DB_DATABASE}
      WORDPRESS_DB_USER: ${DB_USER}
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
networks:
  net:
    name: blog_net
```

```Dockerfile:build/app/Dockerfile
FROM wordpress:latest

ARG USER_UID
ARG USER_NAME
ARG GROUP_GID
ARG GROUP_NAME

RUN groupadd -g ${GROUP_GID} ${GROUP_NAME} \
  && useradd -m -s /bin/bash -u ${USER_UID} -g ${GROUP_GID} ${USER_NAME} \
  && apt-get update \
  && apt-get install -y sudo \
  && echo "${USER_NAME} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/${USER_NAME} \
  && chmod 0440 /etc/sudoers.d/${USER_NAME}

USER ${USER_NAME}
```

```Dockerfile:build/db/Dockerfile
FROM mysql:5.7

ARG USER_UID
ARG USER_NAME
ARG GROUP_GID
ARG GROUP_NAME

RUN groupadd -g ${GROUP_GID} ${GROUP_NAME} \
  && useradd -m -s /bin/bash -u ${USER_UID} -g ${GROUP_GID} ${USER_NAME} \
  && apt-get update \
  && apt-get install -y sudo \
  && echo "${USER_NAME} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/${USER_NAME} \
  && chmod 0440 /etc/sudoers.d/${USER_NAME}
```

```text:.env
APP_FORWARD_PORT={Webブラウザからのアクセス用フォワードポート}
DB_FORWARD_PORT={DBクライアントからのアクセス用フォワードポート}

DB_ROOT_PASSWORD={MySQLのrootユーザーパスワード}
DB_DATABASE={MySQLのWordPress用のデータベース名}
DB_USER={MySQLの作業用ユーザーのユーザー名}
DB_PASSWORD={MySQLの作業用ユーザーのパスワード}

USER_UID={DockerホストOSの一般ユーザーのUID}
USER_NAME={DockerホストOSの一般ユーザーのユーザー名}
GROUP_GID={DockerホストOSの一般ユーザーのGID}
GROUP_NAME={DockerホストOSの一般ユーザーのグループ名}
```

```json:.devcontainer/devcontainer.json
{
  "name": "blog",
  "dockerComposeFile": "../docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/var/www/html",
  "extensions": [
    "bmewburn.vscode-intelephense-client"
  ],
  "remoteUser": "${localEnv:USER}"
}
```

###**1. docker-compose.ymlと.envファイルの配置変更**
以前の投稿では、コンテナ名をなるべく短くした方が良いと記載しました。docker composeで生成されるコンテナ名が自動ですごく長い名前になるからです。しかし、その後、docker-compose.ymlファイルの置き場所が問題である事が分かりました。Docker Composeは、生成するコンテナの名前を{起点ディレクトリ名}_{サブディレクトリ名}-{サービス名（コンテナ名）}-{通番}としてしまいます。以前の投稿では.devcontainerディレクトリ配下に配置していたので、{サブディレクトリ}部分が非常に長くなっていただけでした。起点ディレクトリ配下に移動後、コンテナ生成したら名前が短くなりました。更に言ってしまうと、docker-compose.ymlのcontainer_nameプロパティを定義すれば、コンテナ名は自分で決められます。ついでに、生成されたコンテナ内のOSホスト名もhostnameプロパティで定義しました。
![スクリーンショット2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/e0841f30-bf4f-7be5-aa00-a4a5ec58c01e.png)
![スクリーンショット3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/1a228c62-6b27-d0d4-4969-0c6be5290e55.png)
これらの点を改善して最終的に生成されたコンテナのコンテナ名は次の通りとなりました。
![スクリーンショット4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/13105ed9-883f-854c-48cf-774f991e2252.png)
同様に、ネットワークに関しても、ネットワークの識別名とネットワーク名を別々に定義する事で、定義ファイル上にネットワーク名が散在しないようにしました。
![スクリーンショット5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/b0dedfab-7f05-8ffb-1c63-782fd843e09e.png)
![スクリーンショット6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/9b23f75e-15eb-4570-aacd-898e722ce4a3.png)
生成されたネットワークのネットワーク名は次の通りとなりました。
![スクリーンショット7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/2ac3582a-fcdb-1c82-87b7-0befbc99ae81.png)

###**2. Dockerfileの定義**
試行錯誤中の以前の投稿時点では、Dockerfileを定義していませんでした。バインドマウントしたファイルパーミッションや"Remote-Containers"アクセスでのリモートユーザーやワークスペースフォルダの表示で問題が発生した事がきっかけでDockerfileを定義する事にしました。
発生した問題の根本原因を大雑把に言ってしまうと、DockerホストOSの一般ユーザーと同じ権限を持ったユーザー及びグループがリモートコンテナ側にも存在しないと、このような問題が発生します。例えば、「"Remote-Containers"アクセスした時にrootユーザーでしかアクセスできない」とか「リモートコンテナ内でファイルやディレクトリを作成した際、所有者がrootになってしまう」とかです。初めはdevcontainer.jsonのpostCreateCommandで対処しようと思いましたが、コンテナ生成後のタイミングでは遅いからなのかエラーとなってしまいました。その為、Dockfileでイメージをカスタマイズするタイミングで対処する事にしました。
Dockerfileファイルはdocker-compose.ymlのbuildプロパティで紐づいています。
![スクリーンショット8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/e9501021-7dff-2626-2ed3-4692576564fc.png)
![スクリーンショット9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/040ac911-5ab9-e31a-a066-cc69cf6efa19.png)

:::note info
紐付け時、argsプロパティを用いて、.envファイルを参照して得た環境依存情報をDockerfileへ引き渡しています。docker-compose.ymlファイルやDockerfileファイルでは、環境依存情報を直接記述せずに.envファイルを参照する事でメンテナンスしやすいようにしています。
:::

Dockerfileの定義は前述した内容を参照してほしいのですが、大まかな流れは次のようになっています。

1. Docker Hubよりイメージをダウンロード
1. docker-compose.ymlより、環境依存情報をパラメータとして取得
1. DockerホストOSの一般ユーザーと同じユーザー及びグループを作成し、sudo権限を付与
1. 作成した一般ユーザーに切替

このDockerfileの定義により、カスタマイズされたイメージが次のように生成されます。
![スクリーンショット10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/2f24874c-ffff-37a7-06ff-7104483488c0.png)
Docker Composeでは、このカスタマイズされたイメージをもとにコンテナを生成します。

:::note info
db側のDockerfileでは、USERコマンドを定義していません。理由は、バインドマウントした/var/lib/mysqlのデータベース本体のディレクトリ及びファイル群の所有者が、mysqlユーザーではなく、DockerホストOSの一般ユーザーになってしまうのを嫌った為です。
:::

###**3. ボリュームマウントからバインドマウントへ変更**
以前の投稿時は、何の迷いも無くボリュームマウントを選択していました。折角コンテナ環境を構築するのですから、全てコンテナ内部に収めた方が良いと思っていた為です。しかし、前述した「名前が長くなる問題」を改善しようとしていた時、ボリューム名も長くなってしまっていたので、失敗する度にボリュームを消していました。これがすごく面倒でしたし、本当に変更したくなった時に困るなと思いました。それがきっかけでバインドマウントに切り替えました。これなら「名前が長くなる問題」から切り離せますので。また、前述しましたが、バージョンアップ等によるバックアップの際も手順が簡単になるメリットがあります。デメリットは、DockerホストOS側から気軽にアクセスできてしまう為、誤って触って壊してしまわないか心配な事です。結局、どちらが良いのか明確な基準を自分の中で持てていませんが、今の所はバインドマウントの方が良いと思っています。
その為、現在、ボリュームマウントは１つもありません。
![スクリーンショット11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/8cf8db8a-1fb4-6014-855b-909a1c12e832.png)
バインドマウントは前述のツリー構造で言うと次の箇所です。
![スクリーンショット12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/232be2bd-1a53-f274-35f3-6ac5f5ef7e7e.png)

#**その他の改善点について**

###**devcontainer.jsonの定義内容について**
前述のDockerfileの定義により、Comopse Upで生成されるコンテナ内には、DockerホストOSの一般ユーザーと同じユーザー及びグループが作成されますので、リモートコンテナアクセス時、そのユーザーでアクセスできるようになっています。その為、その切り替え定義を加えました。ただ、ここでも環境依存情報を直接記載したくなかったので、次のように定義しました。私の試した限りでは、devcontainer.jsonファイルから.envファイルを参照する方法が見出せませんでした。そこで、localEnvプロパティを使ってDockerホストOSの環境変数"USER"を参照するようにしました。この方法がベストかは分かりませんが、現時点ではこれが私の中では最良の定義方法でした。
![スクリーンショット13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/4e520287-a61c-81ac-1ef5-4e002eb659c0.png)

###**Docker ComposeのバージョンをVer2系からVer1系へ変更**
当初、Docker ComposeはVer2系（v2.2.2）をインストールしていました。しかし、VSCodeのGUI上からCompose Up、Compose Down等を行うとエラーが発生してしまう事象に悩まされました。
![スクリーンショット14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/5c2e507f-daeb-f131-34d8-dacf8d0b90c4.png)
なぜかこの方法では、裏で実際に実行されているコマンドが「docker-compose up ...」となっており、これが原因でコマンドが見つかりませんというエラーを引き起こしていました。Ver2系からは「docker compose up ...」とハイフン無しのコマンド体系に変更されたので、VSCode側がまだこれに未対応なのかなと思いました。一応、VSCodeの設定に次のような箇所があったので、ハイフンを半角スペースに設定変更して試しましたが、結果は変わりませんでした。
![スクリーンショット15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/35ad8243-c09e-df4d-de02-d2f3fefe2ef2.png)
その為、Docker ComposeをVer1系の最終版（v1.29.2）に下げたら、エラーが発生しなくなりました。

###**"Remote - Containers"拡張機能の不具合？**
"Remote - Containers"拡張機能の「Open Folder in Container」もしくは「Reopen in Container」を使って、リモートコンテナへのアクセスとCompose Upを同時に行おうとすると、辺な名前（「VSC...」とか「SHA...」とか）のコンテナ、イメージ、ボリュームが生成されてしまいます。「SHA...」は定義が間違っていてエラーになった時に大量に生産されていた記憶があります。「VSC...」は定義が間違っているわけではなさそうです（ゴミが残ってしまうだけ？）。再現してみました。
![スクリーンショット16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/6d54f27a-e631-b34f-d3a0-9fa365c2b4ae.png)
全く同じ定義ファイルで、docker-compose.ymlファイルを右クリックして[Compose Up]するやり方だと問題ありません。
![スクリーンショット17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/ff0c2e9a-74cd-a3ce-ebcf-7bf834532f2c.png)
その後、リモートコンテナにもアクセスできます。ただ、アクセス後、"Remote - containers"拡張機能の「Rebuild Container」を実行すると、事象が再び発生します。よく分からないので、今は操作を気を付けるしかないです。
![スクリーンショット18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206277/6e526d68-9d90-773e-ff12-d0e548c8deb0.png)

#**まとめ**
"Remote - Containers"拡張機能はまだまだ不安定なところがある印象ですが、それを差し引いても有り余る魅力があります。今後の進化が非常に楽しみです。
VSCodeもまだまだ使った事が無い機能の方が多いので、次は「Remote-Container上でのデバッグ」や「GitHubとの連携」等を学んでみたいと思っています。

お疲れ様でした！