コンテナー内で Node.js をデバッグする

Node.js プロジェクトに Docker ファイルを追加すると、Docker コンテナー内でアプリケーションをデバッグできるようにタスクと起動構成が追加されます。ただし、Node.js を取り巻く大規模なエコシステムのため、これらのタスクはすべてのアプリケーションフレームワークまたはライブラリに対応できるわけではありません。つまり、一部のアプリケーションでは追加の構成が必要になります。

Docker コンテナーのエントリーポイントの構成

Docker 拡張機能は、`package.json` のプロパティを介して、Docker コンテナーのエントリーポイント (つまり、Docker コンテナー内でデバッグモードでアプリケーションを起動するためのコマンドライン) を推測します。拡張機能は、まず `scripts` オブジェクト内の `start` スクリプトを探します。見つかった場合、`node` または `nodejs` コマンドで始まる場合は、それを使用してデバッグモードでアプリケーションを起動するためのコマンドラインを構築します。見つからない場合、または認識された `node` コマンドでない場合は、`package.json` の `main` プロパティが使用されます。どちらも見つからないか、認識されない場合は、Docker コンテナーを起動するために使用される `docker-run` タスクの `dockerRun.command` プロパティを明示的に設定する必要があります。

一部の Node.js アプリケーションフレームワークには、アプリケーションを管理するための CLI が含まれており、`start` スクリプトでアプリケーションを起動するために使用されます。これにより、基になる `node` コマンドが不明瞭になります。このような場合、Docker 拡張機能は起動コマンドを推測できず、起動コマンドを明示的に構成する必要があります。

例: Nest.js アプリケーションのエントリーポイントの構成

{
  "tasks": [
    {
      "type": "docker-run",
      "label": "docker-run: debug",
      "dependsOn": ["docker-build"],
      "dockerRun": {
        "command": "nest start --debug 0.0.0.0:9229"
      },
      "node": {
        "enableDebugging": true
      }
    }
  ]
}

例: Meteor アプリケーションのエントリーポイントの構成

{
  "tasks": [
    {
      "type": "docker-run",
      "label": "docker-run: debug",
      "dependsOn": ["docker-build"],
      "dockerRun": {
        "command": "node --inspect=0.0.0.0:9229 main.js"
      },
      "node": {
        "enableDebugging": true
      }
    }
  ]
}

アプリケーションのエントリーページへのブラウザーの自動起動

Docker 拡張機能は、デバッガーで起動した後、アプリケーションのエントリーポイントへのブラウザーを自動的に起動できます。この機能はデフォルトで有効になっており、`launch.json` のデバッグ構成の `dockerServerReadyAction` オブジェクトを介して構成されます。

この機能は、アプリケーションのいくつかの側面に依存しています

  • アプリケーションは、デバッグコンソールにログを出力する必要があります。
  • アプリケーションは、「サーバー準備完了」メッセージをログに記録する必要があります。
  • アプリケーションは、閲覧可能なページを提供する必要があります。

デフォルト設定は Express.js ベースのアプリケーションでは機能する可能性がありますが、他の Node.js フレームワークでは、これらの側面のうち 1 つ以上を明示的に構成する必要がある場合があります。

アプリケーションログがデバッグコンソールに書き込まれるようにする

この機能は、アプリケーションがアタッチされたデバッガーのデバッグコンソールにログを書き込むことに依存しています。ただし、すべてのロギングフレームワークがデバッグコンソールに書き込むわけではありません。コンソールベースのロガーを使用するように構成されている場合でも (一部の「コンソール」ロガーは実際にはコンソールをバイパスして `stdout` に直接書き込みます)。

解決策はロギングフレームワークによって異なりますが、一般的には *実際に* コンソールに書き込むロガーを作成/追加する必要があります。

例: Express アプリケーションがデバッグコンソールに書き込むように構成する

デフォルトでは、Express.js は debug ロギングモジュールを使用します。これはコンソールをバイパスできます。これは、ログ関数をコンソールの `debug()` メソッドに明示的にバインドすることで解決できます。

var app = require('../app');
var debug = require('debug')('my-express-app:server');
var http = require('http');

// Force logging to the debug console.
debug.log = console.debug.bind(console);

また、`debug` ロガーは、`docker-run` タスクで設定できる `DEBUG` 環境変数を介して有効になっている場合にのみログを書き込むことに注意してください。(この環境変数は、Docker ファイルがアプリケーションに追加されると、デフォルトで `*` に設定されます。)

{
  "tasks": [
    {
      "type": "docker-run",
      "label": "docker-run: debug",
      "dependsOn": ["docker-build"],
      "dockerRun": {
        "env": {
          "DEBUG": "*"
        }
      },
      "node": {
        "enableDebugging": true
      }
    }
  ]
}

アプリケーションが「準備完了」になるタイミングの構成

拡張機能は、Express.js がデフォルトで行うように、`Listening on port ` 形式のメッセージをデバッグコンソールに書き込むときに、アプリケーションが HTTP 接続を受信する準備が「完了」したと判断します。アプリケーションが異なるメッセージをログに記録する場合は、デバッグ起動構成の dockerServerReadyAction オブジェクトの `pattern` プロパティを、そのメッセージに一致する JavaScript 正規表現に設定する必要があります。正規表現には、アプリケーションがリッスンしているポートに対応するキャプチャグループを含める必要があります。

たとえば、アプリケーションが次のメッセージをログに記録するとします

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
  debug('Application has started on ' + bind);
}

デバッグ起動構成 (`launch.json` 内) の対応する `pattern` は次のとおりです

{
  "configurations": [
    {
      "name": "Docker Node.js Launch",
      "type": "docker",
      "request": "launch",
      "preLaunchTask": "docker-run: debug",
      "platform": "node",
      "dockerServerReadyAction": {
        "pattern": "Application has started on port (\\d+)"
      }
    }
  ]
}

ポート番号の `(\\d+)` キャプチャグループと、`\d` 文字クラスのバックスラッシュの JSON エスケープ文字としての `\` の使用に注意してください。

アプリケーションのエントリーページの構成

デフォルトでは、Docker 拡張機能はブラウザーの「メイン」ページを開きます (ただし、これはアプリケーションによって決定されます)。ブラウザーを特定のページで開く必要がある場合は、デバッグ起動構成の dockerServerReadyAction オブジェクトの `uriFormat` プロパティを、ポートを代入する場所を示す 1 つの文字列トークンを含む Node.js 形式の文字列に設定する必要があります。

メインページではなく `about.html` ページを開くためのデバッグ起動構成 (`launch.json` 内) の対応する `uriFormat` は次のとおりです

{
  "configurations": [
    {
      "name": "Docker Node.js Launch",
      "type": "docker",
      "request": "launch",
      "preLaunchTask": "docker-run: debug",
      "platform": "node",
      "dockerServerReadyAction": {
        "uriFormat": "http://localhost:%s/about.html"
      }
    }
  ]
}

Docker コンテナーのソースファイルをローカルワークスペースにマッピング

デフォルトでは、Docker 拡張機能は、実行中の Docker コンテナー内のアプリケーションソースファイルが `/usr/src/app` フォルダーにあると想定し、デバッガーは、コンテナーから Visual Studio Code にブレークポイントを変換するために、これらのファイルを、開いているワークスペースのルートにマップバックします。

アプリケーションのソースファイルが異なる場所にある場合 (たとえば、Node.js フレームワークが異なると規則が異なります)、Docker コンテナー内または開いているワークスペース内のいずれかで、デバッグ起動構成の node オブジェクトの `localRoot` および `remoteRoot` プロパティのいずれかまたは両方を、ワークスペースと Docker コンテナー内のルートソースの場所にそれぞれ設定する必要があります。

たとえば、アプリケーションが代わりに `/usr/my-custom-location` に存在する場合、対応する `remoteRoot` プロパティは次のようになります

{
  "configurations": [
    {
      "name": "Docker Node.js Launch",
      "type": "docker",
      "request": "launch",
      "preLaunchTask": "docker-run: debug",
      "platform": "node",
      "node": {
        "remoteRoot": "/usr/my-custom-location"
      }
    }
  ]
}

トラブルシューティング

`node_modules` がないため、Docker イメージのビルドまたは起動に失敗する

Dockerfile は、イメージのビルド時間、イメージサイズ、またはその両方を最適化するように配置されることがよくあります。ただし、すべての Node.js アプリケーションフレームワークが、一般的な Node.js Dockerfile の最適化をすべてサポートしているわけではありません。特に、一部のフレームワークでは、`node_modules` フォルダーはアプリケーションのルートフォルダーの直下のサブフォルダーである必要がありますが、Docker 拡張機能は、`node_modules` フォルダーが親または祖先のレベルに存在する Dockerfile をスキャフォールドします (これは一般的に Node.js で許可されています)。

解決策は、その最適化を `Dockerfile` から削除することです

FROM node:lts-alpine
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"]
# Remove the `&& mv node_modules ../` from the RUN command:
# RUN npm install --production --silent && mv node_modules ../
RUN npm install --production --silent
COPY . .
EXPOSE 3000
RUN chown -R node /usr/src/app
USER node
CMD ["npm", "start"]