🐳

VS Code 開発コンテナー(Dev Container)で Nest.js アプリのデバッグ

2024/10/07に公開

はじめに

VS Code で Docker コンテナーを開発コンテナーとして使うためには、開発コンテナー機能に対応する Docker コンテナーへ「Visual Studio Code をアタッチする」のが手軽です。開発コンテナーがどういうものか理解して慣れるためには、この方法が便利なのですが、本格的に開発コンテナーを使いたいとなってきたら devcontainer.json ファイルを用意して、開発コンテナー専用の Docker イメージを使うのが良いです。

ここでは、VS Code 開発コンテナー(Dev Container)のための devcontainer.json を用意する方法と、開発コンテナーで Nest.js アプリのデバッグをする方法について説明します。

筆者は、単純に devcontainer.json の説明だけを読むよりは、実際に使用する開発コンテナーについて理解が深まるようにサンプルアプリも動かした方が良いと考えています。そのため、Nest.js の雛形アプリを作成して、そのソースコードを開発コンテナー内で編集したりデバッグ実行する方法と、そのために必要な VS Code のワークスペースファイルについても説明します。

必要な環境

  • Docker Desktop (Docker Engine + Docker Compose でも可)
  • Visual Studio Code
    • Docker 拡張機能
    • Dev Containers 拡張機能
  • Bash 環境(Git Bash for Windows 含む)
  • Node.js v22 環境
  • Git が使える環境

動作確認は Ubuntu 22.04 を使っています。基本的に Windows や macOS でも動作する範囲で説明しています。なお、シェルスクリプトは改行コードが LF となっている必要があります。他のファイルも Docker を使うことを考慮するなら LF としておいた方が無用なトラブルが起こりにくくなります。Windows や macOS のテキストエディタでファイルを作成する場合は注意してください。

用意するファイルの構成

今回、用意するファイルの構成は次のようになります。

dvc-nestjs-debug/ ... Nest.js をデバッグ可能な開発コンテナー環境
├── .devcontainer/
│   └── devcontainer.json
├── .gitignore
├── README.md
└── app001/ ... dvc-nestjs で使用するサンプルアプリ
   ├── (略)
   ├── app001.code-workspace
   ├── node_modules/
   ├── (略)
   └── tsconfig.json

ここでは、最初に Nest.js という Node.js のフレームワークを使うアプリの雛形を作成してから、それを開発するための VS Code 開発コンテナー環境を用意します。それから、開発コンテナー環境をカスタマイズしやすくするためのファイル構成について説明します。

なお、開発コンテナーに慣れてくると、Docker ホスト側に Node.js の環境を用意せずに開発コンテナーで Node.js 関連の作業ができるようになります。そのため、アプリの雛形を作成するところも開発コンテナーで行うこともできます。とはいえ、実際には既存のアプリ開発用のプロジェクトを開発コンテナーで開発できるように、後から開発コンテナー用のファイルを追加することも多いので、この方法を採用しています。

dvc-nestjs-debug

  1. Nest.js サンプル app001 の用意
  2. 開発コンテナー用ファイルの用意

最初に dvc-nestjs-debug フォルダーを作成して、VS Code で開きます。この VS Code の画面でターミナルを開き、そこでコマンドを実行します。

mkdir -p dvc-nestjs-debug
code dvc-nestjs-debug

Nest.js サンプル app001 の用意

最初に Nest.js をデバッグ可能な開発コンテナー環境として dvc-nestjs-debug を用意します。サンプルで使用するアプリを npm exec コマンドで作成します。 npm exec はパッケージ名を指定することで、パッケージに含まれるデフォルトのコマンドを実行することができます。

npm exec <パッケージ名>

ここではパッケージ名として Nest.js 用のコマンドラインツールを提供する @nestjs/cli を利用することにして、npm exec @nestjs/cli コマンドを実行します。このコマンドを実行すると、@nestjs/cli に含まれる nest コマンドを実行するのと同じ処理が動きます。

ここで、nest コマンドで新規アプリの作成をするには次のようにします。

nest new <アプリ名>

Node.js のアプリを開発するにあたっては、パッケージ管理システムというものを使って、使用するパッケージの管理をするのですが、その種類には npmyarn といったものがあります。nest コマンドでは、どれを選択するかを -p オプションで指定することができます。

nest new -p <使用するパッケージ管理システム> <アプリ名>

この nest コマンドを npm exec @nestjs/cli コマンドへ置き換えます。ただし、nest コマンドのパラメーターを指定するには -- の後に続ける必要があります。また、デフォルトでは対話モードで実行されるので、これを非対話モードにするために npm exec コマンドに --yes オプションも指定します。

また、アプリ名は app001 とします。

まとめると、実際に実行するコマンドは次のようになります。

npm exec @nestjs/cli -- new -p npm app001

作成されるアプリには .git ディレクトリーが用意されて Git リポジトリとなっています。普段、Git を使っていない場合は、git configuser.emailuser.name の設定をする必要があります。ここでは使っている環境に影響がないように、app001 のリポジトリにだけ、これらの設定を反映します。

git -C app001 config user.email user001@example.jp
git -C app001 config user.name user001

git コマンドを使う準備ができたら、リポジトリへ app001 のコードをコミットしておきます。

git -C app001 add .
git -C app001 commit -m "init"

以上で Nest.js サンプル app001 の用意はおしまいです。

開発コンテナー用ファイルの用意

次に開発コンテナー用ファイルを用意します。必要なファイルを手作業で作成することもできますが、ここでは VS Code であらかじめ用意されているテンプレートから作成することにします。

まず、VS Code でコマンドパレットを表示(Ubuntu Desktop なら Ctrl + Shift + P)します。それから、表示される入力欄に次のテキストを入力します。入力しはじめると一覧が表示されます。途中まで入力して、表示される一覧から同じテキストを選択します。

Dev Containers: Add Dev Container Configuration Files

「Dev Containers: Add Dev Container Configuration Files」を選択すると、次の選択肢が表示されます。

ユーザーデータフォルダーに構成を追加する
ワークスペースに構成を追加する

ここでは「ワークスペースに構成を追加する」を選択します。

次に、コンテナー構成テンプレートの選択となります。ここでは一覧の中から Node.js & TypeScript を選択します。

次に、Node.js version の選択となります。ここでは規定の 22-bookworm を選択します。

次に、機能の選択となります。ここでは、次の4つの Feature を選択(チェック)してから、「OK」をクリックします。

Common Utilities devcontainers
Docker(docker-outside-of-docker) devcontainers
Git(from source) devcontainers
Git Large File Support(LFS) devcontainers

次のオプションについては「既定値」を選択して「OK」をクリックします。

最後の「オプションのファイル/ディレクトリ」では、何も選択せずに「OK」をクリックします。

以上で、次の内容の .devcontainer/devcontainer.json ファイルが作成されます。

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
  "name": "Node.js & TypeScript",
  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
  "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
  "features": {
    "ghcr.io/devcontainers/features/common-utils:2": {},
    "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/git-lfs:1": {}
  }

  // Features to add to the dev container. More info: https://containers.dev/features.
  // "features": {},

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

  // Use 'postCreateCommand' to run commands after the container is created.
  // "postCreateCommand": "yarn install",

  // Configure tool-specific properties.
  // "customizations": {},

  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
  // "remoteUser": "root"
}

// で始まる行はコメント行です。有効になっているのは、次のキーです。

キー 説明
name 開発コンテナー名
image 使用する開発コンテナーのベースとなる Docker のイメージ名
features 使用する開発コンテナー用のフィーチャー(機能)名

このファイルを見ることで、Node.js & TypeScript という開発コンテナー名で利用できることがわかります。また、mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm という Microsoft が提供する開発コンテナー用の Docker イメージをベースにして、common-utils:2docker-outside-of-docker:1git:1git-lfs:1 の機能を追加した開発コンテナーを利用できることもわかります。

なお、features は <フィーチャーの識別子>:<バージョン> で指定するので、使用しているフィーチャーのバージョンもわかります。ここではメジャーバージョンしか指定してませんが、バージョン管理をするときは、マイナーバージョンなどを指定したいときもあります。その場合は、より詳細なバージョン指定をします。また、フィーチャーについての説明は次の URL で確認できます。

フィーチャー URL
common-utils https://github.com/devcontainers/features/tree/main/src/common-utils
docker-outside-of-docker https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker
git https://github.com/devcontainers/features/tree/main/src/git
git-lfs https://github.com/devcontainers/features/tree/main/src/git-lfs

開発コンテナー利用の準備

それでは、開発コンテナー利用の準備をしましょう。

使用する予定の mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm という Docker イメージはサイズが大きいので docker image pull で取得しておきます。

docker image pull mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm

次に、 app001 の開発がしやすくなるように、devcontainer.json の内容を変更します。

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
  "name": "dvc-nestjs-debug",
  // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
  "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
  "features": {
    "ghcr.io/devcontainers/features/common-utils:2": {},
    "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/git-lfs:1.2.3": {}
  },
  "remoteUser": "node",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
  "workspaceFolder": "/workspace",
  "mounts": [
    {
      "source": "dvc-nestjs-debug-node_modules",
      "target": "/workspace/app001/node_modules",
      "type": "volume"
    }
  ],
  "postCreateCommand": "sudo chown -R node app001/node_modules && cd app001 && npm install",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint"
      ]
    }
  }
}

追加したキーは次のとおりです。ここで使うものについて、簡単に説明します。

キー 説明
remoteUser 開発コンテナー内で使用するユーザー名
workspaceMount ワークスペースとして利用するフォルダーのマウント情報
workspaceFolder ワークスペースフォルダーのパス
mounts 開発コンテナーで使用するフォルダーのマウント情報
postCreateCommand コンテナー作成直後に実行するコマンド
customizations カスタマイズ用の設定

今回使用する Docker イメージでは、あらかじめ node というユーザーが用意されています。一般的に開発作業は一般ユーザーで作業するものなので、開発コンテナーでも基本的に root 以外のユーザーを使います。そのため、ここでは node を指定してます。

なお、このユーザーを追加する機能は common-utils フィーチャーに含まれていて、node ユーザーは管理者権限を使ったコマンド実行ができる sudo コマンドが使えるように設定されています。

開発コンテナーを開いたときに表示するデフォルトのワークスペースを workspaceFolder で指定しますが、ここでは Docker ホストにある dvc-nestjs-debug フォルダーを開発コンテナー内の /workspacebind タイプでマウントして使えるようにしてみます。

ワークスペースのマウントについては、workspaceMount で指定します。なお、開発コンテナー内で Docker ホストにあるファイルを bind タイプでマウントするときは ${localWorkspaceFolder} という VS Code のワークスペースで使える変数を利用します。この値は .devcontainer フォルダーを含むフォルダーのパスになります。

ここで app001 の開発プロジェクトでは、package.json があるフォルダーに node_modules フォルダーを用意して使用するパッケージを置きます。ここには OS 依存のファイルが置かれることもあるので、Docker ホストと開発コンテナー内とでは別に管理できた方が良いということになります。そこで、mounts を使って、開発コンテナー内の /workspace/app001/node_modules のパスに対しては、volume タイプのマウントをすることで対策します。volume タイプのマウント用には、Docker ボリュームの dvc-nestjs-debug-node_modules を指定します。このボリュームは開発コンテナー起動時に自動で作成されます。

Docker ボリュームを使うにあたって、それが自動作成される場合は、使用するユーザーがファイル操作できるように調整する必要があります。dvc-nestjs-debug-node_modules については node ユーザーが使うためのものなので、chown コマンドで調整します。この調整用の処理は、コンテナー作成直後に実行すれば良いので、postCreateCommand で指定します。

開発コンテナーを起動したら、そこで開発をするため、VS Code の設定も同時に指定できると便利です。そのために用意されているのが customizations です。この中で vscode のキーと設定用の JSON オブジェクトの指定をすることができます。設定用の JSON オブジェクトには基本設定用の settings、タスク用の tasks、実行とデバッグ用の launch、拡張機能用の extensions といったものを指定することができます。

例では、VS Code の dbaeumer.vscode-eslint 拡張機能を使うように指定をしてあります。

開発コンテナーの利用

ファイルの準備ができたら、開発コンテナーを利用してみましょう。

ターミナルから code コマンドで、devcontainer.json を含む .devcontainer フォルダーが存在するフォルダー、ここでは dvc-nestjs-debug フォルダーのパスを指定すると、dvc-nestjs-debug フォルダーを開いた VS Code が開きます。

code <dvc-nestjs-debug の絶対パス>

そのとき、右下に「コンテナーで再度開く」というボタンを含む通知が表示されます。この「コンテナーで再度開く」をクリックすると、VS Code をアタッチした開発コンテナーが起動します。

この通知が表示されない状態の dvc-nestjs-debug フォルダーを開いた VS Codeを表示している場合は、VS Code の左下の隅のスペースに >< を組み合わせたマークがあります。これをクリックして表示されるメニューから「コンテナーで再度開く」を選択します。

もしくは、VS Code でコマンドパレットを表示(Ubuntu Desktop なら Ctrl + Shift + P)します。それから、表示される入力欄に次のテキストを入力します。入力しはじめると一覧が表示されます。途中まで入力して、表示される一覧から同じテキストを選択します。

Dev Containers: Open Folder in Container

以上のいずれかの方法で、VS Code をアタッチした開発コンテナーを起動することができます。

VS Code をアタッチした開発コンテナーが起動すると、VS Code のエクスプローラーで開発コンテナー内の /workspace を開いた画面となります。また、そのフォルダーには Docker ホストの dvc-nestjs-debug フォルダー内のファイルが表示されているはずです。

次に、app001 の開発をするために必要なワークスペースの設定をするファイルを用意します。この VS Code の画面で /workspace/app001/app001.code-workspace を次の内容で作成します。

{
  "folders": [
    {
        "path": "."
    }
  ],
  "launch": {
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "npm run start:debug の起動",
            "runtimeExecutable": "npm",
            "runtimeArgs": [
                "run",
                "start:debug",
            ],
            "console": "integratedTerminal",
        },
        {
            "type": "node",
            "request": "attach",
            "name": "Debug (attach)",
            "address": "localhost",
            "port": 9229,
        }
    ]
  },
  "extensions": {
    "recommendations": [
        "dbaeumer.vscode-eslint"
    ]
  }
}

このファイルには、app001 の実行とデバッグをするための launch の指定が含まれています。configurations に含まれる name"npm run start:debug の起動" となっている設定は、npm run start:debug コマンドを実行するためのものです。name"Debug (attach)" となっている設定は、別途実行されているデバッガーへ VS Code をアタッチするためのものです。

簡単に "npm run start:debug の起動" の指定内容について説明しておくと、ここでは "type": "node" で Node.js 用の設定であること、"request": "launch" で実行やデバッグのためのプロセスを起動するリクエストを発行すること、"runtimeExecutable": "npm"npm コマンドを実行すること、"console": "integratedTerminal" でコンソールとして統合ターミナルを使うことを指定しました。なお、runtimeArgs には、runtimeExecutable で指定したコマンドへ渡すパラメーターを配列で指定します。

"name": "Debug (attach)" の指定内容については、"type": "node" で Node.js 用の設定であること、"request": "attach" で別途実行されているデバッガーへ VS Code をアタッチすること、"address": "localhost""port": 9229 でデバッガーが待機しているホストとポート番号を指定しました。

また、ここでは、開発コンテナーを使わない場合のことも考慮して、extensionsrecommendationsdbaeumer.vscode-eslint を指定しました。こちらはワークスペースで推奨する拡張機能となるので、devcontainer.json で指定したものとは違ってインストールが済んでいない場合は、別途インストールが必要になります。

ワークスペースファイルが用意できたら使ってみましょう。続けて、同じ VS Code の画面(VS Code をアタッチした開発コンテナー)でターミナルを開き、下記コマンドを実行します。

code /workspace/app001/app001.code-workspace

すると app001.code-workspace を開いた VS Code の画面が表示されます。次に、ワークスペースで指定した npm run start:debug の起動 を実行してデバッグ実行をしたいところですが、その前に、これが前提としているコマンドが動作するかを確認しておきます。

つまり、npm run start:debug コマンドの動作を、ターミナルを開いて実行して、確認しておきましょう。

npm run start:debug

デバッガが起動すると、Debugger listening という行を含むメッセージが表示されます。

[3:29:05 P] Starting compilation in watch mode...

[3:29:06 P] Found 0 errors. Watching for file changes.

Debugger listening on ws://127.0.0.1:9229/97834f8e-7699-4cce-9b02-2d01696806da
For help, see: https://nodejs.org/en/docs/inspector
[Nest] 2054  - 09/30/2024, 3:29:06 PM   LOG [NestFactory] Starting Nest application...
[Nest] 2054  - 09/30/2024, 3:29:06 PM   LOG [InstanceLoader] AppModule dependencies initialized +7ms
[Nest] 2054  - 09/30/2024, 3:29:06 PM   LOG [RoutesResolver] AppController {/}: +4ms
[Nest] 2054  - 09/30/2024, 3:29:06 PM   LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 2054  - 09/30/2024, 3:29:06 PM   LOG [NestApplication] Nest application successfully started +2ms

app001.code-workspace を開いた VS Code の画面で app001/src/app.service.ts をエディタで開き、行番号の左側をマウスでクリックしてブレイクポイントを設定します。

それから、VS Code のアクティビティバーで「実行とデバッグ」のアイコンをクリックして「実行とデバッグ」の画面にします。その画面の上部にあるドロップボックスから Debug (Attach) を選択して実行すると、このデバッグ用プロセスへ VS Code からアタッチできます。アタッチすると、npm run start:debug を実行したターミナルの出力に Debugger attached. が出力されます。

(略)
[Nest] 2054  - 09/30/2024, 3:29:06 PM   LOG [NestApplication] Nest application successfully started +2ms
Debugger attached.

これでデバッグの準備ができました。

デバッグするには、デバッガーで動作しているプロセスの処理を進める必要があります。VS Code をアタッチした開発コンテナーで、npm run start:debug を起動したターミナルとは別にターミナルを開き、curl コマンドで http://localhost:3000/ へアクセスします。

実際にコマンドを実行するときは、次のように、出力結果に改行コードを追加するためのオプションである -w'\n' をつけると見やすくなります。

curl -w'\n' http://localhost:3000/

すると、ブレークポイントで処理が一時停止して、デバッグ機能が動作していることを確認することができます。

VS Code の画面にある □ のアイコンをクリックすると、Debug (attach) は終了します。それから、npm run start:debug を起動したターミナルで Ctrl+C を入力すると、Nest.js アプリのデバッグモードでの実行が終了します。

ここでは、動作確認のために、Nest.js アプリのデバッグモードでの実行と、VS Code のデバッグ実行を別々にしてみましたが、VS Code の「実行とデバッグ」で npm run start:debug の起動 を実行することで、これらを一緒に実行することができます。

VS Code をアタッチした開発コンテナーの「実行とデバッグ」を開いて npm run start:debug の起動 を実行します。これで、Debug (Attach) を使ったときと同じように Nest.js アプリのデバッグができます。

開発作業が終了したら、VS Code をアタッチした開発コンテナーの画面を閉じます。これで、開発コンテナーは停止します。docker container ls -agrep コマンドを組み合わせると確認できます。

$ docker container ls -a | grep dvc-nestjs-debug
affc41c3c0fb   vsc-dvc-nestjs-debug-(略) Exited (0) (略)angry_pike

コンテナーを削除する場合は、docker container rm コマンドを使います。ここではコンテナーについていた名前の angry_pike を使っていますが、コンテナー ID を指定することもできます。

docker container rm angry_pike

作成された開発コンテナーのイメージとイメージの ID は下記のように確認できます。

$ docker image ls | grep debug
vsc-dvc-nestjs-debug-88436136cceb86fe3bc0cc0c87fa4c91c506537a71caf2bb8adb74dbbfaf6690-features-uid (略)
vsc-dvc-nestjs-debug-88436136cceb86fe3bc0cc0c87fa4c91c506537a71caf2bb8adb74dbbfaf6690-features (略)
$ docker image ls | grep debug | awk '{print $3}'
435c009ad924
8ed228e4eaf7

利用しなくなった開発コンテナー用のイメージについて削除したいと思うはずです。これらについて削除する場合は docker image rm コマンドを使います。

$ docker image rm 435c009ad924
Untagged: vsc-dvc-nestjs-debug-88436136cceb86fe3bc0cc0c87fa4c91c506537a71caf2bb8adb74dbbfaf6690-features-uid:latest
Deleted: sha256:435c009ad924b8a0cab797b6f9a5fed2670ce105433b10c3c7c80d219d27a3cc
$ docker image rm 8ed228e4eaf7
Untagged: vsc-dvc-nestjs-debug-88436136cceb86fe3bc0cc0c87fa4c91c506537a71caf2bb8adb74dbbfaf6690-features:latest
Deleted: sha256:8ed228e4eaf78d915a9092d3154ffc129ea3fc30053d7993d7484f342be38146

Discussion