initd-foreverでNode.jsアプリをデーモン化する

2015.12.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、大瀧です。

最近Node.jsで作ったアプリをシステム起動時に自動起動させることが良くあるのですが、/etc/rc.localnode <SCRIPT_NAME>と直書きするのも使い勝手が良くないかなと思い、foreverとSysVinitを連携させるinitd-foreverというツールを試してみたのでレポートします。

initd-foreverとは

Node.jsのプロセスマネージャーとしてはforeverが著名ですが、Linuxの枯れたシステム管理サービスのSysVinitからforeverをキックするスクリプトを生成するのがinitd-foreverです。foreverと同じく、npmでインストールします。

$ npm install -g forever initd-forever
npm WARN optional dep failed, continuing fsevents@1.0.6
/home/vagrant/.nvm/versions/node/v0.12.9/bin/initd-forever -> /home/vagrant/.nvm/versions/node/v0.12.9/lib/node_modules/initd-forever/bin/initd-forever.js
/home/vagrant/.nvm/versions/node/v0.12.9/bin/forever -> /home/vagrant/.nvm/versions/node/v0.12.9/lib/node_modules/forever/bin/forever
initd-forever@0.1.8 /home/vagrant/.nvm/versions/node/v0.12.9/lib/node_modules/initd-forever
├── commander@2.3.0
├── colors@1.1.2
└── swig@1.4.2 (optimist@0.6.1, uglify-js@2.4.24)

forever@0.15.1 /home/vagrant/.nvm/versions/node/v0.12.9/lib/node_modules/forever
├── path-is-absolute@1.0.0
├── object-assign@3.0.0
├── colors@0.6.2
├── clone@1.0.2
├── timespan@2.3.0
├── optimist@0.6.1 (wordwrap@0.0.3, minimist@0.0.10)
├── nssocket@0.5.3 (eventemitter2@0.4.14, lazy@1.0.11)
├── shush@1.0.0 (strip-json-comments@0.1.3, caller@0.0.1)
├── cliff@0.1.10 (eyes@0.1.8, colors@1.0.3)
├── utile@0.2.1 (deep-equal@1.0.1, ncp@0.4.2, async@0.2.10, i@0.3.3, mkdirp@0.5.1, rimraf@2.4.4)
├── winston@0.8.3 (cycle@1.0.3, eyes@0.1.8, stack-trace@0.0.9, isstream@0.1.2, async@0.2.10, pkginfo@0.3.1)
├── nconf@0.6.9 (ini@1.3.4, async@0.2.9, optimist@0.6.0)
├── prettyjson@1.1.3 (colors@1.1.2, minimist@1.2.0)
├── flatiron@0.4.3 (optimist@0.6.0, director@1.2.7, prompt@0.2.14, broadway@0.3.6)
└── forever-monitor@1.6.0 (minimatch@2.0.10, ps-tree@0.0.3, broadway@0.3.6, chokidar@1.4.1)
$

initd-foreverコマンドで実行します。まずは--helpオプションでヘルプを見てみましょう。

$ initd-forever --help

  Usage: initd-forever [options]

  Options:

    -h, --help             output usage information
    -V, --version          output the version number
    -a, --app [path]       Path to node.js main file
    -c, --command [value]  Command to execute on main file
    -e, --env [value]      Export NODE_ENV with value
    -l, --logfile [path]   Logs the daemon output to LOGFILE
    -n, --name [value]     Application name
    -p, --pidfile [path]   The pid file
    -m, --monit [value]    Generate the monit script file with the listen port number
    -f, --forever [value]  The location of forever
$

foreverのオプションと同じような項目が並んでいますね。サンプル/home/vagrant/index.jsを用意して、スクリプトを生成してみます。

$ initd-forever -a /home/vagrant/index.js -n samplenode
Script daemon file saved to samplenode
$ ls
index.js  samplenode
$

カレントディレクトリに、-nで指定したサービス名のスクリプトが生成されました。中身はこんな感じです。

/etc/init.d/samplenode

#!/bin/bash
### BEGIN INIT INFO
# If you wish the Daemon to be lauched at boot / stopped at shutdown :
#
#    On Debian-based distributions:
#      INSTALL : update-rc.d scriptname defaults
#      (UNINSTALL : update-rc.d -f  scriptname remove)
#
#    On RedHat-based distributions (CentOS, OpenSUSE...):
#      INSTALL : chkconfig --level 35 scriptname on
#      (UNINSTALL : chkconfig --level 35 scriptname off)
#
# chkconfig:         2345 90 60
# Provides:          /home/vagrant/index.js
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: forever running /home/vagrant/index.js
# Description:       /home/vagrant/index.js
### END INIT INFO
#
# initd a node app
# Based on a script posted by https://gist.github.com/jinze at https://gist.github.com/3748766
#

if [ -e /lib/lsb/init-functions ]; then
	# LSB source function library.
	. /lib/lsb/init-functions
fi;

pidFile="/var/run/samplenode.pid"
logFile="/var/run/samplenode.log"

command="node"
nodeApp="/home/vagrant/index.js"
foreverApp="forever"

start() {
	echo "Starting $nodeApp"

	# Notice that we change the PATH because on reboot
   # the PATH does not include the path to node.
   # Launching forever with a full path
   # does not work unless we set the PATH.
   PATH=/usr/local/bin:$PATH
	export NODE_ENV=production
   #PORT=80
   $foreverApp start --pidFile $pidFile -l $logFile -a -d -c "$command" $nodeApp
   RETVAL=$?
}

restart() {
	echo -n "Restarting $nodeApp"
	$foreverApp restart $nodeApp
	RETVAL=$?
}

stop() {
	echo -n "Shutting down $nodeApp"
   $foreverApp stop $nodeApp
   RETVAL=$?
}

status() {
   echo -n "Status $nodeApp"
   $foreverApp list
   RETVAL=$?
}

case "$1" in
   start)
        start
        ;;
    stop)
        stop
        ;;
   status)
        status
       ;;
   restart)
   	restart
        ;;
	*)
       echo "Usage:  {start|stop|status|restart}"
       exit 1
        ;;
esac
exit $RETVAL

5〜11行目にDebianとRedHatそれぞれのコメントがありますので、SysVinitが採用されている多くのディストリビューションで使えそうですね。今回はDebian Wheezyを利用しているので、/etc/init.dにスクリプトを配置し、insservコマンドでサービスの有効化を行います。

$ sudo mv samplenode /etc/init.d/
$ chmod +x /etc/init.d/samplenode
$ sudo insserv -d samplenode
$ ls /etc/rc?.d/*samplenode
/etc/rc0.d/K01samplenode  /etc/rc2.d/S17samplenode  /etc/rc4.d/S17samplenode  /etc/rc6.d/K01samplenode
/etc/rc1.d/K01samplenode  /etc/rc3.d/S17samplenode  /etc/rc5.d/S17samplenode
$

ランレベル2〜5でS17samplenodeとなっており、有効になっていることがわかりますね。

いろいろ補足

自動生成されたスクリプトではいくつか不備がありました。フォローしていきます。

nvm環境の場合

nvm環境ではnvmの初期化スクリプトの読み込みが必要ですので、35行目あたりに追加すると良いでしょう。

pidFile="/var/run/samplenode.pid"
logFile="/var/run/samplenode.log"

# load nvm
export NVM_DIR="/home/vagrant/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm

command="node"
nodeApp="/home/vagrant/index.js"

statusやstopを動くようにする

生成されたスクリプトではstartは正常に動作するのですが、statusstopが動きません。これは、foreverのプロセス管理ファイルを格納するディレクトリが設定されていないためのようなので、ディレクトリを作成し、環境変数FOREVER_ROOTをスクリプト内に指定します。

$ sudo mkdir /etc/forever
# set FOREVER_ROOT
export FOREVER_ROOT="/etc/forever"

依存サービスの調整

実行するアプリケーションによっては他のサービスに依存する場合があると思います。Debian/UbuntuであればRequired-Start/Required-Stopにサービス名を追加しましょう。例えばアプリケーションからBLEデバイスにアクセスするのであればbluetoothサービスに依存するので、以下のように追記します。

# Required-Start:    $remote_fs $syslog bluetooth
# Required-Stop:     $remote_fs $syslog bluetooth

こうすることで、bluetoothサービスが起動した後でアプリケーションが実行されるよう、/etc/rc?.d/以下のファイルの番号が調整されます。

追記前

$ ls /etc/rc2.d/
README   S13rpcbind     S16rsyslog  S17acpid  S17cron  S17exim4  S17samplenode  S18avahi-daemon  S19bootlogs  S19saned     S20rmnologin
S01motd  S14nfs-common  S16sudo     S17atd    S17dbus  S17rsync  S17ssh         S18bluetooth     S19cups      S20rc.local
$

追記後

$ sudo insserv -r samplenode
$ sudo insserv -d samplenode
$ ls /etc/rc2.d/
README   S13rpcbind     S16rsyslog  S17acpid  S17cron  S17exim4  S17ssh           S18bluetooth  S19cups        S19saned     S20rmnologin
S01motd  S14nfs-common  S16sudo     S17atd    S17dbus  S17rsync  S18avahi-daemon  S19bootlogs   S19samplenode  S20rc.local
$

17番から19番(S18bluetoothの後)になっているのがわかりますね。

RedHat系ディストリビューションの場合は、chkconfig行の起動時の番号(以下の90)と終了時の番号(以下の60)を自分で調整しましょう。

# chkconfig:         2345 90 60

まとめ

いろいろ補足事項はありましたが、さくっとSysVinit互換のスクリプトを生成するinitd-foreverは扱いやすい印象を持ちました。systemdでも同じような話が出てくると思うので、早めに検証/習得しておきたいですね。

参考URL