2026年8月9日
2026年8月9日
NginxでNode.jsアプリをホスティングする方法(systemd常駐とリバースプロキシ設定)
Node.js のアプリを本番サーバーで公開するときは、アプリ自体は 127.0.0.1:3000 のような内部のポートだけで待ち受けさせ、systemd(または PM2)で常駐させたうえで、外からの 80/443 番ポートは Nginx が受けてアプリへ中継する構成が定番です。この記事では、Ubuntu の VPS を例に、アプリの常駐化、Nginx のリバースプロキシ設定、HTTPS 化、そして 502 エラーなど公開時によくつまずく点の確認方法までを順に説明します。
なぜNodeを直接公開せずNginxを前に置くのか
Node.js の http モジュールや Express は単体でも 80 番ポートで待ち受けられますが、本番では前段に Nginx を置くのが一般的です。理由は次のとおりです。
- 権限の分離: 1024 番未満のポートを開くには特権が必要です。Node を root で動かさずに済むよう、ポートの受け口は Nginx に任せます。
- TLS の終端: 証明書の設定や自動更新は Nginx と Certbot の組み合わせが扱いやすく、アプリ側のコードに証明書の処理を書かずに済みます。
- 静的ファイルと遅いクライアントの処理: 画像や CSS の配信、回線の遅い利用者への送信待ちを Nginx が引き受けるため、Node のイベントループが本来の処理に集中できます。
- 複数アプリの同居: ドメインやパスごとに転送先を分けられるので、同じサーバーで WordPress と Node のアプリを並べて動かすこともできます。
構成を図にすると「利用者 → Nginx(80/443)→ 127.0.0.1:3000 の Node アプリ」です。3000 番ポートは外部に開けません。
前提と準備
ここでは Ubuntu の LTS 版、Nginx は OS のパッケージ、Node.js は LTS 版を使う前提で書いています。Node.js は偶数番号のメジャー版が LTS になり、サポート期間は版ごとに決まっています(2026年10月現在の対応状況は Node.js 公式サイトのリリース一覧で確認してください)。インストール方法はいくつかありますが、どの方法でも「node コマンドの実際のパス」をあとで systemd に書くため、次のように確認しておきます。
which node
node -v
nvm で入れた場合、パスはログインユーザーのホームディレクトリ配下になります。systemd からはそのユーザーの環境変数が読まれないため、node とだけ書くと起動に失敗します。この記事の例では /usr/bin/node としていますが、ご自身の環境の値に置き換えてください。
アプリは専用のユーザーで動かすと、万一アプリに脆弱性があっても被害の範囲を狭められます。
sudo adduser --system --group --home /srv/myapp myapp
sudo chown -R myapp:myapp /srv/myapp
ステップ1:アプリを内部ポートで待ち受けさせる
アプリ側では、待ち受けるアドレスを 127.0.0.1 に限定し、ポート番号は環境変数から受け取るようにしておきます。動作確認用の最小構成は次のとおりです。
// /srv/myapp/server.js
const http = require('node:http');
const port = Number(process.env.PORT) || 3000;
const host = process.env.HOST || '127.0.0.1';
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Hello from Node.js\n');
});
server.listen(port, host, () => {
console.log(`listening on http://${host}:${port}`);
});
host を省略したり 0.0.0.0 にしたりすると、ファイアウォールの設定次第で 3000 番ポートに外から直接届いてしまいます。また、localhost と書くと環境によって IPv6 の ::1 で待ち受けることがあり、Nginx 側の 127.0.0.1 と食い違って 502 エラーの原因になります。アドレスは数字で明示しておくのが無難です。
Express を使う場合は、Nginx 経由で届いたリクエストの送信元 IP やプロトコルを正しく扱うために app.set('trust proxy', 'loopback') を設定します。これを入れないと req.ip がすべて 127.0.0.1 になり、ログやレート制限が意味をなさなくなります。
ステップ2:systemdで常駐させる
ターミナルで node server.js を実行しただけでは、ログアウトやサーバーの再起動でアプリが止まります。systemd のユニットファイルを作り、OS の起動時に立ち上がり、落ちたら自動で再起動するようにします。
# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js app
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=HOST=127.0.0.1
ExecStart=/usr/bin/node /srv/myapp/server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
journalctl -u myapp -f
console.log の出力は journald に記録されるので、journalctl -u myapp で確認できます。API キーなどの秘密情報はユニットファイルに直接書かず、EnvironmentFile= で権限を絞った別ファイルから読み込むと、設定ファイルの共有時に漏れにくくなります。
PM2 を使い慣れている場合は、pm2 start server.js --name myapp で起動し、pm2 startup が表示するコマンドを実行してから pm2 save で自動起動を登録する方法もあります。クラスタモードやログのローテーションを PM2 にまとめて任せたいなら PM2、OS 標準の仕組みで管理したいなら systemd、と考えると選びやすいです。どちらか一方に決め、二重に起動しないよう注意してください。
ステップ3:Nginxのリバースプロキシを設定する
次に、ドメインへのアクセスを Node アプリに中継する server ブロックを作ります。
# /etc/nginx/sites-available/myapp.conf
upstream myapp_backend {
server 127.0.0.1:3000;
keepalive 16;
}
# WebSocket のときだけ Connection: upgrade を送る
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}
server {
listen 80;
listen [::]:80;
server_name app.example.com;
client_max_body_size 10m;
location / {
proxy_pass http://myapp_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60s;
}
location /static/ {
alias /srv/myapp/public/;
expires 7d;
access_log off;
}
}
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
設定のポイントは次のとおりです。
proxy_http_version 1.1は WebSocket と upstream への keepalive の両方に必要です。X-Forwarded-ForとX-Forwarded-Protoを渡すことで、アプリ側が元の送信元 IP と「HTTPS で来たかどうか」を判断できます。mapで、WebSocket の接続時だけConnection: upgradeを送り、通常のリクエストでは Connection ヘッダーを空にして keepalive を活かしています。- 静的ファイルは
aliasで Nginx から直接返しています。locationとaliasの末尾のスラッシュをそろえないとパスがずれるので注意してください。
nginx -t で構文エラーがないことを確かめてから reload します。いきなり restart すると、設定ミスがあった場合に既存のサイトまで止まります。
ステップ4:HTTPS化とファイアウォール
DNS の A レコード(必要なら AAAA レコード)がサーバーを向いていることを確認したら、Certbot で証明書を取得します。
sudo certbot --nginx -d app.example.com
sudo certbot renew --dry-run
Certbot が server ブロックに 443 番の設定とリダイレクトを追記します。--dry-run で更新の予行演習が通ることも確認しておくと、期限切れの事故を防げます。
ファイアウォールは 80/443 番だけを開け、3000 番は開けません。UFW なら sudo ufw allow 'Nginx Full' で両方のポートを許可できます。sudo ss -tlnp | grep 3000 を実行して、待ち受けアドレスが 127.0.0.1:3000 になっていれば外部からは直接届きません。
動かないときの確認方法
502 Bad Gateway が出る
Nginx がアプリに接続できていない状態です。まず /var/log/nginx/error.log を見ます。connect() failed (111: Connection refused) while connecting to upstream とあれば、次の順で確認します。
- アプリが動いているか(
systemctl status myapp)。起動直後に落ちている場合はjournalctl -u myappでエラーを確認します。node のパスの誤りや、依存パッケージの未インストールが多い原因です。 - ポート番号とアドレスが一致しているか(
ss -tlnpの結果と upstream の値を比べる)。 curl -I http://127.0.0.1:3000/でサーバー内から直接アクセスできるか。
RHEL 系の OS で SELinux が有効な場合、Nginx からの外向き接続が拒否されて同じ症状になることがあります。その場合は httpd_can_network_connect という SELinux のブール値を確認してください。
504 Gateway Timeout が出る
アプリの処理が proxy_read_timeout を超えています。重い処理はジョブキューに回すなどアプリ側で短くするのが本筋で、タイムアウトの延長は一時的な対処と考えてください。
WebSocket がつながらない、ファイルアップロードが413になる
WebSocket は Upgrade と Connection のヘッダー転送が抜けていると失敗します。413 Request Entity Too Large は client_max_body_size の不足です。
WordPressと同じサーバーで動かす場合
WordPress のサイトと Node アプリを同居させるなら、app.example.com のようにサブドメインを分けて server ブロックを別にするのが管理しやすい方法です。同じドメインのサブディレクトリで動かすこともできますが、アプリ側のパスの扱いや Cookie の範囲を合わせる手間が増えます。ヘッドレス構成で Node のフロントエンドから WordPress の REST API を呼ぶ場合も、まずはドメインを分けて動作を確認するとトラブルを切り分けやすくなります。設定を変える前には、既存の Nginx 設定ファイルのバックアップを取っておいてください。
まとめ
- Node アプリは
127.0.0.1の内部ポートだけで待ち受け、外部の 80/443 番は Nginx が受け持ちます。 - 常駐化は systemd のユニットファイルか PM2 のどちらかに決め、node コマンドの実際のパスを指定します。
- Nginx では
proxy_http_version 1.1、Host と X-Forwarded 系のヘッダー、WebSocket 用の Upgrade ヘッダーを設定し、nginx -tの後に reload します。 - 502 はアプリの停止かポートの食い違い、504 は処理時間、413 は
client_max_body_sizeが主な原因です。