【Windows環境】DockerでNode+Express+postgres+heroku CLIの爆速コンテナ構築

## 【Windows環境】DockerでNode+Express+postgres+heroku CLIの爆速コンテナを作る～ロングストーリー

Docker、ややこしいねえ。書いてある通りにやってもうまくイカン事多いネ。WindowsでやろうとするとWSLが絡むから尚ややこイ。自分なりに試行錯誤してあくまで自己流ですんで自己責任で参考にしてください。

## Windows+WSL環境でデフォルトでコンテナ作成するとパフォーマンスがめっちゃ悪い

まず、普通にWindows上のどこかに開発環境（プロジェクトレポジトリ）を置いてそれをDockerコンテナ化するとパフォーマンスがめっちゃ悪い。さらにビルドされたDockerイメージのサイズが巨大:cold_sweat:
（:warning: VSCodeから「新しい開発コンテナの作成」メニューでコンテナ作るとこうなるヨ。それとも裏技あるのだろうか…？）

###### 簡単に言うとWindows上のファイルシステムはWSL Ubuntuでは `/mnt/Ubuntu/c/*` としてマウントされており、そのファイルシステムがWSL Ubuntu自身のファイルシステムとは違うから。
Windows上のどこか、例えば`C:¥Users¥[ユーザ名]¥app_project`に開発プロジェクトを置くと、それはWSLから見た場合、`/mnt/Ubuntu/c/[Users/ユーザ名]/app_project`になる。つまりマウント先になるわけだ。ところがDockerコンテナはデフォルトでWSLの`\var\lib\docker`あたりに置かれる。つまりUbuntu自分自身のファイルシステム上に置かれる。

ということは、Windows上で開発しながら`C:¥Users¥[ユーザ名]¥app_project`配下のコードを作成・編集するってことは、WSL Ubuntuから見ると`/mnt/Ubuntu/c/[Users/ユーザ名]/app_project`配下と`\var\lib\docker`配下のファイル群をシンクロさせ続けることになる。

`/mnt/Ubuntu/c/[Users/ユーザ名]/app_project`はマウントしたWindowsファイルシステムだから<b>NTFS</b>だ。一方で`\var\lib\docker`はUbuntu上のファイルシステムだから<b>EXT4</b>だ。NTFSとEXT4という異なるファイルシステム同士でシンクロし続けようと頑張るのでめっちゃパフォーマンスが悪いというわけです。

### 解決方法

開発レポジトリをWindows上のどこか（例：`C:¥Users¥[ユーザ名]¥app_project`）ではなく、<b>WSLのUbuntu上に置いてしまおう</b>。

例えば、`/home/[ユーザ名]/Projects`などである。

このWSL Ubuntuのフルパスは、Windowsからは`\\wsl.localhost\Ubuntu\home\[ユーザ名]\Projects`として見ることができる。エクスプローラーからでもアクセスできる。
次のように:point_down:。
`\\wsl.localhost\Ubuntu\home\atom\Projects`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/576982ac-21e3-f218-2456-32b9f8ace7f0.png)

結論から言うと、開発プロジェクトをホストもゲストもWSL Ubuntuの中だけで完結してしまおう、ということになる。

Ubuntuの中の開発レポジトリ`/home/[ユーザ名]/Projects`と、同じくUbuntuの中のDockerコンテナが配置される`\var\lib\docker`とのシンクロになるので、これでパフォーマンスは爆速になる。

何だったらWindowsホストマシンを汚したくないだけ、という目的ならDocker関係なく「開発はWSL Ubuntuで」という理由だけでWSLの/home/[ユーザ名]配下にプロジェクトルートを置いても良いかもしれない。VSCodeでWSL Ubuntuのフォルダにアクセスできるし。

※ 参考：[Windows + WSL2 + docker + laravel を 10 倍速くする方法](https://www.aska-ltd.jp/jp/blog/197)

<br />
:question: Macの場合こういう悩みがあるのかまったく不明です。ご面倒でなければコメント下さい・・・

## じゃあDockerコンテナを作ろう


Dockerファイル

```Dockerfile
FROM node:20-alpine3.18

# 必要なパッケージのインストールとアップデート
RUN apk update \
    && apk add --no-cache \
    git \
RUN rm -rf /var/cache/apk/*
```

docker-compose.ymlファイル（イメージ名やコンテナ名をexpress-testとしてあるのはあくまで例です）。
```yaml
#docker-compose.yml
version: "3"
services:
  node:
    build:
      context: .
      dockerfile: Dockerfile
    image: express-test
    container_name: express-test
    volumes:
      - ./app:/usr/src/express-test
    ports:
      - "3000:3000"
    stdin_open: true 
    environment:
      - WATCHPACK_POLLING=true
    user: 1000:1000
```

:warning: Dockerファイル。FROMセクションはnode:20-alpine3.18のように軽量Linuxを示すalpineを付けた方が良い（3.18というのはただのバージョン番号）。bullseye, slimでもいいけど。よく単にnode:latestとかnode:18とかしてる事例を見るけどイメージサイズがギガレベルになる。alpineやbullseye付ければ150MB程度のイメージサイズになる（alpineがRedHat系でbullseyeがDebian系、だったと思う）

:warning:docker-compose.ymlのvolumes:セクションを見てほしいが、デフォルトのコンテナ配置場所`\var\lib\docker`だと永続化できないので、`/usr/src/`配下をvolumes:で永続化＆コンテナ配置場所（ワーキングディレクトリ）に指定している。

:warning:stdin_open, environmentの- WATCHPACK_POLLING, user: 1000:1000の設定の解説はChatGPTに聞いて。

<br />

#### コンテナの作成方法
【前提条件】
あくまでWSL+UbuntuのインストールおよびDockerデスクトップのインストールが終わっていること。
:beginner:参考：[WSL2+ubuntu20.04: GUI化して使う方法](https://qiita.com/atomyah/items/887a5185ec9a8206c7c4)
:beginner:参考：[Windows上でDocker環境を作成する方法とその構成](https://qiita.com/hoshimado/items/51c99ccaee3d4222d99d)

<br />
では始めます。

- `\\wsl.localhost\Ubuntu\home\atom\Projects`配下にプロジェクトルートフォルダを作成する（例：`\\wsl.localhost\Ubuntu\home\atom\Projects\express-test`）
- その中にDockerファイル、docker-compose.yml、.dockerignoreファイル、appフォルダ（中身カラ）を置く。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/29a99921-7758-3094-2a81-726ed7bb5baf.png)
- .dockerignoreファイルは以下の一行書くだけ
   ```Dockerfile
    node_modules
   ```
- WSL Ubuntuでプロジェクトルートに移動して下記のコマンド。
- `docker-compose up -d`
- イメージがビルドされコンテナが立ち上がる。
- `docker ps`コマンドでコンテナが立ち上がってるのが確認できるはず。
   ```bash
    ✔ Container LINEbot-express   Running
    ```
<br />

## じゃあnodeコンテナにExpress等を入れてみよう。
ここからはVSCodeを使う。

#### まずVSCodeでコンテナにアタッチ
VSCodeで「実行中のコンテナにアタッチ」すれば`docker container exec -it express-test bash`コマンドでコンテナ内に入るのと同じことで、VSCodeのターミナルがコンテナ内に入ってる状態になる。そのままindex.jsファイルを作るとかpackage.jsonを編集するとか同時にできるので作業しやすい。

- VSCodeを立ち上げる。
- VSCodeの左下の青い><アイコンをクリック。
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/318d0b4f-e952-7f06-1ebf-5d91ed01e0c0.png)
- 「実行中のコンテナにアタッチ」を選択。
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/bce7e6e8-271f-b67e-07ea-1db5505f1963.png)
- 作成したコンテナの名前が出てくるのでそれを選択する。
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/d61863b3-a31a-4969-2714-55e59a55b153.png)
- もしここで「ワークスペースがない」みたいなこと言われたら、docker-compose.ymlのvolumes:セクションで指定したワークスペース（例：/usr/src/express-test）を入力する。
- ３０秒くらいかけてVSCodeでコンテナが開く。
- ご覧の通り、コンテナを開いた状態。ターミナルのパスをご覧あれ。
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/0de0120f-229c-07c2-46bf-eb6d6bf84d61.png)


- ここではプロジェクトルートがLINEbot-expressという名前だが、Volume:セクションで指定したコンテナワークスペース`/usr/src/LINEbot-express`が開けているということ。

:warning: 興味があればWSL Ubuntuで`ls /usr/src`コマンドを打ってみてほしい。中には何もない。Volume:セクションで指定したパスはあくまでコンテナの中のワークスペースだということだ。OS（Ubuntu）から覗いてみてもコンテナの実体は見れない。


#### コンテナの中でnpm init, expressとnodemonのインストール
- VSCodeのターミナルで`npm init -y`コマンド
- package.jsonが出来るので開く。
- "scripts"セクションで次のコード`"start": "node index.js",`を追加しておく。
    ```json
      "scripts": {
        "start": "node index.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
    ```
    <br />
- `npm install express`でExpressをインストール。
- 同じ階層で良いからindex.jsを作成し、以下のコードを入力。
   ```javascript
    const express = require('express')
    const app = express()
    const port = 3000
    app.get('/', (req, res) => {
        res.send('Hello World!')
    })
    app.listen(port, () => {
        console.log(`Example app listening on port ${port}`)
    })
    ```
    <br />
- ターミナルで`npm start`コマンドを打つ。
- ブラウザを開きhttp://localhost:3000/をロードする。「Hello World!」と画面に表示されるはず。
- このままだとindex.jsに変更を加えるたびにいちいちnode index.jsを再起動しなければならない。そこで`npm install nodemon`でnodemonをインストールしpackage.jsonの `"start": "node index.js",`を`"start": "nodenon index.js",`に変更。
    ```json
      "scripts": {
        "start": "nodemon index.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
    ```
<br />

- そしたらターミナルで`npm start`する。

- 今度はnodemonが立ち上がりnodemonがjsファイルやjsonファイルを監視してるよーというログが黄色文字でlog出力され最後に緑文字でnodemonが`node index.js`を実行とlogってるのが確認できるはず。

- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/12f25484-180b-c894-4ee0-32b1fe42f958.png)


- index.jsの「Hello World！」の箇所を「こんにちは！」に変えてみる。
- nodemonがすぐに再起動するのが分かる（一瞬で終わる）。
- ブラウザをリロードすると「こんにちは！」という表示に変わっているはず。

<br />

これでnode.js+Express環境が整った。

## じゃあ次にPostgresコンテナを作成しよう。
あまりやる事ない。
ほとんどdocker-compose.ymlを編集するだけ。以下は変更済みdocker-compose.ymlファイル。

```diff_yaml
version: "3"
services:
  node:
    build:
      context: .
      dockerfile: Dockerfile
    image: linebot-reserve-image
    container_name: LINEbot-express
    volumes:
      - ./app:/usr/src/LINEbot-express
    ports:
      - "3000:3000"

    stdin_open: true 

    environment:
      - WATCHPACK_POLLING=true # 
      - 
    user: 1000:1000

########## ここから下のコードを追加  ###########

    depends_on:
      - postgres


  postgres:
    image: postgres:16-alpine3.18
    container_name: LINEbot-postgres
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_db
    ports:
      - "5432:5432"

################### ここまで  ###################

```

- Node.jsのコンテナは `depends_on:` を使用してPostgreSQLのコンテナが起動してから実行されるようにしている。
- docker-compose.ymlを上記のように編集したら`docker-compose up -d`コマンドでもう一度コンテナを上げなおす。この過程でpostgresイメージも作成される。Postgres用Dockerファイルを用意しても良いが、しなくてもよい（postgres:16-alpine3.18をDockerHubから直落としすることになるだけ）。
- データの永続化のためにボリュームを設定しなくてもいいのかと思ったが、PostgreSQLのDockerイメージでは、デフォルトでデータディレクトリがボリュームにマウントされており、データの永続化が行われている。したがって、特別なボリューム設定を行わなくてもデータはコンテナの外部に永続的に保存されるらしい（ChatGPT調べ）。
- さて、`docker ps`でnodeコンテナとpostgresコンテナが立ち上がってることを確認できたら、WSLで次のコマンドを打ちpostgresコンテナに潜入する。
- `docker container exec -it LINEbot-postgres psql -U postgres`
- プロンプトが`postgres=#`という表示に変わる。
  ```bash
  postgres=#
  ```
  <br />
- ここでpsqlメタコマンドを打つことができる。
- \l でデータベース一覧を表示
- \c [データベース名]でデータベースに接続
- テーブルを作成したい場合、
- `select current_schema;`でデフォルトスキーマを選択
- そしてテーブル作成してみる。`create table users (id integer, name varchar(10)); `
- \dt でテーブル一覧表示
- `insert into users VALUES (1, 'Atom');`　でデータをテーブルに入力。
- `select * from users;`でテーブルのデータを確認できる。
- \q　でpostgresコンテナから抜ける。

<br />

これでnode.js+Express+postgres環境が整った。

## NodeコンテナからPostgresコンテナに接続テストしてみよう

簡単です。

- ルートディレクトリにtest.jsなど適当にファイルを作成。
- test.jsに次のようにコーディング。

    ```javascript
    const express = require('express');
    const { Pool } = require('pg');
    
    const app = express();
    const port = 3000;
    
    // PostgreSQL接続情報
    const pool = new Pool({
      user: 'postgres', // データベースユーザー
      host: 'LINEbot-postgres', // Postgresコンテナのホスト名
      database: 'app_db', // データベース名
      password: 'password', // データベースパスワード
      port: 5432, // データベースのポート
    });
    
    // ユーザーテーブルからデータを取得
    app.get('/', async (req, res) => {
      try {
        const result = await pool.query('SELECT * FROM users');
        res.json(result.rows);
      } catch (error) {
        console.error('Error executing query', error);
        res.status(500).json({ error: 'Internal Server Error' });
      }
    });
    
    app.listen(port, () => {
      console.log(`Server is running on port ${port}`);
    });
    ```
<br />

- package.jsonの`"start": "nodemon index.js",`を`"start": "nodemon test.js",`に変更。
- VSCodeのターミナルで`npm start`
- ブラウザでhttp://localhost:3000を開く。usersテーブルのデータがJSON形式で表示されているはず。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307441/996f84cd-07fc-c1a4-2aa3-39bd78ab3a8a.png)
 

## コンテナをチームで共有

こうして構築したコンテナをチームで共有したかったらdockerhubにプッシュしておけば良い。Expressやpgのバージョン指定があったらコンテナ構築段階で指定してインストールすればよろしい（例：npm install pg@7.0）

:beginner:参考：[コンテナイメージをDockerHubにpushして共有する](https://gray-code.com/blog/container-image-push-for-dockerhub/)

<br />

以上

### 後述
alpineは軽量ゆえいろいろとよく使う系ツールが入っていないことが分かった。curlもない。bashもない。glibcもsudoもなんもない。そのためDockerファイルを以下のように追記修正した。

```docker
FROM node:20-alpine3.18

# 必要なパッケージのインストールとアップデート
RUN apk update \
    && apk add --no-cache \
    git \
    curl \    # ← 追記
    bash \    # ← 追記
    sudo \    # ← 追記
    postgresql-client \   # ← 追記
    # 追加のパッケージをここに列挙する

    # 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
```

Dockerファイルを変更したら現存するコンテナ・イメージを削除して`docker-compose up -d`コマンドを再度打つべし。

heroku CLIは手動でインストールするしかなかった。

###### heroku CLIのインストール方法：

- WSL Utuntuにてコンテナに入る（root権限で。-u 0 がそれを意味する）。
コマンド：`docker exec -it -u 0 LINEbot-express /bin/sh`
- heroku CLIをインストールする。
コマンド：`curl https://cli-assets.heroku.com/install.sh | sh`


また開発途中でいろいろ必要になってくるかもしれない。
「＃ 追加のパッケージをここに列挙する」
とあるように必要なものが出てきたらここに列挙し、コンテナとイメージを削除した後、`docker-compose up -d`コマンドを打つべし。


###### 例えばaws CLIインストールの場合：
`apk add aws-cli`なので

```docker
FROM node:20-alpine3.18

# 必要なパッケージのインストールとアップデート
RUN apk update \
    && apk add --no-cache \
    git \
    curl \
    bash \   
    sudo \
    aws-cli \     # ← 追記
    # 追加のパッケージをここに列挙する

    # 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
```

###### 例えばfirebase CLIインストールの場合：
`npm install -g firebase-tools`なので、


```docker
FROM node:20-alpine3.18

# 必要なパッケージのインストールとアップデート
RUN apk update \
    && apk add --no-cache \
    git \
    curl \
    bash \   
    sudo \
    aws-cli \
    postgresql-client \
    # 追加のパッケージをここに列挙する

RUN npm install -g firebase-tools     # ← 追記

    # 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*
```

※ しかしnpm install firebase-toolsはコンテナ内で実行できるだろう。たぶん。


#### heroku CLIも自動インストールさせる

毎回Dockerを立ち上げるたびにheroku CLIを手動でインストールさせられるのはつらいので以下のようにDockerファイルを書き換えた。


```docker
FROM node:20-alpine3.18

# 必要なパッケージのインストールとアップデート
FROM node:20-alpine3.18

# 必要なパッケージのインストールとアップデート
RUN apk update \
    && apk add --no-cache \
    git \
    curl \
    sudo \
    bash \
    postgresql-client \
    # 追加のパッケージをここに列挙する

# 以下はDockerにおけるキャッシュのクリーンをしている
RUN rm -rf /var/cache/apk/*

# heroku CLIをインストール
RUN adduser -D heroku \
    && echo "heroku ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers \
    && su - heroku -c 'curl https://cli-assets.heroku.com/install.sh | sh' \
    && addgroup heroku root
```

ただ単純に
`RUN curl https://cli-assets.heroku.com/install.sh | sh`
を追加するだけだとエラー、エラー、エラー・・・。

Claudeに助けてもらったヨ（ChatGPTより賢い！）

<br />

以上