nvm環境でのpm2設定方法 の履歴(No.1)

更新


公開メモ

nvm 環境で pm2 を使う際の推奨設定

pm2 の位置づけ

pm2 はプロセスマネージャーであり、アプリケーションの外側でアプリを管理するもの。 概念的には nginx や systemd と同じ層にいる。

ただし、ユーザーレベルの nvm と親和性があり、システムグローバル、というよりもユーザーグローバルな存在になっている。

ユーザーごとに pm2 デーモンが立ち上がり、そこで複数のプロジェクトの node サーバーを管理可能。

そのため、pm2 は各プロジェクトの `package.json` に入れるのではなく、ユーザーグローバルにインストールする のがベストプラクティスとなる。

OS 起動
  └── pm2(ユーザーグローバル)
        ├── プロジェクトA(node_modules に pm2 なし)
        ├── プロジェクトB(node_modules に pm2 なし)
        └── プロジェクトC(node_modules に pm2 なし)

nvm 環境での推奨構成

pm2 を起動する Node バージョンを固定する

nvm 環境ではプロジェクトごとに(実際にはフォルダー毎に)異なる node バージョンを使い分けられる

しかしユーザーレベルで見ると pm2 デーモンは同時に1つだけ存在しており、それと異なる node 上の pm2 コマンドを叩くとバージョンの不一致が生じてしまう。

つまり pm2 を呼び出す際には常に「デーモンとして常駐する pm2 と同じ Node バージョン」を使う必要がある。

そこで、`nvm alias default` で固定された node バージョン にグローバルにインストールされた pm2 をユーザーグローバルな唯一の pm2 として利用するのが良い。

LANG:console
$ nvm alias default v24.14.1
$ npm install -g pm2   # あるいは pnpm global add pm2

pm2 自身が使う Node とアプリの Node は独立している

pm2 はアプリを子プロセスとして fork する。

起動する Node は `pm2.config.cjs` の `interpreter` で指定できる。

LANG:js
// pm2.config.cjs
module.exports = {
  apps: [{
    name: 'my-app',
    script: './index.js',
    interpreter: '/home/takeuchi/.nvm/versions/node/v20.0.0/bin/node',
  }]
}

したがって pm2 デーモン自体が v24 で動いていても、アプリは v20 で動く、といった構成が可能。 `interpreter` を省略すると PATH 上の node が使われるため、本番では明示的に指定する のが安全。

`~/bin/pm2` ラッパーで nvm を意識しない運用

プロジェクトフォルダに `.nvmrc` があって別バージョンの node が有効になっていると、そのフォルダーからユーザーグローバルの `pm2` コマンドが見えなくなる。

これを解決するために、常に default の node 環境の pm2 を呼ぶラッパースクリプト を `~/bin/pm2` として用意しておく。

LANG:bash
#!/bin/bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
nvm use default --silent # pm2 を呼び出すために PATH を設定
exec "$NVM_DIR/versions/node/$(nvm version default)/bin/pm2" "$@"
LANG:console
$ chmod +x ~/bin/pm2

このスクリプトを作ったうえで `~/bin` を PATH の先頭に置いておけば、どのプロジェクトフォルダにいても `pm2` コマンドは常に default 環境のものが呼ばれる。`$@` で全引数を透過的に渡すので、通常の pm2 コマンドがそのまま使える。

LANG: console
$ pm2 list
$ pm2 restart my-app
$ pm2 save

pm2 自体は常に default の node で動き、アプリが使う node は `interpreter` で個別指定 という役割分担が明確になる。

プロジェクトの `.nvmrc` は pm2 には無関係になり、nvm との関係をほぼ意識しなくてよくなる。

起動時の自動起動:crontab @reboot + ラッパースクリプト

ユーザーレベルの crontab を使うことにより、 pm2 で起動したプロセスを、マシンのリブート後に自動で再起動することが可能になる。

このためにはまず、必要なプロセスをすべて立ち上げた状態で、

LANG:console
$ pm2 save

を行い、プロセスリストを保存しておく。

また、起動ログを取るために

LANG:console
$ mkdir -p ~/logs

としたうえで、

$ crontab -e
@reboot /home/takeuchi/bin/pm2 resurrect >> /home/takeuchi/logs/reboot-pm2.log 2>&1

のように設定すれば、pm2 save したプロセスがマシン起動時に自動実行され、 その際のログが ~/logs/reboot-pm2.log に記録される。

ポイント

  • cron では .bashrc が読まれず nvm の PATH が設定されないが、~/bin/pm2 スクリプトが正しく PATH を設定したうえで pm2 を呼んでくれる
  • Node バージョンを変えても `nvm alias default vXX.XX.X` するだけでスクリプトは変更不要
  • `pm2 resurrect` で `~/.pm2/dump.pm2` に保存済みのプロセスをすべて復元する
  • アプリを追加・削除したら `pm2 save` し直すだけでよい
  • ログに残しておくと起動失敗時の調査に役立つ

起動タイミングについて

`@reboot` は OS 起動直後に実行されるため、ネットワークや DB がまだ立ち上がっていない場合がある。 ただし、アプリ側で接続リトライを実装しているか、pm2 の再起動設定を入れておけば実用上は問題になりにくい。

LANG:js
// pm2.config.cjs
module.exports = {
  apps: [{
    name: 'my-app',
    script: './index.js',
    max_restarts: 10,
    restart_delay: 5000,  // 再起動まで 5 秒待つ(ms)
  }]
}

「DB に繋がらないから即落ちる」作りのアプリでなければ、これで吸収できるはず。

systemd への登録について

リブート後の pm2 再起動には、 sudo が使えるユーザー限定の代替手段として、 `pm2 startup` を使って systemd サービスファイルを生成しておく方法もある。

これはこれで `journalctl` でログを確認できる、起動順を `network.target` の後にできるといったメリットはあるのだが・・・

systemd に焼き込まれるのは実行時のフルパス なので:

ExecStart=/home/takeuchi/.nvm/versions/node/v24.14.1/bin/pm2 resurrect

後から `nvm alias default` を変えても systemd は古いパスを呼び続ける。

日常の pm2 操作はログインシェルで直接行うことがほとんどなので、systemd で pm2 プロセス自体を管理できるメリットもあまり実感しにくい。

nvm を使っている環境では systemd ではなく、crontab + ラッパースクリプトの方が追従性・シンプルさの面で素直 と思われる。

まとめ

ユーザー takeuchi
  └── nvm default (v24) の node + pm2(グローバル)
        ├── ~/bin/pm2 ラッパー経由で常に default の pm2 を呼ぶ
        └── crontab @reboot + ラッパースクリプトで自動起動
              └── ~/.pm2/ がそのユーザーの管理空間
                    └── 各アプリは interpreter で Node バージョンを個別指定
  • pm2 はグローバルに1つ、プロジェクトには入れない
  • pm2 用 Node バージョンは `nvm alias default` で固定
  • アプリが使う Node バージョンは `pm2.config.cjs` の `interpreter` で明示
  • `~/bin/pm2` ラッパーにより、どのフォルダからでも nvm を意識せず pm2 を呼べる
  • 自動起動は crontab + ラッパースクリプトが nvm 環境では素直
  • 起動タイミング問題は pm2 の `max_restarts` + `restart_delay` で吸収する

コメント・質問





Counter: 17 (from 2010/06/03), today: 2, yesterday: 15