言語を切り替える
テーマを切り替える

Next.js Docker 自前ホスティング完全ガイド:Vercel からの卒業

Vercel は素晴らしいプラットフォームです。プッシュするだけでデプロイでき、設定もほぼ不要、パフォーマンスも抜群です。しかし、プロジェクトが成長するにつれて、ある「壁」にぶつかることがあります。

それはコスト制限です。

私が運営していた中規模サイトでは、画像の帯域幅や Serverless Function の実行時間制限により、毎月 Vercel に $50〜$100 を支払っていました。さらに、特定のバックエンドサービスとの連携や、VPC 内での運用が必要になった際、Vercel の環境が逆に制約となる場面も出てきました。

そこで、月額 $5 の VPS(仮想専用サーバー)と Docker を使った自前ホスティングに切り替えたところ、コストは 1/10 になり、インフラの自由度も劇的に向上しました。

ただ、Next.js の自前ホスティングは一筋縄ではいきません。特に Docker イメージの最適化や、ストリーミングレンダリング(Edge Runtime)の対応にはコツがいります。

この記事では、Next.js アプリケーションを Docker で本番環境にデプロイするための「最適解」を共有します。

Vercel vs 自前ホスティング:どちらを選ぶべき?

盲目的に自前ホスティングへ移行する前に、メリット・デメリットを整理しましょう。

項目VercelDocker 自前ホスティング
コスト重量課金(高くなる可能性あり)VPS固定費(安価・安定)
デプロイGit Push だけパイプライン構築が必要
機能Edge Config, Analytics 等が即座に使えるすべて自分で用意する必要あり
スケーリング自動手動(K8s 等が必要)
制御プラットフォームの制限内OS レベルで完全制御

自前ホスティングが向いているケース:

  • 予算が限られており、固定費で運用したい
  • 特定のリージョンにデータを置きたい
  • イントラネットや VPN 内で動かしたい
  • Docker エコシステム(Docker Compose 等)を活用したい

ステップ 1: Next.js の設定最適化

Docker イメージを小さくするために、Next.js の Standalone モード を有効にします。これにより、依存関係(node_modules)の中から「実際に使われているファイルだけ」を抽出してくれます。

// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
  // 以前は推奨されていましたが、現在は standalone が主流です
  // target: 'serverless',
};

export default nextConfig;

この一行を追加して npm run build を実行すると、.next/standalone というフォルダが生成されます。これがデプロイに必要な全てを含んだ「ミニマルな Next.js」です。通常、数百 MB ある node_modules が、必要なものだけに絞られるため劇的に軽くなります。

ステップ 2: マルチステージ Dockerfile の作成

ここが最重要ポイントです。ただ COPY . . するだけの Dockerfile は作らないでください。イメージサイズが 1GB を超えてしまいます。

以下のマルチステージビルド用 Dockerfile を使いましょう。これは Next.js 公式の推奨をベースに、さらに最適化したものです。

# 1. 依存関係のインストール(Deps ステージ)
FROM node:20-alpine AS deps
WORKDIR /app

# パッケージマネージャに応じたロックファイルをコピー
COPY package.json package-lock.json* ./
# 依存関係をインストール(ci は clean install の略で本番向け)
RUN npm ci

# 2. ビルド(Builder ステージ)
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 環境変数はビルド時に必要な場合のみここで宣言
# ENV NEXT_PUBLIC_API_URL=https://api.example.com

RUN npm run build

# 3. 実行(Runner ステージ)
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

# nextjs ユーザーを作成(セキュリティのため root で実行しない)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 静的ファイルをコピー
# standalone モードでは public と .next/static は自動で含まれないため手動コピーが必要
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# standalone ビルド成果物をコピー
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./

USER nextjs

EXPOSE 3000
ENV PORT 3000
# ホスト名設定(コンテナ外からアクセスできるようにする)
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

ポイント解説

  1. alpine イメージの使用: 軽量な Alpine Linux ベースの Node.js イメージを使用。
  2. マルチステージ: ビルド環境(devDependencies含む)と実行環境を分離。
  3. 静的ファイルのコピー: standalone モードは public.next/static を含まないので、手動でコピーする必要があります。これを忘れると画像が表示されなかったり、CSS が当たらない問題が起きます。
  4. 非 root ユーザー: セキュリティのため、専用ユーザー nextjs で実行します。
  5. HOSTNAME 設定: デフォルトだと localhost (127.0.0.1) でしかリッスンしない場合があるため、0.0.0.0 を明示します。

ステップ 3: Docker Compose で起動

ローカルでのテストや、本番での簡易デプロイには docker-compose.yml が便利です。

version: '3'
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nextjs-app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - NEXT_PUBLIC_API_URL=https://api.myapp.com

起動コマンド:

docker-compose up -d --build

これで http://localhost:3000 でアプリが動きます。

ステップ 4: Nginx リバースプロキシとストリーミング対応

本番環境では、Docker コンテナの前に Nginx を置くのが一般的です(SSL 終端、キャッシュ、ロードバランシングのため)。

しかし、ここで一つ大きな罠があります。Nginx のデフォルト設定は Next.js のストリーミング(App Router の逐次ロード)を壊します。

Nginx がレスポンスをバッファリング(溜め込み)してしまうため、Next.js が「データを少しずつ送る」処理をしても、Nginx が「全部揃ってからブラウザに送る」挙動をしてしまうのです。

これを防ぐための nginx.conf 設定:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;

        # 重要:ストリーミングのためにバッファリングを無効化
        proxy_buffering off;
        proxy_set_header X-Accel-Buffering no;
    }
}

proxy_buffering off; が魔法の言葉です。これにより、AI の回答生成のような逐次表示UIが正しく動作するようになります。

よくあるトラブルと解決策

1. 静的ファイル(画像・CSS)が 404 になる

原因: Dockerfile で public フォルダや .next/static フォルダをコピーし忘れている。
対策: Runner ステージの COPY コマンドを確認してください。standalone ビルドはこれらをバンドルしません。

2. 環境変数が undefined になる

Next.js には「ビルド時」に埋め込まれる変数(NEXT_PUBLIC_)と、「実行時」に読み込まれる変数があります。
Docker イメージをビルドする際、.env ファイルは通常含まれません。
対策:

  • NEXT_PUBLIC_ 変数: docker build --build-arg NEXT_PUBLIC_API_URL=... で渡すか、ビルド用 .env を一時的にコピーする。
  • サーバーサイド変数: docker run -e DATABASE_URL=... で起動時に渡す。

3. メモリ不足(OOM Kill)

ビルド中にプロセスが殺されることがあります。Next.js のビルドはメモリを食います。
対策: Docker デーモンのメモリ割り当てを増やすか、VPS のスワップ領域を作成してください。

# 2GBのスワップファイルを作成する例
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

まとめ

自前ホスティングは、最初は設定の手間がかかりますが、一度構築してしまえば「自由」と「安さ」が手に入ります。

  1. output: 'standalone' でビルドを軽量化する
  2. マルチステージビルド でイメージサイズを削減する
  3. 静的ファイルの手動コピー を忘れない
  4. Nginx のバッファリングをオフ にしてストリーミングを守る

この4点を守れば、Next.js はどこでも快適に動きます。あなたのアプリが大成功してトラフィックが爆増したとしても、VPS のプランを一つ上げるだけで済み、クラウド破産することはないでしょう。

Next.js Docker 自前ホスティング構築フロー

standalone モードの設定から Dockerfile 作成、Nginx 設定までの完全ガイド

⏱️ Estimated time: 1 hr

  1. 1

    Step1: Next.js 設定の変更

    `next.config.mjs` に `output: 'standalone'` を追加します。これにより、実行に必要な最小限のファイルを生成するようになります。
  2. 2

    Step2: Dockerfile の作成

    マルチステージビルドを採用した Dockerfile を作成します。Builder ステージでビルドし、Runner ステージでは `standalone` フォルダと `public`、`.next/static` フォルダのみをコピーして軽量化します。
  3. 3

    Step3: Docker イメージのビルド

    コマンド `docker build -t nextjs-app .` を実行してイメージを作成します。`NEXT_PUBLIC_` 系の環境変数が必要な場合は `--build-arg` で渡します。
  4. 4

    Step4: コンテナの起動

    `docker run -p 3000:3000 nextjs-app` で起動します。本番環境では `docker-compose` の使用を推奨します。
  5. 5

    Step5: Nginx リバースプロキシの設定

    Nginx 設定ファイルで `proxy_buffering off;` を指定し、ストリーミングレンダリングが正しく機能するようにプロキシ設定を行います。

FAQ

自前ホスティングでどのくらいコスト削減できますか?
トラフィックによりますが、Vercel の月額 $20/user + 超過料金と比較して、VPS なら $5〜10/月 固定で運用可能です。多くのケースで 70% 以上の削減が見込めます。
静的リソース(CSS/JS)が 404 エラーになるのはなぜ?
`output: 'standalone'` モードは `public` ディレクトリと `.next/static` ディレクトリを自動的には出力先に含めません。Dockerfile の最終ステージで、これらをビルドステージから明示的に COPY する必要があります。
ストリーミングレンダリング(ChatGPTのような表示)が動かない
Nginx などのリバースプロキシがレスポンスをバッファリングしている可能性が高いです。Nginx 設定で `proxy_buffering off;` と `proxy_set_header X-Accel-Buffering no;` を追加してください。
ビルド時に環境変数が必要です。どうすればいいですか?
`NEXT_PUBLIC_` で始まる変数はビルド時に JS バンドルに埋め込まれるため、`docker build` コマンドの `--build-arg` オプションで渡す必要があります。サーバーサイドの変数は実行時(`docker run -e`)に渡せばOKです。
Docker イメージのサイズを小さくするには?
マルチステージビルドを使用し、最終イメージには `alpine` ベースの Node.js を使い、ビルドツールを含めないようにします。また、`.dockerignore` ファイルで不要なファイルを除外することも重要です。最適化すれば 150MB 程度まで小さくできます。

4 min read · 公開日: 2025年12月20日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事