【Docker Storage】バインドマウントでホストマシンのファイルを Node.js コンテナで実行する

# はじめに
この記事では、Docker のバインドマウント機能を利用して、ホストマシンのファイルを Node.js コンテナで実行する手順について記載します。

https://docs.docker.com/storage/bind-mounts/

以下の記事でも Node.js / TypeScript のコンテナを作成しましたが、このケースでは、ホストマシンのファイルに変更があった場合、都度イメージをビルドする必要があります。

https://qiita.com/Yasushi-Mo/items/789871a7f3eb86b01a62

バインドマウントを利用して、ホストマシンのファイルをコンテナで実行できるようにすることで、都度イメージをビルドする必要がなくなります。


# 開発環境
開発環境は以下の通りです。

- Windows 11
- Docker Engine 26.1.1
- Node.js 20.15.1
- npm 10.8.2
- @types/node 20.14.10
- nodemon 3.1.4
- ts-node 10.9.2
- TypeScript 5.5.3

# Node.js / TypeScript アプリケーションの作成
`package.json` を作成します。

```powershell
npm init -y
```

TypeScript 及び以下のパッケージをインストールします。

- `ts-node` : TypeScript を Node.js で直接実行できるようにする
- `@types/node` : Node.js の型定義

https://typestrong.org/ts-node/

https://github.com/DefinitelyTyped/DefinitelyTyped

```powershell
npm install typescript ts-node @types/node nodemon --save-dev
```

TypeScirpt 設定ファイルを作成します。

```powershell
npx tsc --init
```

Node.js 20 系を、`ES Modules` (`ECMA Script Modules`) で利用する設定に変更します。

```tsconfig.json
{
  "compilerOptions": {
    /* Language and Environment */
    "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
    "lib": [
      "ES2023"
    ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,

    /* Modules */
    "module": "node16" /* Specify what module code is generated. */,

    /* Interop Constraints */
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,

    /* Type Checking */
    "strict": true /* Enable all strict type-checking options. */,

    /* Completeness */
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}

```

`src/index.ts` ファイルを作成し、ログを出力するコードを実装します。

```src/index.ts
import http from "http";

const HOSTNAME = "0.0.0.0";
const PORT = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/plain");
  res.end("Hello World");
});

server.listen(PORT, HOSTNAME, () => {
  console.log(`Server running at http://${HOSTNAME}:${PORT}`);
});

```

`package.json` でエントリーポイントの変更とアプリケーションを実行するスクリプトの追加を行います。
```package.json
{
  ...
  "main": "src/index.ts",
  "scripts": {
    "start": "ts-node src/index.ts",
    ...
  ...
}
```

ここまでで、一度動作確認します。

```powershell
npm start
```

実装通りログが出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/e6b36a42-8520-dc84-237c-6cbeee08c912.png)

# Dockerfile の作成
Dockerfile を作成し、イメージをビルドします。

```Dockerfile
FROM node:20-alpine3.19
WORKDIR /app
COPY package*.json ./
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
```

:::note
今回ソースコードはバインドマウントしたホストマシンのファイルを利用するので、ホストマシンの全ファイルのコピー（`COPY . .`）は不要です。
:::

作成した Dockerfile からイメージをビルドします。

```powershell
docker image build --tag node-ts:1.0.0 .
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/a7a795c7-3422-76a6-fff0-494b0528dce7.png)

イメージがビルドできたか確認します。

```powershell
docker image ls node-ts
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/b4aae174-f258-e3bf-043f-dcc8a7ca5b67.png)

# コンテナの起動とバインドマウント
ビルドしたイメージを使ってコンテナを起動します。また、コンテナ起動時にホストマシンのファイルをバインドマウントします。

```powershell
docker container run                          `
--name node-ts                                `
--rm                                          `
--publish 3000:3000                           `
--mount type=bind,source="$(pwd)",target=/app `
node-ts:1.0.0
```

実装したログが出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/949a63ee-8c82-3b48-479d-f180d94a5393.png)

Node.js のバージョンを出力するログを追加して再度コンテナを起動します。

```src/index.ts
import http from "http";

const HOSTNAME = "0.0.0.0";
const PORT = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/plain");
  res.end("Hello World");
});

server.listen(PORT, HOSTNAME, () => {
  console.log(`Server running at http://${HOSTNAME}:${PORT}`);
  console.log("node version is:" + process.version); // 追加
});

```

イメージを再ビルドしなくても Node.js のバージョンも出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/ee9f1af3-1612-95dd-555c-2e2347c510bf.png)

:::note
Docker Compose を利用するとイメージのビルドコマンド実行が不要になります。また、コンテナ起動時のコマンドも `docker compose up --build` のみで大丈夫です。

```compose.yaml
services:
  app:
    ports:
      - "3000:3000"
    volumes:
      - type: bind
        source: ./
        target: /app
    build: .
```

```powershell
docker compose up --build
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/8889e85e-e32a-2430-07df-0a11778ee10b.png)


:::

# 参考
- [Definitely Typed](https://github.com/DefinitelyTyped/DefinitelyTyped)
- [node - Official Image](https://hub.docker.com/_/node)
- [Node Target Mapping](https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping)
- [nodemon](https://nodemon.io/)
- [npm-init](https://docs.npmjs.com/cli/v10/commands/npm-init)
- [ts-node](https://typestrong.org/ts-node/)
- [tsc CLI Options](https://www.typescriptlang.org/docs/handbook/compiler-options.html)

# 関連リンク

https://qiita.com/Yasushi-Mo/items/678fefffbf4807c83d44

https://github.com/yasushi-mo/docker-samples/tree/main/node-bind-mount