以下の環境でGrowiを利用。

  • Growi v7.4.1
  • node v20.10.2
  • Ubuntu 24.04
  • Growi実行環境 /home/www-data/growi
  • Growi実行ユーザ:root

v7.4.1で以下の問題点にぶつかったため、growiのスタートアップスクリプトとsystemdで対処したときのメモです。

問題点

  • daemon-reload の遅延: 設定反映に約5分を要していました。
  • 起動プロセスの停滞: サービス開始から実際にアクセス可能になるまで約6分かかっていました。(以前は数秒)
  • 不安定な運用: 異常終了時の自動再起動設定がなく、ログも標準出力のみで追跡が困難でした。

旧設定

  • /etc/systemd/system/growi.service
[Unit]
Description = growi
After=network-online.target mongod.service
After=network.target elasticsearch.service
ConditionPathExists=/home/www-data/growi

[Service]
ExecStart=/home/www-data/growi/growi-start.sh
Restart=no
Type=simple

[Install]
WantedBy=multi-user.target
  • /home/www-data/growi/growi-start.sh
#!/bin/bash

# NVM environmentをロード (NVM_DIRを直接指定)
export NVM_DIR="/root/.nvm" # $HOMEの代わりに直接パスを指定
if [ -s "$NVM_DIR/nvm.sh" ]; then
  \. "$NVM_DIR/nvm.sh"  # nvmをロード
  # 次の行でスクリプト実行時のnodeとnpmのバージョンをログに出力
  echo "NVM for GROWI startup script loaded. Using Node version: $(node -v), npm version: $(npm -v)" > /tmp/growi_nvm_load.log
else
  # NVMが見つからない場合もログに出力
  echo "NVM_DIR ($NVM_DIR) not found or nvm.sh not found for GROWI startup script." > /tmp/growi_nvm_load.log
fi

cd /home/www-data/growi
NODE_ENV=production \
AUDIT_LOG_ENABLED=true \
FORCE_WIKI_MODE=private \
MONGO_URI=mongodb://localhost:27017/growi \
ELASTICSEARCH_URI=http://localhost:9200/growi \
REDIS_URI=redis://localhost:6379 \
PASSWORD_SEED=password \
npm run app:server

原因分析

以下、分析はGemini。

  1. systemdの過負荷: ConditionPathExists が大規模なディレクトリ(growi)をチェックする際、OSレベルでスキャン待ちが発生していた可能性。
  2. NVMの初期化コスト: 起動のたびに nvm.sh を読み込んでいた。これは数百行のシェルスクリプトを実行する処理であり、本番環境のサービス起動としては非常に重い。
  3. プロセスの二重管理: シェルスクリプトが npm プロセスを「子プロセス」として抱えていたため、systemdからの制御効率が悪かった。

何が問題だったのか(ボトルネックの正体)

今回の事象で最大の問題は、「本番環境のサービス起動に、開発環境のような動的な初期化プロセスを組み込んでいたこと」にありました。

具体的には、以下の3つの「待ち」が連鎖していました。

  1. システムチェックによる停滞 (ConditionPathExists) systemdのユニットファイルでGROWIのインストールディレクトリをチェックしていましたが、node_modules を含む膨大なファイル群をOSレベルでスキャンしに行った際、I/O待ちやカーネルレベルのオーバーヘッドが発生し、daemon-reload や起動そのものを著しく遅延させていました。
  2. シェルスクリプトによる二重起動のオーバーヘッド 起動のたびに nvm.sh をロード(source)し、Node.jsのバージョン判定を動的に行っていました。これは開発時には便利ですが、本番サービスとしては数百行のシェルスクリプトを毎回実行することになり、CPUリソースと時間を無駄に消費していました。
  3. プロセスの「親子関係」の不備 systemdから見ると、管理対象が「GROWI本体」ではなく「起動用のシェルスクリプト」になっていました。このため、GROWIが内部でハングアップしてもsystemdが検知できず、再起動もかからないという「運用上の死角」が生まれていました。

これを是正した設定ファイル

設定の前に!

  • 設定ファイルのバックアップ
sudo cp -pi /etc/systemd/system/growi.service /path/to/backup/growi.service.$(date +%Y%m%d)
sudo cp -pi /home/www-data/growi/growi-start.sh /path/to/backup/growi-start.sh.$(date +%Y%m%d)
  • diffによるバックアップ確認
sudo diff -u /path/to/backup/growi.service.$(date +%Y%m%d) /etc/systemd/system/growi.service 
sudo diff -u /path/to/backup/growi-start.sh.$(date +%Y%m%d) /home/www-data/growi/growi-start.sh

新しいファイル本体

  • /etc/systemd/system/growi.service
[Unit]
Description=GROWI Service
After=network-online.target mongod.service elasticsearch.service redis.service
Wants=network-online.target

[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/home/www-data/growi
ExecStart=/bin/bash /home/www-data/growi/growi-start.sh
Restart=always
RestartSec=10
StandardOutput=append:/var/log/growi.log
StandardError=append:/var/log/growi-error.log

[Install]
WantedBy=multi-user.target
  • /home/www-data/growi/growi-start.sh
#!/bin/bash

# Node.jsバイナリへのパスを直接追加 (nvm.shのロードを回避して高速化)
export PATH="/root/.nvm/versions/node/v20.19.2/bin:$PATH"
GROWI_DIR="/home/www-data/growi"

cd $GROWI_DIR

# 環境変数の設定
export NODE_ENV=production
export AUDIT_LOG_ENABLED=true
export FORCE_WIKI_MODE=private
export MONGO_URI=mongodb://localhost:27017/growi
export ELASTICSEARCH_URI=http://localhost:9200/growi
export REDIS_URI=redis://localhost:6379
export PASSWORD_SEED=password

# execにより、このシェル自体をnpmプロセスに切り替える
exec npm run app:server

※このpasswordは、旧設定をそのまま利用します。でない場合、「Growiにログインできない」という地獄が待っています。

ファイル差し替え後の挙動

  • systemdリロード
sudo systemctl daemon-reload
  • growi再起動
sudo systemctl restart growi.service
  • growi再起動確認
systemctl status growi.service

active(running) を確認します。

その後、

  1. growiが起動する
  2. 新しいセッション(ゲストセッション)で管理者アカウントにログインできる
  3. 一通りの操作 (Wikiページの作成や編集)が行えればOKです。

設定の比較

■ systemd ユニットファイル (growi.service)

項目旧設定 (遅延の原因)新設定 (最適化済)
依存関係Afterが分散、Redisの指定なしAfter/WantsにRedis含め統合
パスチェックConditionPathExists (5分停滞の疑い)削除(高速化に寄与)
実行ユーザ指定なし (デフォルト)User=root / Group=root 明示
作業ディレクトリスクリプト内で cdWorkingDirectory で定義
再起動設定Restart=no (手動復旧が必要)Restart=always (10秒後に自動復旧)
ログ管理標準出力のみ (systemdログに混在)/var/log/growi.log に直接出力

■ 起動スクリプト (growi-start.sh)

項目旧設定 (遅延の原因)新設定 (最適化済)
Node環境構築source nvm.sh (数秒〜数十秒のロス)PATH を直接追加 (0秒)
環境変数\(バックスラッシュ)連結 (ミスしやすい)export 方式 (確実で読みやすい)
実行コマンドnpm run app:server (子プロセスとして実行)exec npm... (プロセスを置き換え)

4. 対処方法のポイント

  • 「動的な環境構築」から「静的なパス指定」へ: 本番サーバでは nvm を毎回読み込む必要はありません。パスを直接通すことが最速の解決策でした。
  • systemdの責務を明確にする: ディレクトリの存在チェックやパス移動はスクリプトではなく、ユニットファイルの WorkingDirectory 等に任せることで、systemdの管理サイクルが正常化しました。
  • プロセスの直結 (exec): OS (systemd) -> Bash -> npm となっていた階層を、exec で OS (systemd) -> npm に直結させたことで、シグナルの伝達やメモリ効率が改善しました。

今後のメンテナンス

Node.jsのバージョンを変更した際のみ、growi-start.sh 内の v20.19.2 というパス文字列を書き換えるだけで対応可能です。