【PM2】ecosystem.conf.jsからNode.jsアプリの起動を試そうとしたら、ハマった!
はじめに
先に結論を書きますが、Windows環境だと、npm startのようなnpm scriptは、PM2のconfigファイルから起動が出来ない場合があります。
React系のアプリをecosystem.config.jsで起動しようとしたらうまくいかず、ハマってしまいました。ドキュメント等を見ても原因が掴めず、試しにubuntuで試したところ、普通に動きましたので、私の中では一部Windowsでは利用できない機能があるという結論に達しました。
PM2
PM2はプロセス管理をしてくれるNode.jsのパッケージです。エラー落ちしたプログラムを自動で再起動してくれるので、私はこのサイトのWebサーバや自動取引BOTのゾンビ化に使用しています。
PM2にはconfigファイル(ecosystem.config.js)にアプリ名、実行パス、実行コマンド、環境変数やその他のオプションを記載しておいて、そこからstart、stop、restart、deleteなどのPM2のコマンドを実行する機能があります。複数のアプリをPM2に管理したい場合に使えそうですね。
私も2つのアプリをPM2で管理していますが、いずれも手動で登録しています。一度登録してしまえば、再起動するのは簡単なのですが、、、ファイルでコマンドごと管理できれば、今後OSを入れ直した時とか、再度登録が必要になった場合に何かと便利だろうと、試してみようと思ったわけです。
参考までに、configファイルの詳細はconfigファイルのドキュメントで確認できます。
やりたいこと
経緯として記載します。私のサーバ(ubuntu)では以下2つのアプリが起動しています。
nextblog
実行パスは/home/user-name/mysitenext
で、実行コマンドはnpm start
です。Node.jsのアプリなので、PM2で起動するときに環境変数NODE_ENVを"production"に設定したいです。
oanda-bot
実行パスは/home/user-name/goproject/oanda-bot
です。実行コマンドは、oanda-bot
です。goのアプリなので、実行コマンドはコンパイルした実行ファイルの名前です。
ecosystem.config.jsでやりたいこととしては、以下のようになります。これだけ見ると大した事なさそうなのですが。。。
- Node.jsとgoの2つのアプリを管理
- Node.jsアプリはnpm script(npm start)で起動
- Node.jsは環境変数を設定して起動
ecosystem.config.jsについて
pm2 init
コマンドを実行するとecosystem.config.jsが自動で作成されます。なお、pm2 init simple
という簡略版のconfigファイルを生成するコマンドもあります。今回は後者を対象にします。
pm2 init simple
を実行すると、ecosystem.config.jsの中身は以下の内容になっています。
module.exports = {
apps : [{
name : "app1",
script : "./app.js"
}]
}
非情にシンプルなテンプレートです。配列になっているので、複数のアプリを登録する場合は中のオブジェクトを足していけばOKです。nameとscript以外の属性も今回は使っていきます。使うものは以下の通りです。ドキュメントから抜粋し日本語に訳しています。
- name : アプリ名
- script : pm2 startから見た実行スクリプトの相対パス
- cwd : アプリを実行するときのディレクトリ
- args : scriptを実行するときの引数
- env : アプリで使用される環境変数
実行環境
最終的にはサーバ(ubuntu)で実行したいのですが、いきなり本番で試せないので、自宅のWindowsで検証します。まずは、検証用のnodeアプリを作成し、単体でconfigファイルから起動できるか見ていきます。
準備
- pm2を実行するパス
C:\Users\user-name\desktop\pm-test
- Node.jsアプリのパス
C:\Users\user-name\desktop\pm-test\node-app
- Node.jsアプリのpackage.json
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./start.js"
},
"keywords": [],
"author": "",
"license": "ISC"
}
scriptsのstartを手動で追加しています。これでnpm startすれば、node ./start.js
が実行されるようになります。start.jsは以下の内容で、package.jsonと同じ階層に作成しました。
setInterval(() => {
console.log(process.env.NODE_ENV);
throw new Error("エラーで落ちます");
}, 5000)
5秒ごとに、NODE_ENVを出力し、「エラーで落ちます」というメッセージを吐きながら終了するスクリプトです。
まずは、コマンドプロンプトでnpm startして想定どおり動くか試してみます。
> app@1.0.0 start
> node start.js
undefined
C:\Users\user-name\desktop\pm-test\node-app\start.js:3
throw new Error("エラーで落ちます");
^
Error: エラーで落ちます
at Timeout._onTimeout (C:\Users\user-name\desktop\pm-test\node-app\start.js:3:11)
at listOnTimeout (node:internal/timers:557:17)
at processTimers (node:internal/timers:500:7)
はい。
NOD_ENVは設定していないのでundefinedになっていますが、ちゃんと動いています。
ecosystem.config.jsを作成
PM2を実行するパス、C:\Users\user-name\desktop\pm-test
でpm2 init simple
を実行します。
[PM2] Spawning PM2 daemon with pm2_home=C:\Users\user-name\.pm2
[PM2] PM2 Successfully daemonized
File C:\Users\user-name\desktop\pm-test\ecosystem.config.js generated
ecosystem.config.jsが作られたとメッセージが出ました。中身を見るとさきほどのテンプレート通りの内容です。
module.exports = {
apps : [{
name : "app1",
script : "./app.js"
}]
}
ここに、さきほどのNodeアプリのパス(C:\Users\user-name\desktop\pm-test\node-app)で、npm startの実行を登録していきます。後は、NODE_ENVに"production"を設定しておきます。
module.exports = {
apps: [{
name: "app1",
cwd: "./node-app",
script: "npm",
args: "start",
env: { "NODE_ENV": "production" }
}]
}
cwdは、アプリが実行されるパスです。PM2を起動するパスからの相対パスで記載しています。scriptはドキュメントによると、「PM2 startからの相対パス」となっていますが、今回実行するのはnpmで、どこのパスにいても実行できるためこれでOKです。
実際に実行するのは、npm startですが、startはnpmの引数扱いとなります。そのため、scriptにnpm、argsにstartを指定します。
ecosystem.config.jsからPM2 start
それではconfigファイルのあるC:\Users\user-name\desktop\pm-test
で、
pm2 start ecosystem.config.js
を実行します。
statusがstoppedになっています。pm2 logs app1
でログを見てみます。
[TAILING] Tailing last 15 lines for [app1] process (change the value with --lines option)
C:\Users\user-name\.pm2\logs\app1-out.log last 15 lines:
C:\Users\user-name\.pm2\logs\app1-error.log last 15 lines:
0|app1 | C:\PROGRAM FILES\NODEJS\NPM.CMD:1
0|app1 | :: Created by npm, please don't edit manually.
0|app1 | ^
0|app1 |
0|app1 | SyntaxError: Unexpected token ':'
0|app1 | at Object.compileFunction (node:vm:352:18)
0|app1 | at wrapSafe (node:internal/modules/cjs/loader:1031:15)
0|app1 | at Module._compile (node:internal/modules/cjs/loader:1065:27)
0|app1 | at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
0|app1 | at Module.load (node:internal/modules/cjs/loader:981:32)
0|app1 | at Function.Module._load (node:internal/modules/cjs/loader:822:12)
0|app1 | at Object.<anonymous> (C:\Users\user-name\AppData\Roaming\npm\node_modules\pm2\lib\ProcessContainerFork.js:33:23)
0|app1 | at Module._compile (node:internal/modules/cjs/loader:1101:14)
0|app1 | at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
0|app1 | at Module.load (node:internal/modules/cjs/loader:981:32)
ここで滅茶苦茶ハマりました。いろいろ試したのですが、解決できなかったので割愛します。。。
どうやら、GitHubのフォーラムを見ると、Windows10で同じ事象が発生している方がいるようなので、環境をWSL2のubuntuに変えて試してみます。
ubuntu(WSL2)で試す
準備したファイルをubuntuにコピペしていきます。ファイルの内容は同じなので割愛します。ディレクトリの位置関係も同じですが、configファイルを配置するパスは/home/user-name/pm-test
、Nodeアプリのパスは/home/crypto/pm-test/node-app
です。
pm2 start ecosystem.config.js
を実行してみます。
おお。。。onlineになっています。ログも見てみます。
エラーも、NODE_ENVも"production"が出力されていることが確認できます。想定通りですね。npm start
はOSにより差異がありそうです。startの部分が引数として渡されるので、cmdとbashで処理のされ方が異なるのでしょうか。
少なくとも、現時点の結果は「Windows環境でnpm scriptをconfigファイルから起動できない」です。まだ、「Windows環境ではconfigファイルからアプリの起動が出来ない」とまでは言えません。
次は、Windowsでnpm scriptではなく、直接jsファイルの実行を試してみます。
Windowsで直接jsファイルを実行
package.jsonでstartスクリプトでnode ./start.js
を実行させていましたが、今度はconfigファイルで直接このstart.jsファイルを指定する形に変更し、試してみます。ecosystem.config.jsを次のように修正します。
module.exports = {
apps: [{
name: "app1",
cwd: "./node-app",
script: "./start.js",
env: { "NODE_ENV": "production" }
}]
}
scriptを直接実行するスクリプトのパスに書き換え、argsは削除しています。ちなみに、cwdを指定しているので、scriptのパスは「pm2 startからの相対パス」ではなく、「cwdからの相対パス」となります。このあたりはドキュメントが少々不親切に感じます。
ちなみに、scriptはnode ./start.js
と書く必要はありません。nodeとかpythonとかのインタプリタは、interpreter属性で指定します。デフォルトがnodeのため、今回は省略可能です。
pm2 start ecosystem.config.js
を実行します。
動いていますね。ログも見てみます。
[TAILING] Tailing last 15 lines for [app1] process (change the value with --lines option)
C:\Users\user-name\.pm2\logs\app1-out.log last 15 lines:
0|app1 | production
0|app1 | production
0|app1 | production
0|app1 | production
C:\Users\user-name\.pm2\logs\app1-error.log last 15 lines:
0|app1 | at Timeout._onTimeout (C:\Users\user-name\Desktop\pm-test\node-app\start.js:3:11)
0|app1 | at listOnTimeout (node:internal/timers:557:17)
0|app1 | at processTimers (node:internal/timers:500:7)
0|app1 | Error: エラーで落ちます
0|app1 | at Timeout._onTimeout (C:\Users\user-name\Desktop\pm-test\node-app\start.js:3:11)
0|app1 | at listOnTimeout (node:internal/timers:557:17)
0|app1 | at processTimers (node:internal/timers:500:7)
0|app1 | Error: エラーで落ちます
0|app1 | at Timeout._onTimeout (C:\Users\user-name\Desktop\pm-test\node-app\start.js:3:11)
0|app1 | at listOnTimeout (node:internal/timers:557:17)
0|app1 | at processTimers (node:internal/timers:500:7)
0|app1 | Error: エラーで落ちます
0|app1 | at Timeout._onTimeout (C:\Users\user-name\Desktop\pm-test\node-app\start.js:3:11)
0|app1 | at listOnTimeout (node:internal/timers:557:17)
0|app1 | at processTimers (node:internal/timers:500:7)
0|app1 | production
0|app1 | Error: エラーで落ちます
0|app1 | at Timeout._onTimeout (C:\Users\user-name\Desktop\pm-test\node-app\start.js:3:11)
0|app1 | at listOnTimeout (node:internal/timers:557:17)
0|app1 | at processTimers (node:internal/timers:500:7)
「Windows環境だとconfigファイルからの起動が一律出来ない」、という訳ではなさそうですね。
結論
Windowsだと、npm script等、一部ecosystem.config.jsからアプリの起動が出来ない場合があります。npm start、npm run devのように、引数付きのコマンドがダメなのかは不明ですが、少なくともnpm scriptは私の環境だとconfigファイルからの起動は出来ません。
PM2のnpmリポジトリを見てみると、Windowsは安定稼働すると記載がされていますが、もしかするとWindowsとPM2のバージョンの特定の組み合わせでうまく動かない場合があるのかもしれません。
参考まで、私のWindowsは21H2 ビルド19044.2486、PM2は5.2.2です。
最後に
このWebサイトを含み、基本は開発はWindows、稼働はLinuxでやってきましたが、今までOSによる違いが出たことはほとんどありませんでした。せいぜい違いが出ても、pythonを実行する時にpythonと打つかpython3と打つかくらいでした。
今回、軽い気持ちで本番適用前に検証しようと思ったら、思いの他深くハマってしまいました。 もし、WindowsでPM2が想定どおりに動かない場合は、試しにWSL2でLinuxを稼働させて検証してみると良いと思います。
幸い、サーバはLinuxなので、Windowsで検証できないだけで澄みました。これを機に、WSL2をちゃんと使ってみようと思います。