見出し画像

開発環境にNode.jsをインストールせずに、DockerコンテナでNode.jsを実行して開発する

はじめに

こんにちは、SHIFTの開発部門に所属している Katayama です。

Node.jsの更新ができていないプロジェクトがあり、そのプロジェクトではNode.js16.xを使っていた。
ただ、Node.js のサポートが以下のような状態になっており、2023 年中に 20 に上げないといけない状態になっている。

そこで Node.js のバージョンを段階的に上げようと、まず Node.js18.x を動かそうとしたが、以下のようにエラーになってしまった。

study@localhost:~/workspace/node-express (main *)
$ nodebrew use v18.14.1
study@localhost:~/workspace/node-express (main)
$ node -v
node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.25' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)

上記のエラーは、今回 Node.js のバージョンを上げようとしていた環境の OS が CentOS7.9(Virtualbox 上)だったので、もう最新が更新されず"GLIBC_2.27"などが入っていなかった事が原因だった。
CentOS7 のサポートも 2024 年で切れるので、こうなると 1 から新しい環境を構築した方がいいが、開発途中のものがあったのでそれが終わってからにしたかった。

というわけで、暫定対応として実行環境だけ Docker を使い、何とかローカルでの開発を継続する事を考えて実際にやってみた。

どうやるか?

まず、アプリケーションが他のサーバー(MySQL とか Redis とか)に依存して動く場合とそうでない場合とで docker-compose を使うか、使わないでもいいかが変わるので、以下の 2 パターンで見ていく。

① アプリケーション単体で動く
② 他のサーバーに依存してアプリケーションが動く

① アプリケーション単体で動く

アプリケーション単体で動くもののイメージとしては、以下のようなシンプルな Express アプリケーションのイメージ(package.json に"type": "module"を指定して ES Module を実行できる環境で開発しているものとする)。

srv/index.jsimport express from 'express';

const app = express();
app.get('/hello', (req, res) => {
	res.json({ msg: 'hello' });
});
app.listen(3000);
package.json	"scripts": {
		...
		"express": "nodemon srv/index.js",
	}

この場合、Node.js18.x の実行環境を持つ Docker イメージを使い、そこにローカルのファイルをマウントして実行すればいい。
具体的には以下のような docker run コマンドで今までのローカルでの開発と同じようにサーバーを起動できる。

study@localhost:~/workspace/vite-vue3-vuetify-express (main *)
$ docker run -v "$(pwd):/app:ro" -w /app -p 3000:3000 -it node:18.14.0-alpine sh -c "yarn install && yarn express"

コマンドの詳細については以下の通り。

  • -v "$(pwd):/app:ro"
    ホスト上のカレントディレクトリを、コンテナー内の/app ディレクトリにバインドマウントする
    ただし、ディレクトリを読み込み専用ボリュームとしてマウントし、コンテナ内での変更はホストに同期されないようにする(詳細はUse a read-only volumeも参照)

  • -w /app
    コンテナのワーキングディレクトリ(カレントディレクトリ)を/app に設定する(コンテナ内のコマンドはこのディレクトリで実行されるようになる)

  • -p 3000:3000
    ポートマッピング(コンテナ内のポート 3000 をホストのポート 3000 にマッピング)を設定する(これにより、ホストの curl localhost:3000 は、コンテナ内のポート 3000 で立っているサーバーへの GET リクエストになる)

  • -it
    ホストマシンとコンテナ間で対話できるシェルを起動する
    (Docker のドキュメントに記載されている内容を直訳すると、「コンテナの標準入力に接続された疑似 TTY を割り当てるよう Docker に指示し、コンテナ内に対話型の bash シェルを作成する」。詳細はAssign name and allocate pseudo-TTY (--name, -it)を参照。)

  • sh -c "yarn install && yarn express"
    コンテナ起動時に実行するコマンドを定義しており、node_modules のインストールと package.json のスクリプトを実行している
    ちなみに、今回のように docker run でコマンドを引数として渡せているのは、今回の node:18.14.0-alpine のイメージの docker-entrypoint.shexec "$@"が実装されており、引数で渡したものをコンテナ内で exec してくれているから(この設定がない場合には、自分で entrypoint を上書きするなどが必要になる)

上記のようにサーバーを Docker で起動した後は、以下の動画の通り、curl などで API を実行してレスポンスが返ってくるのが確認できる。
また、Docker のコンテナに対し、ボリュームでホストのプロジェクトのディレクトリをマウントしているので、ホスト側でのファイル変更があればそれが自動でコンテナに反映される(ただし、上記のコマンドの詳細でも触れたが、今回は-v オプションに"ro"を付けているのでコンテナに入ってファイルを修正してもホスト側には反映されない設定になっている)今回は nodemon を利用しているので、コンテナ内の Express サーバーは自動で再起動し、変更内容が反映された。

※ちなみに、ボリュームのマウントの際に、"ro"を指定せずに、-v "$(pwd):/app"でコンテナを起動した場合には、以下のようにコンテナ内に入りファイルを更新すると、それがホスト側にも反映されることが確認できる。

② 他のサーバーに依存してアプリケーションが動く

「他のサーバーに依存して」というのは、例えば、Express で実装しているアプリケーションが、MySQL や Redis といったサーバーとのやり取りができて成立する場合の事。
その場合、ローカルの開発環境で MySQL や Redis といったサーバーを立てる際には、Docker を利用するのが手っ取り早いが、単に docker run でコンテナを立ち上げるとネットワークの設定などが面倒になる。

というわけでその辺りをまるっと解決してくれる docker-compose を利用するのが便利。docker-compose を利用する場合の設定は以下のようになる。

docker-compose.yamlversion: '3.9'
services:
  app:
    image: node:18.14.0-alpine
    container_name: node-express
    command: sh -c "yarn install && yarn dev"
    ports:
      - 3000:3000
    working_dir: /app
    volumes:
      - ./:/app
  mysql:
    image: mysql:8.0.32
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: ''
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      TZ: 'Asia/Tokyo'
    ports:
      - 3306:3306
    volumes:
      - ./data/mysql:/var/lib/mysql
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf

ちなみに、今回の Express の実装のイメージは以下(Node.js で import・export(ES6 の構文)を使えるように webpack × Babel の設定をやってみたにあるような Webpack によるビルドを行い、CommonJS にトランスパイルして実行するように設定している)。

import { Router } from 'express';

const router = Router();

router.get('/:id', async (req, res) => {
	const { models, CustomError } = req.app.locals;

	try {
		const user = await models.user.findByPk(req.params.id, {
			attributes: { exclude: [`password`] }
		});
		if (!user) throw new CustomError(404, 'Not Found');

		res.status(200).json(user.toJSON({ exclude: [`userId`] }));
	} catch (error) {
		res.status(500).error(error);
	}
});
...
	"scripts": {
		"dev": "webpack watch --node-env=development",
		"start:watch": "nodemon dist/index.js",
		...
   }

上記のように設定する事で、Docker コンテナどうしは同じネットワークに所属するので相互に通信でき、Express アプリケーションは MySQL に対して CRUD を実行できるようになる。
また、ホストマシンとは ports のマッピング設定により通信でき、そこは ① のパターンと同じ。

① 同様に、以下の動画の通り curl などで API を実行してレスポンスが返ってくるのが確認できる。
また、ホストのプロジェクトのディレクトリをマウントしているので、ホスト側の変更が自動でコンテナに反映されるので、HMR(Hot Module Replacement)とサーバーの再起動が実行される事も確認できる。

※上記の動画に関して、① と違って今回検証している Express アプリケーションは、Node.js で import・export(ES6 の構文)を使えるように webpack × Babel の設定をやってみたにあるような Webpack によるビルドを行い、CommonJS にトランスパイルして実行するように設定していた。
そのため、docker-compose の commands では Webpack のサーバーを watch モードで起動し、docker exec でコンテナに入った別のプロセスで、yarn start:watch による nodemon dist/index.js コマンドを実行している。
こうしないと、Webpack の watch によりコマンドが終了しないので、yarn dev && yarn start:watch をとしても、永遠に yarn start:watch が実行されず、Express アプリケーションの起動ができない。

※docker-compose ではデフォルトのネットワークが作成されるので、app(node-express のコンテナ) → mysql(mysql のコンテナ)の通信できる。ただし、ネットワーク自体の IP は可変なので、ホスト名(mysql など)で指定するのがいいだろう(以下、公式からの引用)。

Links are not required to enable services to communicate - when no specific network configuration is set, any service MUST be able to reach any other service at that service’s name on the default network.(リンクは、サービスが通信できるようにするために必要ではありません。特定のネットワーク構成が設定されていない場合、どのサービスもデフォルトネットワーク上のそのサービスの名前で他のどのサービスにも到達できなければなりません(MUST)。)

study@localhost:~/workspace/node-express (main *)
$ docker network ls
NETWORK ID     NAME                                DRIVER    SCOPE
...
a2165ccc54ca   node-express_default                bridge    local
...
study@localhost:~/workspace/node-express (main *)
$ docker network inspect node-express_default
[
    {
        "Name": "node-express_default",
        ...
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.19.0.0/16",
                    "Gateway": "172.19.0.1"
                }
            ]
        },
        ...
        "Containers": {
            "d392408bf800601bca5b4eb69827efe588ecdf8e67e356ced6f03c90db1a3058": {
                "Name": "mysql",
                "EndpointID": "fedc2862759bac2f69ffaed94f82a2c590a426b6663b914fc7ce46e64cf6a774",
                "MacAddress": "02:42:ac:13:00:03",
                "IPv4Address": "172.19.0.3/16",
                "IPv6Address": ""
            },
            "e10639f2611d7f8260f0b94c56026c10c9b7da8af5201fcd9fddc323a8c03613": {
                "Name": "node-express",
                "EndpointID": "10f015cb2254cabac802c4a7d579cb1641fa4295cb203c0fd63fe30d7161372a",
                "MacAddress": "02:42:ac:13:00:02",
                "IPv4Address": "172.19.0.2/16",
                "IPv6Address": ""
            }
        },
        ...
    }
]

まとめとして

今回は開発環境に Node.js をインストールせずに、Docker コンテナで Node.js を実行する事で開発を行う方法に関してみてきた。
暫定対応としてやってみたが、今回の方法を取ればわざわざ自分の環境に色々なものをインストールする必要がなくなるので、この方法で開発をするのも 1 つの案なのではないかと思った。

また、今回はデバッグ実行に関しては触れていなかったので、今後Dockerコンテナで実行するアプリケーションをデバッグ実行するという事もやってみたいと思う。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。
経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。
SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!