開発環境に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.sh にexec "$@"が実装されており、引数で渡したものをコンテナ内で 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 など)で指定するのがいいだろう(以下、公式からの引用)。
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コンテナで実行するアプリケーションをデバッグ実行するという事もやってみたいと思う。
《この公式ブロガーの記事一覧》
お問合せはお気軽に
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/