T-CREATOR

Node.js スクリプトからサービスへ:systemd や pm2 による常駐運用

Node.js スクリプトからサービスへ:systemd や pm2 による常駐運用

Node.js アプリケーションを開発中は「node app.js」で簡単に動作確認できますが、本番環境では話が変わります。サーバーの再起動やプロセスの異常終了があっても、アプリケーションが自動で復旧し続ける必要があります。

今回は、Node.js スクリプトを安定した常駐サービスに変身させる方法をご紹介します。systemd と pm2 という 2 つの強力なツールを使って、あなたのアプリケーションを本格的な本番運用レベルまで引き上げていきましょう。

背景

Node.js スクリプトの基本実行方法

通常、Node.js アプリケーションは以下のような方法で実行されます。

javascript// app.js - シンプルなWebサーバー
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

実行は非常にシンプルです。

bash# 基本的な実行方法
node app.js

# または package.json の scripts を使用
yarn start

しかし、この実行方法では重要な問題があります。ターミナルを閉じたり、SSH 接続が切れると、プロセスも一緒に終了してしまうのです。

開発環境と本番環境の違い

開発環境と本番環境では、求められる要件が大きく異なります。

項目開発環境本番環境
可用性開発中は停止 OK24 時間 365 日稼働
復旧手動再起動で問題なし自動復旧が必須
ログコンソール出力で十分ファイル保存・ローテーション
モニタリング不要リソース監視・アラート
セキュリティ緩い制限厳格な権限管理

以下の図は、開発から本番への移行で必要となる要素を示しています。

mermaidflowchart TB
    dev[開発環境] -->|移行| prod[本番環境]
    dev --> manual[手動実行]
    dev --> console[コンソール出力]
    dev --> simple[シンプル構成]

    prod --> auto[自動起動]
    prod --> logs[ログ管理]
    prod --> monitor[監視]
    prod --> security[セキュリティ]

    auto --> systemd[systemd]
    auto --> pm2[pm2]
    logs --> rotation[ローテーション]
    monitor --> metrics[メトリクス]
    security --> user[専用ユーザー]

本番環境では、アプリケーションが予期せず停止した場合の自動復旧、適切なログ管理、リソース監視など、多くの追加要件が発生します。

プロセス管理の重要性

Node.js アプリケーションの本番運用では、以下の理由からプロセス管理が極めて重要になります。

まず、単一プロセスの脆弱性です。Node.js はシングルスレッドで動作するため、一つのエラーでアプリケーション全体が停止する可能性があります。

javascript// 未処理の例外でプロセス全体が停止
process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  // プロセス終了を避けるためのハンドリング
  process.exit(1);
});

次に、リソース管理の課題があります。メモリリークや CPU 使用率の急増など、長時間稼働することで発生する問題への対処が必要です。

さらに、可用性の要求として、サーバーメンテナンスやアップデート時でも、サービスの継続性を保つ仕組みが求められます。

課題

スクリプトの手動実行による運用の限界

手動で Node.js スクリプトを実行する従来の方法には、いくつかの深刻な制約があります。

ターミナル依存の問題

bash# SSH経由で実行した場合
ssh user@server
node app.js
# SSH接続が切れると、プロセスも終了してしまう

この問題を回避するため、nohupscreen を使用することもありますが、根本的な解決にはなりません。

bash# nohup を使った実行(一時的な解決策)
nohup node app.js > app.log 2>&1 &

# バックグラウンドプロセスの確認
ps aux | grep node

運用作業の煩雑さも大きな課題です。サーバーの再起動後、手動でアプリケーションを起動し忘れることが頻繁に発生します。また、複数のアプリケーションを管理する場合、起動順序や依存関係の管理が困難になります。

プロセス停止時の自動復旧問題

Node.js アプリケーションは様々な理由で予期せず停止することがあります。

メモリ不足による停止

javascript// メモリリークの例
const data = [];
setInterval(() => {
  // メモリを消費し続ける処理
  data.push(new Array(1000000).fill('memory leak'));
}, 100);

未処理の例外

javascript// 未処理のPromise rejection
async function riskyOperation() {
  throw new Error('Something went wrong');
}

// この呼び出しで unhandledRejection が発生
riskyOperation();

こうした停止が発生した場合、手動運用では以下の問題が生じます:

  • 停止の検知遅れ:システム管理者が気づくまでサービスが停止
  • 復旧の遅延:手動での再起動作業に時間がかかる
  • ビジネス影響:サービス停止による機会損失

ログ管理とモニタリングの困難さ

手動実行では、適切なログ管理とモニタリングの実現が困難です。

ログ出力の問題

javascript// コンソール出力のみでは不十分
console.log('User login:', userId);
console.error('Database connection failed');

// ファイル出力も手動で実装が必要
const fs = require('fs');
const logMessage = `${new Date().toISOString()} - Error occurred\n`;
fs.appendFileSync('app.log', logMessage);

モニタリングの課題

以下の図は、手動運用時のモニタリングの問題点を示しています。

mermaidstateDiagram-v2
    [*] --> Running: アプリ起動
    Running --> Error: 例外発生
    Error --> Stopped: プロセス終了
    Stopped --> [*]: サービス停止

    Running --> MemoryLeak: メモリリーク
    MemoryLeak --> OutOfMemory: メモリ不足
    OutOfMemory --> Stopped

    note right of Error : 自動復旧なし
    note right of Stopped : 手動確認が必要

手動運用では、アプリケーションの状態をリアルタイムで把握することができず、問題の早期発見と対処が困難になります。

解決策

Node.js アプリケーションの常駐化には、主に 2 つのアプローチがあります。それぞれ異なる特徴と利点を持っているため、用途に応じて選択することが重要です。

systemd による OS レベルでのサービス化

systemd は、現代的な Linux ディストリビューションで標準的に採用されているシステム・サービス管理ツールです。

systemd の主な特徴

  • OS レベルでの統合管理
  • 依存関係の自動解決
  • 標準的な Linux 環境での動作保証
  • systemctl コマンドによる統一的な操作
bash# systemd サービスの基本操作
sudo systemctl start myapp      # サービス開始
sudo systemctl stop myapp       # サービス停止
sudo systemctl restart myapp    # サービス再起動
sudo systemctl status myapp     # ステータス確認
sudo systemctl enable myapp     # 自動起動有効化

systemd を使用することで、Node.js アプリケーションを OS の他のサービス(nginx、postgres など)と同等に扱えるようになります。

pm2 による Node.js 特化型プロセス管理

pm2 は Node.js アプリケーション専用に設計されたプロセス管理ツールです。

pm2 の主な特徴

  • Node.js に特化した豊富な機能
  • クラスター機能による負荷分散
  • ホットリロード対応
  • 詳細なモニタリング機能
bash# pm2 の基本操作
pm2 start app.js                # アプリ開始
pm2 stop app                    # アプリ停止
pm2 restart app                 # アプリ再起動
pm2 list                        # プロセス一覧
pm2 monit                       # リアルタイム監視

以下の図は、systemd と pm2 のアーキテクチャの違いを示しています。

mermaidflowchart TB
    subgraph systemd_arch[systemd アーキテクチャ]
        linux_kernel[Linux Kernel]
        systemd[systemd]
        node_service[Node.js Service]

        linux_kernel --> systemd
        systemd --> node_service
    end

    subgraph pm2_arch[pm2 アーキテクチャ]
        linux_kernel2[Linux Kernel]
        pm2_daemon[PM2 Daemon]
        node_cluster[Node.js Cluster]
        node1[Node.js Process 1]
        node2[Node.js Process 2]
        node3[Node.js Process N]

        linux_kernel2 --> pm2_daemon
        pm2_daemon --> node_cluster
        node_cluster --> node1
        node_cluster --> node2
        node_cluster --> node3
    end

pm2 は Node.js プロセスの上位層で動作し、複数のプロセスインスタンスを管理できるのが特徴です。

各ツールの特徴と使い分け

適切なツールの選択は、プロジェクトの要件とインフラ環境によって決まります。

観点systemdpm2
学習コスト中程度低い
設定の複雑さ中程度簡単
OS 統合優秀限定的
Node.js 特化機能基本的豊富
クラスター手動設定自動
モニタリング基本的詳細
ログ管理journald 統合独自ログ

systemd が適している場面

  • システム全体の一貫した管理が必要
  • 他のシステムサービスとの連携が重要
  • セキュリティ要件が厳格
  • 長期間の安定運用が最優先

pm2 が適している場面

  • Node.js 開発チームでの運用
  • 頻繁なデプロイとアップデート
  • クラスター機能の活用が必要
  • 詳細なアプリケーション監視が重要

具体例

実際の Node.js アプリケーションを使って、systemd と pm2 それぞれの実装方法を詳しく見ていきましょう。

サンプルアプリケーションの準備

まず、共通のサンプルアプリケーションを準備します。

javascript// app.js - Express を使用したWebアプリケーション
const express = require('express');
const os = require('os');
const app = express();
const PORT = process.env.PORT || 3000;

// ヘルスチェック用エンドポイント
app.get('/health', (req, res) => {
  res.json({
    status: 'OK',
    timestamp: new Date().toISOString(),
    pid: process.pid,
    hostname: os.hostname(),
    uptime: process.uptime(),
  });
});

// メインエンドポイント
app.get('/', (req, res) => {
  res.json({
    message: 'Node.js Service is running!',
    pid: process.pid,
    environment: process.env.NODE_ENV || 'development',
  });
});

// グレースフルシャットダウンの実装
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  server.close(() => {
    console.log('Process terminated');
    process.exit(0);
  });
});

const server = app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`PID: ${process.pid}`);
});

package.json も準備しておきます。

json{
  "name": "nodejs-service-demo",
  "version": "1.0.0",
  "description": "Node.js service demonstration",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

systemd での実装

サービスファイルの作成

systemd サービスファイルを作成します。これはアプリケーションの設定を定義する重要なファイルです。

bash# サービスファイルの配置先ディレクトリを確認
sudo ls -la /etc/systemd/system/

サービスファイルを作成します:

ini# /etc/systemd/system/nodejs-app.service
[Unit]
Description=Node.js Web Application
Documentation=https://example.com/docs
After=network.target
Wants=network.target

[Service]
Type=simple
User=nodeuser
Group=nodeuser
WorkingDirectory=/opt/nodejs-app
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node app.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodejs-app

# セキュリティ設定
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/nodejs-app

[Install]
WantedBy=multi-user.target

各設定項目の説明:

設定項目説明
Type=simpleプロセスがフォアグラウンドで実行
User​/​Group実行ユーザー・グループ
WorkingDirectory作業ディレクトリ
Environment環境変数の設定
Restart=on-failure失敗時の自動再起動
RestartSec=5再起動までの待機時間

専用ユーザーとディレクトリの準備

セキュリティのため、専用ユーザーでアプリケーションを実行します。

bash# 専用ユーザーの作成
sudo useradd --system --shell /bin/false nodeuser

# アプリケーションディレクトリの作成
sudo mkdir -p /opt/nodejs-app
sudo chown nodeuser:nodeuser /opt/nodejs-app

# アプリケーションファイルのコピー
sudo cp app.js package.json /opt/nodejs-app/
sudo chown nodeuser:nodeuser /opt/nodejs-app/*

依存関係のインストール:

bash# アプリケーションディレクトリに移動
cd /opt/nodejs-app

# 本番用依存関係のインストール
sudo -u nodeuser yarn install --production

自動起動設定

systemd でサービスを有効化し、自動起動を設定します。

bash# サービスファイルの読み込み
sudo systemctl daemon-reload

# サービスの有効化(自動起動設定)
sudo systemctl enable nodejs-app

# サービスの開始
sudo systemctl start nodejs-app

# ステータス確認
sudo systemctl status nodejs-app

正常に動作している場合、以下のような出力が表示されます:

bash● nodejs-app.service - Node.js Web Application
   Loaded: loaded (/etc/systemd/system/nodejs-app.service; enabled; vendor preset: enabled)
   Active: active (running) since Sun 2024-01-15 10:30:45 UTC; 2min ago
     Docs: https://example.com/docs
 Main PID: 1234 (node)
    Tasks: 11 (limit: 1152)
   Memory: 25.6M
   CGroup: /system.slice/nodejs-app.service
           └─1234 /usr/bin/node app.js

ログ管理

systemd は journald と統合されているため、ログ管理が簡単です。

bash# リアルタイムログの確認
sudo journalctl -u nodejs-app -f

# 最新100行のログ表示
sudo journalctl -u nodejs-app -n 100

# 特定期間のログ表示
sudo journalctl -u nodejs-app --since "2024-01-15 10:00:00"

# ログのフィルタリング
sudo journalctl -u nodejs-app --grep "ERROR"

pm2 での実装

pm2 インストールと基本設定

pm2 をグローバルにインストールします。

bash# pm2 のグローバルインストール
yarn global add pm2

# または npm を使用
npm install -g pm2

# インストール確認
pm2 --version

基本的なアプリケーション起動:

bash# シンプルな起動
pm2 start app.js

# 名前を指定して起動
pm2 start app.js --name "web-app"

# プロセス一覧の確認
pm2 list

設定ファイルによる管理

pm2 では設定ファイル(ecosystem.config.js)を使用して、より詳細な設定を行えます。

javascript// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'nodejs-web-app',
      script: 'app.js',
      cwd: '/opt/nodejs-app',
      instances: 'max',
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      log_file: '/var/log/pm2/nodejs-app.log',
      error_file: '/var/log/pm2/nodejs-app-error.log',
      out_file: '/var/log/pm2/nodejs-app-out.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      max_memory_restart: '500M',
      restart_delay: 5000,
      max_restarts: 10,
      min_uptime: '10s',
    },
  ],
};

設定ファイルを使用したアプリケーション起動:

bash# 設定ファイルでアプリケーション起動
pm2 start ecosystem.config.js --env production

# 設定の再読み込み
pm2 reload ecosystem.config.js --env production

クラスター運用

pm2 の最大の利点の一つは、簡単にクラスター構成を組めることです。

bash# CPU コア数に応じたクラスター起動
pm2 start app.js -i max --name "web-cluster"

# 特定インスタンス数での起動
pm2 start app.js -i 4 --name "web-cluster"

# クラスターの状態確認
pm2 show web-cluster

以下の図は、pm2 クラスター構成を示しています。

mermaidflowchart TD
    client[クライアント] --> loadbalancer[PM2 Load Balancer]
    loadbalancer --> instance1[Node.js Instance 1]
    loadbalancer --> instance2[Node.js Instance 2]
    loadbalancer --> instance3[Node.js Instance 3]
    loadbalancer --> instance4[Node.js Instance 4]

    subgraph pm2_process[PM2 Process Management]
        pm2_daemon[PM2 Daemon]
        pm2_daemon --> instance1
        pm2_daemon --> instance2
        pm2_daemon --> instance3
        pm2_daemon --> instance4
    end

    pm2_daemon --> logs[ログ管理]
    pm2_daemon --> monitoring[監視・メトリクス]

pm2 は自動的にリクエストを各インスタンスに分散し、一つのインスタンスに問題が発生しても他のインスタンスでサービスを継続できます。

監視とデプロイ

pm2 には強力な監視機能が備わっています。

bash# リアルタイム監視ダッシュボード
pm2 monit

# プロセス情報の詳細表示
pm2 show nodejs-web-app

# ログのリアルタイム表示
pm2 logs

# 特定アプリのログ表示
pm2 logs nodejs-web-app

# メモリ使用量の確認
pm2 jlist | jq '.[0].pm2_env.memory'

システム起動時の自動開始

pm2 をシステム起動時に自動で開始するように設定します。

bash# 起動スクリプトの生成
pm2 startup

# 現在の pm2 プロセス一覧を保存
pm2 save

# 設定の確認
pm2 list

デプロイメントを簡素化するスクリプトも作成できます:

bash#!/bin/bash
# deploy.sh - 簡単なデプロイスクリプト

echo "Deploying nodejs-web-app..."

# アプリケーションの停止
pm2 stop nodejs-web-app

# 最新コードの取得
git pull origin main

# 依存関係の更新
yarn install --production

# アプリケーションの再起動
pm2 restart nodejs-web-app

echo "Deployment completed!"

まとめ

各手法の比較と選択指針

Node.js アプリケーションの常駐化において、systemd と pm2 それぞれに明確な利点があります。

systemd の利点

  • OS レベルでの統合管理により、他のシステムサービスとの一貫性
  • セキュリティ機能が充実(PrivateTmp、ProtectSystem など)
  • journald との統合による標準的なログ管理
  • 依存関係の明確な定義が可能

pm2 の利点

  • Node.js に特化した豊富な機能
  • 簡単なクラスター構成とロードバランシング
  • 詳細なアプリケーション監視とメトリクス
  • ホットリロードやゼロダウンタイムデプロイ

以下の図は、選択の判断基準を示しています。

mermaidflowchart TD
    start[プロジェクト要件の確認] --> team_size{チームサイズ}

    team_size -->|小規模・個人| simple_needs{シンプルな要件?}
    team_size -->|中〜大規模| complex_needs{複雑な要件?}

    simple_needs -->|Yes| systemd_choice[systemd を選択]
    simple_needs -->|No| consider_features{必要な機能}

    complex_needs -->|Yes| pm2_choice[pm2 を選択]
    complex_needs -->|No| hybrid_choice[ハイブリッド構成]

    consider_features -->|クラスター必要| pm2_choice
    consider_features -->|OS統合重視| systemd_choice

    systemd_choice --> implement_systemd[systemd で実装]
    pm2_choice --> implement_pm2[pm2 で実装]
    hybrid_choice --> implement_hybrid[systemd + pm2]

選択の判断基準

  1. チーム規模と専門性

    • 小規模・インフラ重視 → systemd
    • 中〜大規模・Node.js 重視 → pm2
  2. 運用要件

    • シンプルで安定 → systemd
    • 柔軟で高機能 → pm2
  3. 既存インフラ

    • システム管理者主導 → systemd
    • 開発者主導 → pm2

運用時の注意点

どちらの手法を選択しても、以下の点に注意することが重要です。

リソース監視

javascript// メモリ使用量の監視例
const used = process.memoryUsage();
console.log('Memory usage:', {
  rss: Math.round(used.rss / 1024 / 1024) + 'MB',
  heapTotal:
    Math.round(used.heapTotal / 1024 / 1024) + 'MB',
  heapUsed: Math.round(used.heapUsed / 1024 / 1024) + 'MB',
});

ログローテーション: 長期運用では、ログファイルのサイズ管理が重要になります。

bash# logrotate の設定例(systemd の場合)
# /etc/logrotate.d/nodejs-app
/var/log/nodejs-app/*.log {
    daily
    missingok
    rotate 30
    compress
    notifempty
    sharedscripts
    postrotate
        systemctl reload nodejs-app
    endscript
}

セキュリティ更新: 定期的な Node.js と依存パッケージの更新を計画的に実施しましょう。

bash# 依存関係の脆弱性チェック
yarn audit

# 自動更新スクリプトの例
#!/bin/bash
# update.sh
yarn upgrade
yarn audit --audit-level high
pm2 restart all

バックアップ戦略: アプリケーションコードだけでなく、設定ファイルも含めた包括的なバックアップ計画を立てることが重要です。

Node.js アプリケーションの常駐化は、本格的な本番運用への重要な第一歩です。systemd と pm2 の特徴を理解し、プロジェクトの要件に最適な選択をすることで、安定したサービス運用を実現できるでしょう。

関連リンク