PR

【Next.jsブログ構築】Next.jsブログをVPSのサーバーモードに切り替えてPM2でプロセス管理する方法

明るい背景に、NginxがリバースプロキシとしてPM2管理下のNode.jsプロセスへリクエストを転送する、Next.jsサーバーモードの構成図を描いたイラスト。サーバーの安定性とプロセス管理を示すアイコンが配置されています。 VPS・RentalServer
この記事は約19分で読めます。
記事内に広告が含まれています。
スポンサーリンク

これまでの Next.js ブログサイト構築シリーズをまとめたページは以下の通りです。

第17回まで、このブログは「静的エクスポートモード」で動かしていました。

ビルドした HTML ファイルを Nginx がそのままユーザーに返すだけのシンプルな構成なので、
表示が速く、セキュリティ上も有利。
非エンジニアの自分にとっては「これで十分じゃないか」と思っていた構成でした。

ただ、静的サイトではできないことがあります。
データベースへの読み書きができないのです。

次の回から実装したい「独自の閲覧数カウンター」は、
ページが開かれるたびにデータベースに数値を書き込む必要があります。
静的サイトのままでは、それができません。

この回は、静的配信から「サーバーモード」への切り替えと、
Node.js プロセスを管理する PM2 の導入を行います。
設定ファイルを1行削除するだけで、サイトの動き方が根本から変わります。

静的配信とサーバーモードの違い

まず、今回の変更で何が変わるのかを整理します。

変更前(静的配信):

ユーザー → Nginx → HTML ファイルを直接返す

変更後(サーバーモード):

ユーザー → Nginx → localhost:3000(Node.js プロセス)→ 処理して返す

静的配信はファイルを見せるだけですが、サーバーモードはリクエストのたびに Next.js が処理を行います。
これにより、データベースへの読み書きや API の作成が可能になります。

表示速度は変わらないのか? と気になる方もいると思います。
結論から言うと、ほぼ変わりません。
記事ページはビルド時に静的 HTML として生成しておく仕組みが引き続き使われます。
動的に処理が走るのは API エンドポイントだけなので、読者が感じる表示速度への影響は実質ありません。

作業の全体像

今回の作業は以下の順序で進めます。順序に意味があるため、上から順に進めてください。

  1. UFW でポート 3000 をブロックする(最優先)
  2. next.config.ts を変更する
  3. Nginx の設定を書き換える
  4. PM2 をインストールして Next.js を起動する
  5. Next.js をビルドして起動する
  6. Nginx をリロードして動作確認する
  7. VPS 再起動後も自動起動する設定をする
  8. サイトマップの自動生成を設定する
  9. deploy.sh を更新する

ポート 3000 を最初にブロックする理由

他の作業よりも先に、必ずポート 3000 をブロックします。

Node.js はデフォルトでポート 3000 を使います。
next start を実行した瞬間から、このポートが外部からアクセスできる状態になります。
Nginx を通さずに直接アクセスされると、後で設定するセキュリティヘッダーやレート制限が一切効かなくなります。
そのため、他の作業よりも先に、ポート 3000 をブロックします。

sudo ufw deny 3000
sudo ufw status numbered

ルール一覧に 3000 DENY IN Anywhere3000 (v6) DENY IN Anywhere (v6) が追加されていれば成功です。
IPv4 と IPv6 の両方でブロックされています。

next.config.ts を変更する

作業前にバックアップを取ります。
万が一のときに戻せるよう、この習慣は徹底していきましょう。

cp ~/example-blog/next.config.ts ~/example-blog/next.config.ts.bak

ファイルを書き換えます。

以下のコマンドは「ヒアドキュメント」と呼ばれる書き方で、EOF から EOF の間に書いた内容をそのままファイルに書き込みます。
複数行のテキストを一度にファイルに書き込みたいときに使う方法です。

cat > ~/example-blog/next.config.ts << 'EOF'
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // サーバーモードで動作させるため output: 'export' は削除
  // 画像最適化はサーバーモードで自動的に有効になるため unoptimized も削除

  images: {
    // セキュリティ対策:外部 URL からの画像読み込みを禁止する
    remotePatterns: [],
  },
};

export default nextConfig;
EOF

変更点は3つあります。

output: 'export' を削除しました。
この1行がサーバーモードを静的エクスポートに強制していた設定です。
削除するだけでサーバーモードが有効になります。
たった1行の削除でサイトの動き方が変わります。

unoptimized: true も削除しました。
静的エクスポートモードでは Next.js の画像最適化機能が使えないため必要だった設定です。
サーバーモードでは自動的に有効になるため不要になります。

remotePatterns: [] を新たに追加しました。
外部 URL から画像を読み込むよう仕込まれた攻撃(外部サーバーへの不正なリクエストを強制する攻撃)を防ぐセキュリティ設定です。
空の配列にすることで外部 URL からの画像読み込みをすべて禁止します。

内容を確認します。

cat ~/example-blog/next.config.ts

Nginx の設定を書き換える

バックアップを取る

sudo cp /etc/nginx/sites-available/next.example.com.conf \
        /etc/nginx/sites-available/next.example.com.conf.bak

新しい設定ファイルを書き込む

sudo tee /etc/nginx/sites-available/next.example.com.conf > /dev/null << 'EOF'
# Next.js サーバー(ポート 3000)への転送先を定義する
# upstream とは「上流のサーバー」という意味で、Nginx が転送する先を指す
upstream nextjs_upstream {
    server 127.0.0.1:3000;
    keepalive 64;
}

# HTTP(80番ポート)へのアクセスを HTTPS に転送する
server {
    listen 80;
    server_name next.example.com;
    return 301 https://$host$request_uri;
}

# HTTPS(443番ポート)の本体設定
server {
    listen 443 ssl;
    server_name next.example.com;

    ssl_certificate /etc/letsencrypt/live/next.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/next.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # サーバーのバージョン情報を非表示にする
    server_tokens off;

    # 受け付けるリクエストの最大サイズを 1MB に制限する
    client_max_body_size 1M;

    # ========== セキュリティヘッダー ==========
    # (各ヘッダーの詳細は補足記事で説明します)

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # Next.js が自動でつける「X-Powered-By: Next.js」ヘッダーを隠す
    proxy_hide_header X-Powered-By;

    # ========== アクセス禁止の設定 ==========

    # .env・.git などの隠しファイルへのアクセスを禁止する
    # (.well-known は Let's Encrypt の認証に必要なため例外として許可)
    location ~ /\.(?!well-known).* {
        deny all;
        return 404;
    }

    # 開発サーバー専用のエンドポイントをブロックする
    location /_next/webpack-hmr {
        return 403;
    }

    # ========== Next.js への転送設定 ==========

    # Next.js の静的ファイル(CSS・JS など)を転送する
    # 変更されないファイルなので、ブラウザに長期間キャッシュさせる
    location /_next/static/ {
        proxy_pass http://nextjs_upstream;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # すべてのリクエストを Next.js サーバー(ポート 3000)に転送する
    location / {
        proxy_pass http://nextjs_upstream;

        # HTTP/1.1 を使う(接続の維持・ WebSocket 対応に必要)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';

        # ユーザーのブラウザが接続しているホスト名を Next.js に伝える
        proxy_set_header Host $host;

        # ユーザーの本当の IP アドレスを Next.js に伝える
        # この設定がないと、すべてのアクセスが Nginx の IP から来ているように見えてしまう
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # ユーザーが HTTPS で接続していることを Next.js に伝える
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_cache_bypass $http_upgrade;

        # タイムアウト設定(接続・送信・受信それぞれ 60 秒まで待つ)
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
EOF

設定ファイルについて補足

client_max_body_size 1M は、リクエストサイズの上限を 1MB に制限します。
私の環境では、1つの VPS 上で、Next.js サイトと WordPress サイトの両方を動かしているため、グローバル設定には WordPress のアップロード用に 30MB が設定されていますが、Next.js ブログはファイルアップロード機能を持たないため、このサイト専用に 1MB に絞ります。
巨大なリクエストを送りつけてサーバーをクラッシュさせる攻撃への対策でもあります。

セキュリティヘッダー(X-Frame-Options など)については補足記事で詳しく解説します。
今回は「WordPress サイト側と同等の設定を Next.js 側にも揃える」という方針で追加しています。

設定ファイルのテスト

Nginx は設定ファイルに文法エラーがあると起動できなくなります。変更後は必ずテストします。

sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

と表示されれば問題ありません。

この時点ではまだ Nginx をリロードしません。
Next.js サーバーが起動していない状態でリロードすると、すべてのリクエストが 502 エラーになります。
PM2 で Next.js を起動してからリロードします。
順序が重要です。

PM2 をインストールする

PM2 は Node.js のプロセス管理ツールです。

next start を普通に実行した場合、ターミナルを閉じた瞬間にサーバーが止まってしまいます。
これは開発環境では当たり前のことですが、本番環境では困ります。
誰もいない夜中にターミナルが閉じたら、サイトが止まってしまうのです。

PM2 を使うとこの問題が解決します。

  • ターミナルを閉じてもサーバーが動き続ける
  • クラッシュしても自動で再起動する
  • VPS を再起動しても自動で起動する
  • pm2 status でプロセスの状態をすぐに確認できる

PM2 をインストールします。
-g は「グローバル」の意味で、このオプションをつけることでどのディレクトリにいても pm2 コマンドが使えるようになります。

npm install -g pm2

バージョンを確認します。

pm2 --version

バージョン番号が表示されればインストール成功です。

Next.js をビルドして起動する

ビルドする

cd ~/example-blog && npm run build

コードを変更したときは必ずビルドが必要です。
ビルドにより Next.js がすべてのページを事前に処理して、サーバーが高速に返答できる形に変換します。
エラーなく完了すると、生成されたページの一覧が表示されます。

PM2 で Next.js を起動する

pm2 start npm --name "example-blog" -- start

各部分の意味を説明します。

  • pm2 start npm:npm コマンドを PM2 管理下で起動します
  • --name "example-blog":このプロセスに名前をつけます。後で pm2 stop example-blog のように名前で操作できます
  • -- start:npm に渡す引数です。npm start(= next start)が実行されます

起動後の状態を確認します。

pm2 status
pm2 statusでサイトの状態を表示させた画面の画像

pm2 status を実行すると上記のような画面が表示されます。
name のところは、起動時に指定した名前(example-blog など)が表示されます。
user にはご自身の ID が表示されています。

statusonline になっていれば正常に起動しています。

Nginx をリロードして動作確認する

Next.js が起動したので、Nginx に新しい設定を読み込ませます。

sudo systemctl reload nginx

reload は「動いたまま設定だけ再読み込みする」コマンドです。
restart と異なり、サービスが止まらないためユーザーへの影響がありません。
今後も設定変更後は reload を使う習慣をつけておくと安心です。

ブラウザで https://next.example.com を開いて、サイトが正常に表示されるか確認します。
トップページだけでなく、個別記事・カテゴリーページ・タグページなど各ページを確認します。

VPS 再起動後も自動起動する設定

現在の状態では、VPS を再起動すると PM2 が止まり、サイトが 502 エラーになります。
自動起動を設定します。

pm2 startup

実行すると、画面に以下のような長いコマンドが表示されます。

[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/home/username/.nvm/versions/node/v24.15.0/bin /home/username/.nvm/versions/node/v24.15.0/lib/node_modules/pm2/bin/pm2 startup systemd -u username --hp /home/username

表示されたコマンドをそのままコピーして実行します
このコマンドは Node.js のインストール場所やユーザー名を含んでいるため、環境によって内容が異なります。
上のサンプルをコピーするのではなく、必ず自分の画面に表示されたコマンドを使ってください。

続けて現在のプロセス一覧を保存します。
この保存内容が VPS 再起動時に復元されます。
これを忘れると、再起動後に何も起動しません。

pm2 save

Successfully saved と表示されれば完了です。

サイトマップを自動生成する

これまで public/sitemap.xml を手動で管理していました。
記事を追加するたびに手動で更新する必要があり、更新を忘れることもありました。

サーバーモードに切り替えたことで、Next.js の機能を使ったサイトマップ自動生成が使えるようになります。
app/sitemap.ts というファイルを作るだけで、/sitemap.xml にアクセスしたとき Next.js が自動的に最新の記事一覧からサイトマップを生成してくれます。

古い sitemap.xml を削除する

public/sitemap.xml が残っていると app/sitemap.ts より優先されてしまうため、先に削除します。

rm ~/example-blog/public/sitemap.xml

app/sitemap.ts を作成する

cat > ~/example-blog/app/sitemap.ts << 'EOF'
import { getSortedPostsData } from '@/lib/posts';
import { MetadataRoute } from 'next';

const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://next.example.com';

export default function sitemap(): MetadataRoute.Sitemap {
  // すべての記事情報を取得する
  const posts = getSortedPostsData();

  // 記事ページの URL 一覧を作る
  const postUrls: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `${SITE_URL}/posts/${post.id}`,
    lastModified: new Date(post.date),
    changeFrequency: 'monthly',
    priority: 0.8,
  }));

  // トップページの固定ページ
  const staticUrls: MetadataRoute.Sitemap = [
    {
      url: SITE_URL,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 1.0,
    },
  ];

  // 固定ページ + 記事ページをまとめて返す
  return [...staticUrls, ...postUrls];
}
EOF

getSortedPostsData()lib/posts.ts に定義されている「全記事を日付の新しい順に取得する」関数です。
この関数名はプロジェクトによって異なる場合があります。
以下のコマンドで自分のプロジェクトの関数名を確認できます。

grep "^export function" ~/example-blog/lib/posts.ts

ビルドして動作確認する

npm run build
pm2 restart example-blog

ブラウザで https://next.example.com/sitemap.xml を開きます。
トップページと各記事の URL が一覧で表示されれば成功です。

サイトマップのXMLファイルをブラウザで表示させた画面の画像

ブラウザで sitemap.xml を開いたところです。
各記事の URL が XML 形式で一覧表示されます。

Google サーチコンソールにこの URL を一度登録しておけば、以降は Google が定期的に自動で取りに来てくれます。
記事を追加してデプロイするだけで、Google が最新のサイトマップを認識できます。
手動更新から解放される瞬間です。

deploy.sh を更新する

現在の deploy.sh は静的エクスポート用の処理になっています。

変更前:

#!/bin/bash
set -e

echo "=== ビルド開始 ==="
npm run build

echo "=== デプロイ開始 ==="
sudo rsync -av --delete out/ /var/www/next-example/

echo "=== デプロイ完了 ==="

静的ファイルを /var/www/ にコピーする処理が含まれています。
サーバーモードではこのコピーは不要になります。

変更後:

#!/bin/bash
set -e

echo "=== ビルド開始 ==="
npm run build

echo "=== PM2 再起動(新しいビルドを反映) ==="
pm2 restart example-blog

echo "=== デプロイ完了 ==="

pm2 restart example-blog でプロセスを再起動するだけで、新しいビルドが反映されます。

なお、先頭の set -e は「エラーが起きたら即座に処理を止める」という設定です。
ビルドに失敗したまま PM2 が再起動してしまう事態を防いでいます。

バックアップを取ってから書き換えます。

cp ~/example-blog/deploy.sh ~/example-blog/deploy.sh.bak
cat > ~/example-blog/deploy.sh << 'EOF'
#!/bin/bash
set -e

echo "=== ビルド開始 ==="
npm run build

echo "=== PM2 再起動(新しいビルドを反映) ==="
pm2 restart example-blog

echo "=== デプロイ完了 ==="
EOF

動作確認します。

./deploy.sh

「デプロイ完了」まで表示されれば成功です。

Windows から Obsidian の記事をバッチファイルで公開する操作はこれまで通り変わりません。
バッチファイルは VPS 上の deploy.sh を呼び出しているだけなので、バッチファイル自体の変更は不要です。

まとめ

Next.js でブログサイトの構築を始めた理由の一つは、サイトの表示速度を上げたいということでした。
これまで WordPress でブログサイトを構築していく中で、WordPress はテーマやプラグインが豊富で、非常に便利なのですが、表示速度が遅いのが、私にとって不満でした。

Next.js でサイト構築を始めてから、ページの表示速度がとても速いことに満足していて、今回、静的エクスポートモードからサーバーモードへ切り替えるにあたり、当初は、表示速度の低下を心配していました。

しかし、実際に今回の作業をやってみた結果は、表示速度の変化は、ほとんど見た目に感じられません。
もちろん、今後もサイトの機能開発を進めていくと、表示速度に影響があるかもしれませんが、今のところは一安心です。

最後に今回の作業内容をまとめます。

作業内容
ポート 3000 のブロックUFW でポート 3000 をブロックした
next.config.ts の変更output: 'export' を削除してサーバーモードに切り替えた
Nginx 設定の書き換えリバースプロキシ設定に変更し、セキュリティヘッダーを追加した
PM2 の導入プロセス管理・自動再起動・起動時自動起動を設定した
サイトマップの自動生成app/sitemap.ts を作成して記事追加時の手動更新が不要になった
deploy.sh の更新サーバーモード用の処理に変更した

次の補足記事では、サーバーモードへの移行に伴うセキュリティ強化(fail2ban・Nginx セキュリティヘッダー)と、閲覧数カウンター用のデータベース(SQLite)の準備について書きます。

静的エクスポートモードでは、まったく心配がいらなかったのですが、サーバーモードに切り替えたことにより、外部からの攻撃に対して、十分なセキュリティ対策が必要になります。

コメント

タイトルとURLをコピーしました