- Nuxt
Nuxtのデプロイ方法|Node・Docker・Vercel等をpreset別に実測
Nuxtのデプロイとは、nuxt build で作った .output フォルダを、デプロイ先に合わせた形(preset)で配置して動かすことです。Nodeサーバーなら .output だけをコピーして node .output/server/index.mjs で起動でき、node_modules は不要です。
デプロイ先ごとの違いは、Nuxtのサーバーエンジン Nitro の「preset(出力形式の設定)」 で吸収されます。この記事では Nuxt 4.5.2 で実際にビルドし、.output の中身とサイズ、起動時の環境変数(PORT・HOST・NITRO_)、nuxt generate の出力、presetごとの出力先を実測しました。Node 22 のマルチステージ Dockerfile、PM2 のクラスター起動、nginx のリバースプロキシ設定も、動かした結果つきで紹介します。
検証環境:Node.js 22.22.2 / Nuxt 4.5.2 / Nitro 2.13.4 / PM2 7.0.4 / Docker 29.4.3 / nginx 1.24.0
Nuxtデプロイ先別の早見表
| デプロイ先 | preset | ビルドコマンド | 出力先(実測) | 起動・公開の方法 |
|---|---|---|---|---|
| 自前サーバー・VM | node-server(既定) |
nuxt build |
.output/ |
node .output/server/index.mjs |
| 自前サーバー(複数コア) | node-cluster |
NITRO_PRESET=node-cluster nuxt build |
.output/ |
同上(ワーカーが複数起動) |
| Docker / Cloud Run / ECS 等 | node-server |
nuxt build |
.output/ |
コンテナで node .output/server/index.mjs |
| 静的ホスティング | static |
nuxt generate |
.output/public/ |
ファイルを置くだけ |
| Vercel | vercel(自動判定) |
nuxt build |
.vercel/output/ |
Git連携 or npx vercel deploy --prebuilt |
| Netlify | netlify(自動判定) |
nuxt build |
dist/ と .netlify/ |
Git連携 |
| Cloudflare Pages | cloudflare-pages(自動判定) |
nuxt build |
dist/(_worker.js 含む) |
npx wrangler pages deploy dist |
| Firebase App Hosting | firebase-app-hosting |
nuxt build |
.output/ と .apphosting/bundle.yaml |
App Hosting のGit連携 |
| AWS Lambda | aws-lambda |
NITRO_PRESET=aws-lambda nuxt build |
.output/(Lambda用ハンドラー) |
SAM・CDK等でデプロイ |
| Bun / Deno | bun / deno-server |
NITRO_PRESET=bun nuxt build |
.output/ |
bun run .output/server/index.mjs など |
迷ったら「Nodeサーバー(node-server)+Docker」か「Vercel・Netlify・Cloudflare Pages の自動判定」 のどちらかから始めるとスムーズです。VercelなどはCI上の環境変数からpresetが自動で選ばれる(ゼロコンフィグ)ため、設定なしで動きます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Nuxtのデプロイ方法|Node・Docker・Vercel等をpreset別に実測」
nuxt buildの.output構成とサイズ
認証API・ページ7つ程度の小さなアプリで npx nuxt build を実行しました。ビルドの最後に表示された内容です。
Σ Total size: 2.62 MB (675 kB gzip)
[nitro] √ You can preview this build using node .output/server/index.mjs
— ✨ Build complete!
できあがった .output の構成(実測):
.output/
├── nitro.json # preset名やNuxt・Nitroのバージョン
├── public/ # ブラウザに配る静的ファイル
│ ├── _nuxt/ # JS・CSS(ファイル名にハッシュ付き)
│ └── robots.txt # プロジェクト直下 public/ の中身がそのままコピーされる
└── server/
├── index.mjs # 起動ファイル(エントリーポイント)
├── chunks/ # サーバー側のコード(ページ描画・API)
├── node_modules/ # 実行に必要な依存だけを抜き出したもの
└── package.json
| 場所 | サイズ(du 実測) | 中身 |
|---|---|---|
プロジェクトの node_modules/ |
210MB | 開発・ビルド用の依存すべて |
.output/ 全体 |
4.0MB | 本番で必要なものすべて |
.output/public/ |
308KB | 静的ファイル27個 |
.output/server/chunks/ |
572KB | サーバーコード |
.output/server/node_modules/ |
3.1MB | 実行に必要な27パッケージ(vue、vue-router、jose など) |
本番に必要なのは .output の4.0MBだけで、210MBの node_modules は持っていく必要がありません。 Nitro がビルド時に、実行に必要な依存だけを .output/server/node_modules に抜き出してくれるためです。nitro.json には使われたpresetが記録されます。
{
"preset": "node-server",
"framework": { "name": "nuxt", "version": "4.5.2" },
"versions": { "nitro": "2.13.4" },
"commands": { "preview": "node server/index.mjs" }
}
Nodeサーバーで起動する(PORT・HOST・NITRO_環境変数)
npx nuxt build
NODE_ENV=production node .output/server/index.mjs
Listening on http://0.0.0.0:3000
既定ではポート3000、すべてのネットワークインターフェース(0.0.0.0)で待ち受けます。 起動時に読まれる主な環境変数です(.output/server/chunks/nitro/nitro.mjs の実装と公式ドキュメントで確認)。
| 環境変数 | 既定値 | 意味 |
|---|---|---|
NITRO_PORT / PORT |
3000 |
待ち受けポート。両方あれば NITRO_PORT が優先 |
NITRO_HOST / HOST |
未指定(全インターフェース) | 待ち受けアドレス。両方あれば NITRO_HOST が優先 |
NITRO_SSL_CERT / NITRO_SSL_KEY |
なし | 証明書を渡すとHTTPSで待ち受け |
NITRO_UNIX_SOCKET |
なし | ポートの代わりにUNIXソケットで待ち受け |
NITRO_SHUTDOWN_TIMEOUT |
30000(ミリ秒) | 停止シグナル後、処理中のリクエストを待つ最大時間 |
NODE_ENV |
― | production を指定(公式推奨。開発用の警告がログに出ないように) |
NUXT_… |
― | runtimeConfig の上書き(アプリの設定値) |
PORT=7460 NITRO_PORT=7463 を同時に指定したときの実行結果:
Listening on http://0.0.0.0:7463
HOST=127.0.0.1 を指定したときの実行結果:
Listening on http://127.0.0.1:7460
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7460/healthz # → 200
curl -s -o /dev/null -w '%{http_code}\n' http://192.0.2.2:7460/healthz # → 000(届かない)
HOST=127.0.0.1 は「同じマシンの中からだけ受け付ける」設定です。 nginx を同じサーバーに置いてその裏で動かすときは、外から直接届かないのでむしろ安全です。一方、コンテナの中でこの設定になると外から届かなくなります(後述の「よくある失敗」)。
nuxt preview もありますが、起動時に次のように表示されるとおり確認用です。
| You are previewing a Nuxt app. In production, do not use this CLI.
| Instead, run node server/index.mjs directly.
nuxt generateで静的サイトとして出力する
サーバーを持たない静的ホスティング(GitHub Pages、S3など)に置く場合は nuxt generate を使います。
npx nuxt generate
[nitro] i Prerendered 15 routes in 5.165 seconds
[nitro] √ Generated public .output/public
[nitro] √ You can preview this build using npx serve .output/public
— ✨ You can now deploy .output/public to any static hosting!
出力の一部(実測):
.output/public/
├── index.html
├── 200.html # SPAとしてのフォールバック
├── 404.html
├── _payload.json
├── _nuxt/
├── login/index.html
├── config/index.html
└── dashboard/index.html
| 確認したこと | 結果 |
|---|---|
nitro.json の preset |
static |
.output/server |
作られない(nitro.json と public/ だけ) |
ログイン必須の /dashboard/index.html |
<meta http-equiv="refresh" content="0; url=/login?redirect=/dashboard"> だけのHTML |
server/api のAPI |
静的ファイルには含まれない(サーバーがないため動かない) |
nuxt generate はリンクをたどってページをHTMLに書き出します。 APIやログインのようにサーバーで処理する機能は動かないので、記事サイトやLPは generate、ログインやAPIがあるアプリは Nodeサーバーと分けるのが基本です。すべてをブラウザ側で描画したい場合は nuxt.config.ts で ssr: false にする方法もあります。
Dockerでデプロイする(Node 22マルチステージ)
Dockerfile(コピペ用)
# syntax=docker/dockerfile:1
# ---- ビルドステージ ----
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- 実行ステージ(.output だけを持ってくる)----
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=3000
COPY --from=build --chown=node:node /app/.output ./.output
USER node
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
CMD ["node", ".output/server/index.mjs"]
# .dockerignore
node_modules
.nuxt
.output
.env
.git
// server/routes/healthz.get.ts(ヘルスチェック用のエンドポイント)
export default defineEventHandler(() => ({ status: 'ok' }))
| ポイント | 理由 |
|---|---|
| ビルドと実行でステージを分ける | 実行イメージに210MBの node_modules やソースを入れない |
.output だけをコピー |
実行に必要な依存は .output/server/node_modules に入っている |
USER node(非root) |
公式Nodeイメージに用意されているユーザーで動かす |
HOST=0.0.0.0 を明示 |
実行環境に HOST が別の値で設定されていても外から届くようにする |
CMD ["node", …](exec形式) |
Nodeが直接シグナルを受け取り、停止が速い |
秘密は ENV に書かない |
docker run -e や実行環境の設定で渡す |
| Node 22 系 | Nuxt 4.5.2 の engines は ^22.19.0 || ^24.11.0 || >=26.0.0(npmで確認) |
検証:実行ステージを再現して起動を確認
この検証環境は Docker Hub からイメージを取得できないため、ビルドステージは実行していません(公式イメージ node:22-alpine を使う上記の構成は、公式ドキュメントの手順に沿って記載)。代わりに、ローカルの最小イメージにホストの Node.js 22.22.2 を入れ、実行ステージ(.output だけをコピーし、UID 1001 の非rootユーザーで起動)を再現して動作を確かめました。
# 検証用:実行ステージだけを再現した Dockerfile
FROM mini:1
COPY rootfs/ /
WORKDIR /app
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=3000
COPY --chown=1001:1001 .output ./.output
USER 1001:1001
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
CMD ["node", ".output/server/index.mjs"]
docker build -t nuxt-app .
docker run -d --name nuxt-app -p 7466:3000 \
-e NUXT_SESSION_PASSWORD=… \
-e NUXT_API_SECRET=docker-secret-42 \
nuxt-app
docker logs nuxt-app
curl -s http://localhost:7466/healthz
docker exec nuxt-app id
docker ps --filter name=nuxt-app --format '{{.Names}} {{.Status}}'
実行結果:
Listening on http://0.0.0.0:3000
{"status":"ok"}
uid=1001(app) gid=1001(app) groups=1001(app)
nuxt-app Up 15 seconds (healthy)
| 確認したこと | 結果 |
|---|---|
.output だけで起動 |
起動し、ページ・APIとも応答 |
| 非rootで動作 | uid=1001 |
| HEALTHCHECK | (healthy) |
| 実行時の環境変数 | NUXT_API_SECRET が反映された |
| 未ログインで保護ページ | 302 → /login?redirect=/dashboard(SSRの動作もコンテナ内で同じ) |
docker stop にかかった時間 |
0.27秒(Nodeが直接SIGTERMを受けて終了) |
再現イメージのサイズはNode本体を含めて189MBでしたが、これはホストのNodeを丸ごと入れた検証用の数字です。アプリ由来の部分は .output の4.0MBだけなので、実際のイメージサイズはほぼベースイメージ(node:22-alpine など)で決まります。
PM2でクラスター起動する
VMや物理サーバーで、プロセスの自動再起動やCPUコア数ぶんの起動をしたいときは PM2 が便利です。
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: 'nuxt-app',
script: './.output/server/index.mjs',
exec_mode: 'cluster',
instances: 2, // 'max' で CPU コア数ぶん
env: {
NODE_ENV: 'production',
PORT: 3000,
},
},
],
}
npx pm2 start ecosystem.config.cjs
npx pm2 ls
実行結果(抜粋、PM2 7.0.4):
[PM2] App [nuxt-app] launched (2 instances)
│ id │ name │ mode │ status │ mem │
│ 0 │ nuxt-app │ cluster │ online │ 79.8mb │
│ 1 │ nuxt-app │ cluster │ online │ 79.0mb │
2つのプロセスが同じポートを共有して起動し、/healthz が {"status":"ok"} を返しました。サーバー再起動後も自動で立ち上げたい場合は pm2 save と pm2 startup を使います。
PM2を使わずに複数プロセスで動かしたいときは、preset node-cluster も選べます。
NITRO_PRESET=node-cluster npx nuxt build
NITRO_CLUSTER_WORKERS=3 node .output/server/index.mjs
Listening on http://0.0.0.0:3000
Listening on http://0.0.0.0:3000
Listening on http://0.0.0.0:3000
ワーカーが3つ起動しました(NITRO_CLUSTER_WORKERS 未指定ならCPU数)。なお今回の実測では、SIGTERM を送ってからすぐには終了せず、30秒以内に停止しました(処理中のリクエストを待つ仕組みのため)。コンテナでは1プロセスにしてコンテナの数で増やし、VMではPM2かnode-cluster、と使い分けると運用が単純になります。
nginxでリバースプロキシする
同じサーバーで nginx を前に置き、Nuxt を裏で動かす最小構成です。nginx の設定の詳しい書き方は別記事の領域なので、Nuxt特有の部分だけに絞ります。
upstream nuxt {
server 127.0.0.1:3000;
keepalive 16;
}
server {
listen 80;
server_name example.com;
# ハッシュ付きの静的ファイルは nginx から直接返し、長期キャッシュ
location /_nuxt/ {
alias /var/www/nuxt-app/.output/public/_nuxt/;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
location / {
proxy_pass http://nuxt;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
}
Nuxt は HOST=127.0.0.1 で起動し、nginx 経由でだけ届くようにします。検証結果:
| 確認したこと | 結果 |
|---|---|
/(nginx経由) |
200 |
/dashboard(未ログイン) |
302 → /login?redirect=/dashboard |
/_nuxt/xxxx.js |
200、nginxが直接返した(Nuxtのログに /_nuxt のアクセスなし) |
/_nuxt/ のキャッシュヘッダー |
Cache-Control: public, max-age=31536000, immutable |
expires 1y; と add_header Cache-Control を両方書くと、Cache-Control ヘッダーが2行出ました(max-age=31536000 と public, max-age=31536000, immutable)。どちらか一方にします。なお Nuxt 自身も /_nuxt/ に同じ immutable のキャッシュヘッダーを付けて返すため、nginx で静的配信しない構成でもキャッシュは効きます。
Vercel・Netlify・Cloudflare等の設定例
各サービスの preset で実際にビルドした出力先です(NITRO_PRESET=… npx nuxt build で実測。各サービスへの実際のデプロイは未実行)。
| preset | ビルドの最後の表示(実測) | 生成されたもの |
|---|---|---|
vercel |
You can deploy this build using npx vercel deploy --prebuilt |
.vercel/output/(config.json functions static nitro.json) |
netlify |
Generated public dist |
dist/(_headers _redirects _nuxt)と .netlify/ |
cloudflare-pages |
You can deploy this build using npx wrangler pages deploy dist |
dist/(_worker.js _routes.json _headers _redirects) |
firebase-app-hosting |
Generated .apphosting/bundle.yaml |
.output/ と .apphosting/bundle.yaml |
aws-lambda |
Generated public .output/public |
.output/(Lambda用ハンドラー) |
bun |
You can preview this build using bun run .output/server/index.mjs |
.output/ |
deno-server |
deno task --config .output/deno.json start |
.output/(deno.json 付き) |
presetの指定方法は2通りです。
// nuxt.config.ts で固定する場合
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages',
},
})
# 環境変数で指定する場合
NITRO_PRESET=aws-lambda npx nuxt build
Vercel・Netlify・Cloudflare Pages は、各サービスのCI上でビルドするとpresetが自動判定されます(Nitroが実行環境を検出する仕組み)。自分のPCやGitHub Actionsでビルドしてから送る場合は、presetを明示します。Nitro 2.13.4 で使えるpreset名は node-server、node-cluster、static、vercel、vercel-static、netlify、netlify-edge、netlify-static、cloudflare-pages、cloudflare-module、aws-lambda、aws-amplify、azure-swa、firebase-app-hosting、deno-deploy、bun、github-pages、render-com、heroku などです(nitropack/dist/presets/_types.gen.d.ts で確認。node_server のようなアンダースコア表記も受け付けます)。
NUXT_… の環境変数は、Vercel・Netlify・Cloudflare Pages では各サービスの管理画面の環境変数設定に、Docker・Cloud Run では docker run -e やサービスの環境変数設定に、VM(PM2)では ecosystem.config.cjs の env や systemd の Environment= に登録します。どのデプロイ先でも「ビルド成果物に秘密を入れず、実行環境で渡す」が共通の考え方です。
よくある失敗と直し方
| 失敗 | 症状(実測) | 直し方 |
|---|---|---|
コンテナ内で HOST=127.0.0.1 |
ヘルスチェックは healthy なのに外から届かない | HOST=0.0.0.0 |
静的ファイルを app/public/ に置いた |
404 | プロジェクト直下の public/ |
| 環境変数がビルド時に固定される | 実行時に変えても反映されない | NUXT_ で実行時に渡す |
本番で .env が読まれない |
設定値が空 | 実行環境の環境変数で渡す |
nuxt preview を本番で使う |
確認用の表示が出る | node .output/server/index.mjs |
| 静的ホスティングでAPIが動かない | APIが存在しない | Nodeサーバー等のpresetにする |
失敗1:HOSTの設定ミスで外から届かない
コンテナを -e HOST=127.0.0.1 付きで起動した結果です。
$ docker logs nuxt-app-bad
Listening on http://127.0.0.1:3000
$ curl -sS --max-time 3 http://localhost:7467/healthz
curl: (56) Recv failure: Connection reset by peer
$ docker exec nuxt-app-bad node -e "fetch('http://127.0.0.1:3000/healthz').then(r=>r.text()).then(console.log)"
{"status":"ok"}
$ docker ps --filter name=nuxt-app-bad --format '{{.Status}}'
Up 13 seconds (healthy)
コンテナの中からのヘルスチェックは成功するため healthy と表示されますが、コンテナの外(ポート転送)からは届きません。 起動ログの Listening on http://127.0.0.1:3000 が見分けるポイントです。Dockerfile で HOST=0.0.0.0 を明示しておくと、実行環境側に別の HOST が設定されていても上書きできます(docker run -e HOST=… のほうが優先される点には注意します)。
失敗2:publicフォルダの場所
Nuxt 4 では app/ フォルダができましたが、静的ファイルの public/ はプロジェクト直下のままです。
| 置いた場所 | .output/public へのコピー |
アクセス結果(実測) |
|---|---|---|
public/robots.txt |
される | /robots.txt → 200 |
app/public/wrong.txt |
されない | /wrong.txt → 404 |
public/ のファイルは名前にハッシュが付かないため、Nuxtは /_nuxt/ のような長期キャッシュ(immutable)を付けずに返しました(Last-Modified のみ)。頻繁に差し替える画像などは、ファイル名を変えるかCDN側のキャッシュ設定で調整します。
失敗3:環境変数がビルド時に固定される
nuxt.config.ts の中で process.env.API_URL のように読んだ値は、ビルドした時点の値が成果物に書き込まれます。実行時に変えたい値は runtimeConfig に既定値つきで定義し、NUXT_ で始まる環境変数で渡します。Dockerなら「1回ビルドしたイメージを、環境変数だけ変えて検証・本番で使い回す」形になります。
デプロイ前のチェックリスト
| チェック | 確認方法 |
|---|---|
| Node.js 22.19以上(または24.11以上) | node -v |
本番起動は node .output/server/index.mjs |
nuxt preview を使っていない |
NODE_ENV=production |
起動コマンドや Dockerfile |
コンテナでは HOST=0.0.0.0 |
起動ログの Listening on |
| ヘルスチェック用のルートがある | curl /healthz |
| 秘密は実行環境の環境変数で渡している | Dockerfile や Git に秘密がない |
静的ファイルは直下の public/ |
.output/public に入っている |
| presetがデプロイ先と合っている | .output/nitro.json の preset |
| 非rootで動かしている | docker exec … id |
よくある質問
Q. .output以外に本番サーバーへ持っていくものはありますか?
A. node-server preset なら .output だけで起動します。実測でも、.output だけをコピーしたコンテナで、ページ・API・リダイレクトがすべて動きました。package.json や node_modules は不要です。
Q. nuxt buildとnuxt generateはどちらを使えばよいですか?
A. ログイン、API、リクエストごとに変わる表示があるなら nuxt build(Nodeサーバー等)、すべてのページを事前にHTMLにできる記事サイトやLPなら nuxt generate です。1つのアプリの中で一部のページだけ事前生成したい場合は、routeRules の prerender: true を使う方法もあります。
Q. Dockerのベースイメージはalpineとslimのどちらがよいですか?
A. どちらでも .output は動きます。ネイティブモジュール(C言語で書かれた拡張)を使う依存がある場合は、ビルドと実行を同じ種類のイメージにそろえることが大切です。迷ったら、ビルドと実行の両方を同じ node:22-… イメージにしておくと安心です。
まとめ
- 本番に必要なのは
.outputだけ。実測でnode_modules210MB に対し.outputは4.0MBでした。 - 起動は
NODE_ENV=production node .output/server/index.mjs。ポートはPORT、アドレスはHOST(NITRO_付きが優先)。 - ログインやAPIがあるなら
nuxt build、全ページを事前生成できるならnuxt generate。 - Dockerはマルチステージで
.outputだけをコピーし、非root・HOST=0.0.0.0・ヘルスチェック付きにします。 - VMでは PM2 か
node-cluster、前段に nginx で/_nuxt/を長期キャッシュ。 - Vercel・Netlify・Cloudflare Pages はpresetの自動判定で設定なしでも動き、手元でビルドするならpresetを明示します。
- コンテナで
HOST=127.0.0.1だと healthy でも外から届かないので、起動ログで確認します。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Nuxtのデプロイ方法|Node・Docker・Vercel等をpreset別に実測」
株式会社RJCでは、アプリを作るだけでなく「どこで、どう動かすか」まで見通せるエンジニアを目指して、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。