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

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

先月末、いつものように Vercel の請求ページを開きました。$47.32。

頭の中で計算しました。この月のブログトラフィックは 20% 程度しか増えていないのに、請求額が倍になっている。詳細を見ると、原因は serverless function の呼び出し回数でした——キャッシュ設定が不十分な API ルートが、ページ更新のたびに 3 回実行されていたのです。

Vercel の開発体験は確かに滑らかです。git push ですぐデプロイ、グローバルエッジネットワーク、各種機能がすぐ使える。しかしトラフィックが少し増えるだけで、請求はロケットのように跳ね上がります。$20 の Pro プランは入場券に過ぎず、本当に費用がかさむのは従量課金部分です。

その瞬間、気づきました——このプロジェクト、そろそろ移行する時期だ、と。

本記事では、Next.js プロジェクトを Vercel から Docker 自前ホスティングへ移行した全過程を記録します。踏んだ落とし穴、調べたドキュメント、試した設定をすべて整理しました。自前ホスティングを検討している方、または静的リソース 404 やストリーミングレンダリングの不具合に悩んでいる方の参考になれば幸いです。

$35-50
Vercel 月額コスト
トラフィックにより変動
$12
自前ホスティング月額
固定コスト
$300-500
年間節約額
複数プロジェクトを実行可能
200MB
Docker イメージサイズ
3 段階ビルド最適化後
Source: 実践データ

なぜ Vercel から脱出するのか?

まず言っておきますが、Vercel を批判するつもりはありません。エンタープライズプロジェクト、グローバルエッジネットワークが必要な場合、運用チームがいない場合など、多くのシーンでは依然として最適解です。ただ個人プロジェクトや小規模チームにとっては、コストが大きなハードルになります。

Vercel の料金体系

無料プランは一見寛大です。100GB 帯域、100 万 Edge Requests。しかし少しトラフィックがあるプロジェクトでは、すぐに上限に達します。Pro($20/月)にアップグレードしても、それは入場券に過ぎません:

  • Serverless Function 呼び出し:100 万回超過後は従量課金
  • エッジ関数実行時間:100 万 GB-s 超過後は追加課金
  • 画像最適化:5000 回超過後は従量課金
  • 帯域:1TB 超過後は GB 単位で課金

最も厄介なのは、これらの使用量を事前に見積もりにくいこと。キャッシュ設定が不十分な API ルート、クローラーに大量アクセスされるページ——請求は一気に跳ね上がります。

自前ホスティングでどれくらい節約できる?

計算してみました。私のプロジェクトは Vercel で月 $35〜50、トラフィックにより変動していました。DigitalOcean の $12/月サーバーへ移行後:

  • サーバー:$12/月(2 コア 4GB、Next.js アプリ 2〜3 個を実行可能)
  • Cloudflare CDN:無料(もともと利用中)
  • 追加ストレージ:$0(ローカルディスクで十分)

月 $25〜40 の節約、年間 $300〜500 です。さらに重要なのは、このコストが固定で、トラフィック急増による請求の跳ね上がりがないこと。

自前ホスティングが向いているのはどんな場合?

全員が自前ホスティングすべき、というわけではありません。以下に当てはまる方は検討の価値があります:

  • ✅ Linux/Docker の基礎がある
  • ✅ トラフィックが比較的安定し、グローバルエッジネットワークが不要
  • ✅ 5〜10 分の手動デプロイを許容できる
  • ✅ 予算重視(個人プロジェクト、スタートアップ初期)

逆に以下の場合は、Vercel を使い続けた方がいいでしょう:

  • ❌ チームに運用能力がなく、学ぶ気もない
  • ❌ トラフィック変動が大きく、自動スケーリングが必要
  • ❌ Vercel の Analytics、Edge Config など固有機能が必要
  • ❌ 予算に余裕があり、開発効率を優先

よく考えてから動きましょう。節約のために自分を追い詰めないでください。

Next.js Docker デプロイのコア設定

本題に入ります。Next.js の Docker デプロイには 3 つのコア設定があり、これを押さえれば大きな問題は起きにくくなります。

1. Standalone 出力モード

最も重要なステップです。デフォルトでは next build が node_modules を含む大量のファイルを生成し、Docker イメージが巨大になり、起動も遅くなります。

next.config.js に以下を追加:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig

npm run build を実行すると、.next/standalone ディレクトリが生成されます。中身は:

  • server.js:起動スクリプト
  • 最小化された node_modules:実行時に必要なパッケージのみ
  • アプリケーションコード

ポイント:standalone モードは public.next/static を自動コピーしません。これら 2 つは standalone ディレクトリへ手動コピーが必要です。しないと静的リソースがすべて 404 になります。この落とし穴には 2 日間はまりました。

2. マルチステージ Dockerfile

実際に使っている Dockerfile をそのまま載せます。コメントも詳しく書いてあります:

# ============ 段階 1: 依存関係インストール ============
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# 依存関係マニフェストのみコピーし、Docker キャッシュを活用
COPY package.json package-lock.json ./
RUN npm ci

# ============ 段階 2: アプリビルド ============
FROM node:20-alpine AS builder
WORKDIR /app

# 依存関係とソースコードをコピー
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# ビルド時の環境変数(必要な場合)
ENV NEXT_TELEMETRY_DISABLED=1

# ビルド
RUN npm run build

# ============ 段階 3: 本番実行 ============
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# 非 root ユーザーを作成(セキュリティのベストプラクティス)
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# public フォルダをコピー(静的リソース)
COPY --from=builder /app/public ./public

# standalone 出力をコピー
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# static ファイルをコピー(CSS/JS などのビルド成果物)
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

# 起動コマンド
CMD ["node", "server.js"]

要点

  1. 3 段階ビルド:依存関係インストール、ビルド、実行を分離。最終イメージは実行時に必要なファイルのみ含み、1.5GB から 200MB に削減可能
  2. COPY --from=builder /app/public:これを忘れると favicon、robots.txt にアクセスできません
  3. COPY ./.next/static:こちらがさらに重要。ないとすべての JS/CSS が 404 になります
  4. 非 root ユーザー:セキュリティのベストプラクティス。本番環境で root 実行は避けましょう

3. 環境変数の落とし穴

こちらも踏みました。Next.js の環境変数には 2 種類あります:

  • ビルド時変数NEXT_PUBLIC_ プレフィックス。コードにコンパイルされる
  • 実行時変数:サーバー側で使用。データベース接続先など

Standalone モードでは runtimeConfig動作しません。公式推奨は App Router 方式:

// app/api/example/route.ts
export async function GET() {
  // process.env から直接読み取り
  const dbUrl = process.env.DATABASE_URL
  // ...
}

Docker 実行時に環境変数を渡す:

docker run -p 3000:3000 \
  -e DATABASE_URL="postgres://..." \
  -e API_KEY="xxx" \
  your-image-name

または docker-compose.yml:

version: '3.8'
services:
  nextjs:
    image: your-image-name
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: "postgres://..."
      API_KEY: "xxx"
    restart: unless-stopped

注意NEXT_PUBLIC_ プレフィックスの変数はビルド時に設定が必要で、実行時に変更できません。実行時に動的設定が必要な場合は、サーバー側環境変数のみ使用できます。

リバースプロキシ設定の要点

Next.js コンテナを直接インターネットに公開することもできますが、本当にやめた方がいいです。素の Node.js アプリは悪意あるリクエストやスロー攻撃に長くは耐えられません。リバースプロキシはオプションではなく、必須です。

なぜリバースプロキシが必要か

  1. セキュリティ:悪意あるリクエストの遮断、レート制限、DDoS 対策
  2. HTTPS サポート:SSL 証明書の一元管理
  3. 複数アプリのデプロイ:1 台のサーバーで複数プロジェクトをドメイン/パスで振り分け
  4. 静的リソースキャッシュ:アプリサーバーの負荷軽減

私は Nginx を使っています。安定して信頼性が高い。よりシンプルな設定を求めるなら Caddy もおすすめ(自動 HTTPS、設定ファイルも読みやすい)。

Nginx 設定例

server {
    listen 80;
    server_name yourdomain.com;
    
    # HTTPS へ強制リダイレクト(SSL 設定済みの場合)
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;
    
    # SSL 証明書設定(Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
    # Next.js コンテナへリバースプロキシ
    location / {
        proxy_pass http://localhost:3000;
        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_buffering off;
        proxy_cache off;
        proxy_set_header X-Accel-Buffering no;
        
        # WebSocket サポート(必要な場合)
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
    
    # 静的リソースキャッシュ(任意だが推奨)
    location /_next/static/ {
        proxy_pass http://localhost:3000;
        proxy_cache_valid 200 60m;
        add_header Cache-Control "public, max-age=3600, immutable";
    }
}

3 つの必須設定

  1. proxy_buffering off:バッファリングを無効化。しないとストリーミング出力が止まります
  2. X-Accel-Buffering: no:Nginx にレスポンスボディをバッファリングしないよう明示
  3. WebSocket サポート:Socket.io やリアルタイム機能を使う場合、Upgrade ヘッダーが必須

Caddy のシンプルな設定

Nginx の設定が面倒なら、Caddy を試してみてください:

yourdomain.com {
    reverse_proxy localhost:3000 {
        # Caddy はデフォルトでバッファリングしない。特別な設定不要
    }
}

以上です。Caddy は Let’s Encrypt 証明書を自動取得・更新してくれます。

Docker Compose との統合

Nginx もコンテナ化すると管理が楽になります:

version: '3.8'
services:
  nextjs:
    build: .
    restart: unless-stopped
    environment:
      DATABASE_URL: "postgres://..."
    # ホストに公開せず、nginx のみアクセス可能に
    expose:
      - "3000"
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - ./certs:/etc/letsencrypt
    depends_on:
      - nextjs
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

nextjs サービスは ports ではなく expose を使い、同一ネットワーク内のコンテナのみアクセス可能にしています。より安全です。

ストリーミングレンダリングが効かない問題の解決

この問題には丸 1 日悩まされました。ローカル開発では完全に正常な AI チャット機能が、Docker デプロイ後にストリーミング出力が効かなくなり——長時間待ってから一括表示、または完全にフリーズ。

症状

典型的な症状:

  • OpenAI/Anthropic API のストリーミングレスポンスが動作しない
  • Server-Sent Events (SSE) がリアルタイムにプッシュされない
  • ページが長時間待ってから突然更新され、逐語出力がない

ローカルの npm run dev では完全に正常、本番環境に出すと問題発生。

根本原因

2 箇所が原因になります:

  1. リバースプロキシのバッファリング:Nginx はデフォルトでレスポンスボディをバッファリングし、完全な内容が揃ってからクライアントへ送信
  2. Next.js Runtime:非 Edge Runtime の API ルートは、状況によってストリーミング出力をサポートしない

解決策 1: Nginx 設定

リバースプロキシの章で触れた 3 行を、改めて強調します:

proxy_buffering off;
proxy_cache off;
proxy_set_header X-Accel-Buffering no;

この 3 行は location / ブロックに必ず追加。変更後 Nginx を再起動:

nginx -t  # 設定構文をテスト
nginx -s reload  # 設定をリロード

解決策 2: Edge Runtime の使用

ストリーミング出力用の API ルート(AI チャットなど)では、ファイル先頭に以下を追加:

// app/api/chat/route.ts
export const runtime = 'edge'

export async function POST(req: Request) {
  const stream = new ReadableStream({
    async start(controller) {
      // ストリーミングロジック
      const response = await openai.chat.completions.create({
        model: 'gpt-4',
        messages: [...],
        stream: true,
      })

      for await (const chunk of response) {
        controller.enqueue(chunk.choices[0]?.delta?.content || '')
      }
      
      controller.close()
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  })
}

Edge Runtime は軽量ランタイムで、ストリーミングレスポンス向けに最適化されており、Docker 環境でもより安定します。

修正の確認

curl でテストし、行ごとの出力が見えれば成功:

curl -N http://yourdomain.com/api/chat \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello"}'

-N パラメータでバッファリングを無効化。内容が少しずつ出力されれば OK です。

それでもダメ?以下を確認

  1. Cloudflare プロキシ:CF のオレンジ雲を使っている場合もレスポンスをバッファリングします。無効化(グレー雲)するか Pro プラン(Streaming 対応)にアップグレード
  2. Docker ヘルスチェック:一部のヘルスチェック設定がストリーミング接続を妨げる可能性。docker-compose.ymlhealthcheck を確認
  3. ロードバランサー:前段に LB がある場合もバッファリングの可能性。個別設定が必要

よくある問題の診断と修正

踏んだ落とし穴と、コミュニティでよく聞く質問を整理しました。デプロイ失敗の約 80% をカバーしています。

問題 1: 静的リソース 404

症状:ページは開くがスタイルが崩れ、コンソールに 404 エラーが大量。パスは /_next/static/...

原因:Dockerfile で .next/static フォルダが正しくコピーされていない。

修正:Dockerfile に以下 2 行があるか確認:

COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

既にあるのに 404 の場合はファイル権限を確認:

COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

問題 2: Docker ビルド失敗

症状docker build 時に “Could not find a production build in the ‘.next’ directory” エラー

原因.gitignore.dockerignore の設定ミス、ビルド順序の問題。

修正.dockerignore を作成し、不要なディレクトリを除外:

.next
node_modules
.git
.env*.local
out
.DS_Store
*.log

注意:.next は除外します。Docker コンテナ内で再ビルドするため。

問題 3: 環境変数が効かない

症状:コードで process.env.DATABASE_URL を読むと undefined が返る

原因:環境変数の渡し方が間違っている、またはビルド時と実行時を混同。

修正

  1. 実行時変数(DB 接続先、API キーなど)は docker run -e または docker-compose.yml で渡す:

    docker run -e DATABASE_URL="..." your-image
  2. ビルド時変数NEXT_PUBLIC_ プレフィックス)は docker build 時に渡す:

    docker build --build-arg NEXT_PUBLIC_API_URL="https://api.example.com" .

    Dockerfile で宣言:

    ARG NEXT_PUBLIC_API_URL
    ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

問題 4: メモリ不足でビルド失敗

症状:ビルド途中で停止、または “JavaScript heap out of memory” エラー

原因:Node.js のデフォルトメモリ上限が不足。Next.js の大規模プロジェクトはビルド時にメモリを大量消費。

修正:Dockerfile のビルド段階でメモリを増やす:

# builder 段階
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN npm run build

または Docker BuildKit でリソースを制限:

docker build --memory=8g --memory-swap=8g -t your-image .

問題 5: コンテナ起動後にアクセスできない

症状:コンテナは正常に動いているが、http://localhost:3000 に接続拒否

原因:Next.js はデフォルトで 127.0.0.1 をリッスンし、Docker コンテナ内部から外部アクセスできない

修正:Dockerfile で設定:

ENV HOSTNAME="0.0.0.0"
ENV PORT=3000

または起動時に渡す:

docker run -p 3000:3000 -e HOSTNAME="0.0.0.0" your-image

クイック診断コマンド

問題発生時はまず以下を実行:

# 1. コンテナが実行中か確認
docker ps

# 2. コンテナログを確認
docker logs <container-id>

# 3. コンテナ内のファイル構造を確認
docker exec -it <container-id> sh
ls -la .next/
ls -la public/

# 4. コンテナ内部のサービスをテスト
docker exec -it <container-id> wget -O- http://localhost:3000

# 5. ポートマッピングを確認
docker port <container-id>

まとめ

Vercel から Docker 自前ホスティングへの移行は、思ったほど怖くありません。初期設定には時間がかかりますが、一度通ればメンテナンスコストは低い。現在は月 $12 の固定サーバー費用で Next.js プロジェクト 3 つを実行中。請求の跳ね上がりを心配する必要はありません。

本記事の 3 つのコア設定を再確認:

  1. Standalone モード - next.config.js に 1 行追加。public.next/static の手動コピーを忘れずに
  2. マルチステージ Dockerfile - 3 段階ビルド。最終イメージは約 200MB、起動も速い
  3. リバースプロキシ - Nginx ではバッファリング無効化(proxy_buffering off)が必須。しないとストリーミングレンダリングが機能しない

ストリーミングレンダリングで詰まっているなら、99% はリバースプロキシのバッファリングが原因。export const runtime = 'edge' を追加すればほぼ解決します。

Vercel vs 自前ホスティング比較

観点VercelDocker 自前ホスティング
デプロイ速度⚡️ git push で即デプロイ🐢 5〜10 分の手動操作
開発体験🌟 プレビュー環境、ログ、Analytics🔧 監視を自分で設定
コスト💸 $20+/月、トラフィック増でさらに高額💰 月 $12 固定(複数プロジェクト可)
スケーラビリティ📈 自動スケーリング📊 手動でリソース調整
制御権⚠️ プラットフォームの制約あり✅ 完全にコントロール
向いているシーンエンタープライズ、グローバルサービス個人プロジェクト、小規模チーム、予算重視

最後のアドバイス

  • 個人開発者で side project が複数あるなら、自前ホスティングでかなり節約できます
  • チームに運用能力がない、トラフィック変動が大きいなら、Vercel を使い続けましょう
  • 技術選定に正解はなく、合っているかどうかだけ

完全な設定ファイルと詳細は GitHub リポジトリ(プレースホルダー、実際の利用時に差し替え)に置いています。質問があればコメントでどうぞ。踏んだ落とし穴は、後から来る人に繰り返してほしくない。

Next.js Docker 自前ホスティング完全デプロイフロー

standalone モードの設定からデプロイ、リバースプロキシ、ストリーミングレンダリング修正までの完全手順

⏱️ 目安時間: 2 時間

  1. 1

    ステップ1: Standalone 出力モードの設定

    next.config.js で standalone モードを有効化:

    1. next.config.js を開く
    2. 設定を追加:output: 'standalone'
    3. ビルドを実行:npm run build
    4. 出力を確認:.next/standalone ディレクトリが生成されていることを確認

    ポイント:
    • standalone モードは public と .next/static を自動コピーしない
    • これら 2 つのディレクトリは Dockerfile で手動コピーが必要
    • コピーしないと静的リソースがすべて 404 になる

    設定例:
    ```javascript
    const nextConfig = {
    output: 'standalone',
    }
    module.exports = nextConfig
    ```
  2. 2

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

    3 段階ビルドの Dockerfile を作成:

    段階 1 - 依存関係インストール:
    • node:20-alpine をベースイメージに使用
    • package.json と package-lock.json のみコピー
    • npm ci で依存関係をインストール(Docker キャッシュを活用)

    段階 2 - アプリビルド:
    • 段階 1 から node_modules をコピー
    • すべてのソースコードをコピー
    • npm run build でアプリをビルド

    段階 3 - 本番実行:
    • 非 root ユーザーを作成(セキュリティのベストプラクティス)
    • public フォルダをコピー(静的リソース)
    • .next/standalone 出力をコピー
    • .next/static ファイルをコピー(CSS/JS ビルド成果物)
    • HOSTNAME="0.0.0.0" と PORT=3000 を設定
    • 起動コマンド:node server.js

    ポイント:
    • 3 段階ビルドでイメージを 1.5GB から 200MB に削減可能
    • public と .next/static のコピーは必須。しないと静的リソースが 404
    • 非 root ユーザーで実行し、セキュリティを向上
  3. 3

    ステップ3: Nginx リバースプロキシの設定

    Nginx リバースプロキシを設定し、バッファリングを無効化:

    1. Nginx をインストール(または Caddy を使用)
    2. SSL 証明書を設定(Let's Encrypt)
    3. Nginx 設定ファイルを作成

    必須設定(必ず追加):
    • proxy_buffering off;(バッファリング無効化)
    • proxy_cache off;(キャッシュ無効化)
    • proxy_set_header X-Accel-Buffering no;(Nginx にバッファリングしないよう明示)

    必須リクエストヘッダー:
    • 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;

    WebSocket サポート(必要な場合):
    • proxy_set_header Upgrade $http_upgrade;
    • proxy_set_header Connection "upgrade";

    設定テスト:
    ```bash
    nginx -t # 構文テスト
    nginx -s reload # 設定リロード
    ```

    注意:バッファリングを無効化しないと、ストリーミングレンダリングが機能しません
  4. 4

    ステップ4: 環境変数の扱い

    ビルド時と実行時の環境変数を区別:

    ビルド時変数(NEXT_PUBLIC_ プレフィックス):
    • docker build 時に渡す必要がある
    • --build-arg パラメータを使用
    • Dockerfile で宣言:ARG NEXT_PUBLIC_API_URL
    • 環境変数を設定:ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

    実行時変数(サーバー側で使用):
    • docker run -e で渡す
    • または docker-compose.yml で設定
    • コードから process.env で直接読み取り

    Standalone モードでは:
    • runtimeConfig は動作しない
    • App Router 方式で環境変数を読み取る必要がある
    • サーバー側コード:const dbUrl = process.env.DATABASE_URL

    例:
    ```bash
    # ビルド時
    docker build --build-arg NEXT_PUBLIC_API_URL="https://api.example.com" .

    # 実行時
    docker run -e DATABASE_URL="postgres://..." your-image
    ```
  5. 5

    ステップ5: ストリーミングレンダリング問題の修正

    ストリーミングレンダリングの不具合を解決(AI チャット、SSE など):

    症状:
    • ストリーミング出力が動作せず、長時間待ってから一括表示
    • Server-Sent Events がリアルタイムにプッシュされない

    解決策 1 - Nginx 設定(必須):
    • proxy_buffering off が追加されていることを確認
    • X-Accel-Buffering: no が追加されていることを確認
    • Nginx サービスを再起動

    解決策 2 - Edge Runtime の使用:
    • API ルートファイルの先頭に追加:export const runtime = 'edge'
    • Edge Runtime はストリーミングレスポンス向けに最適化
    • Docker 環境でもより安定して動作

    修正の確認:
    ```bash
    curl -N http://yourdomain.com/api/chat \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{"message": "Hello"}'
    ```
    -N パラメータでバッファリングを無効化し、行ごとの出力が見えるはず

    その他の確認:
    • Cloudflare プロキシ:オレンジ雲を使っている場合もバッファリングされる(無効化または Pro プランへ)
    • Docker ヘルスチェック:ストリーミング接続を妨げる可能性
    • ロードバランサー:前段に LB がある場合は個別設定が必要
  6. 6

    ステップ6: デプロイと検証

    イメージをビルドしてデプロイ:

    1. Docker イメージをビルド:
    ```bash
    docker build -t nextjs-app .
    ```

    2. コンテナを起動:
    ```bash
    docker run -d \
    -p 3000:3000 \
    -e DATABASE_URL="postgres://..." \
    -e API_KEY="xxx" \
    --name nextjs-app \
    nextjs-app
    ```

    3. デプロイを検証:
    • コンテナ状態を確認:docker ps
    • ログを確認:docker logs nextjs-app
    • アクセスをテスト:curl http://localhost:3000
    • 静的リソースを確認:/_next/static/ パスにアクセス

    4. Nginx を設定して再起動:
    • リバースプロキシ設定が正しいことを確認
    • HTTPS アクセスをテスト
    • ストリーミングレンダリング機能を検証

    5. 監視とメンテナンス:
    • コンテナ自動再起動を設定:--restart unless-stopped
    • 定期的にログを確認して問題を排查
    • サーバーリソース使用状況を監視

    よくある問題の排查:
    • 静的リソース 404:Dockerfile で public と .next/static をコピーしているか確認
    • 環境変数が効かない:ビルド時と実行時の変数を区別
    • コンテナにアクセスできない:HOSTNAME が 0.0.0.0 に設定されているか確認

FAQ

自前ホスティングでどれくらい節約できる?コスト比較は?
Vercel の月額コストは $35〜50(トラフィックにより変動)、Docker 自前ホスティングは月 $12 の固定費(複数プロジェクトを実行可能)。月 $25〜40 の節約、年間 $300〜500 の削減が見込めます。さらに重要なのは、自前ホスティングのコストが固定で、トラフィック急増による請求の跳ね上がりがないことです。個人プロジェクト、小規模チーム、予算重視のシーンに向いています。
静的リソースが 404 になるのはなぜ?どう修正する?
standalone モードは public と .next/static ディレクトリを自動コピーしません。修正方法:Dockerfile の runner 段階に以下 2 行を追加:
• COPY --from=builder /app/public ./public
• COPY --from=builder /app/.next/static ./.next/static
それでも 404 の場合はファイル権限を確認し、--chown=nextjs:nodejs で正しい所有者を設定してください。
ストリーミングレンダリングが動かない場合は?
99% はリバースプロキシのバッファリングが原因です。修正方法:
1) Nginx 設定に必ず追加:proxy_buffering off; proxy_set_header X-Accel-Buffering no;
2) API ルートで Edge Runtime を使用:export const runtime = 'edge'
3) Cloudflare プロキシを使っている場合:オレンジ雲を無効化するか Pro プランにアップグレード
4) 検証:curl -N でテストし、行ごとの出力が見えることを確認
環境変数が効かない場合は?
ビルド時と実行時の変数を区別:
• NEXT_PUBLIC_ プレフィックス:docker build 時に --build-arg で渡し、Dockerfile で ARG と ENV を宣言
• 実行時変数:docker run -e または docker-compose.yml で渡し、コードから process.env で読み取り
• Standalone モードでは runtimeConfig は動作しないため、App Router 方式で環境変数を読み取る必要がある
Docker イメージが大きすぎる場合は?
マルチステージビルドで最適化:
• 段階 1:依存関係のみインストール(Docker キャッシュを活用)
• 段階 2:アプリをビルド
• 段階 3:実行時に必要なファイルのみコピー(standalone 出力、public、static)
• 最終イメージを 1.5GB から 200MB に削減可能
• node:20-alpine ベースイメージでさらにサイズを削減
自前ホスティングと Vercel、それぞれいつ向いている?
自前ホスティング向き:Linux/Docker の基礎がある、トラフィックが安定、予算重視、手動デプロイを許容できる、個人プロジェクト/小規模チーム。
Vercel 向き:チームに運用能力がない、トラフィック変動が大きく自動スケーリングが必要、グローバルエッジネットワークが必要、Vercel 固有機能(Analytics、Edge Config)が必要、予算に余裕があり開発効率を優先。
コンテナ起動後にアクセスできない場合は?
以下を確認:
1) HOSTNAME は必ず 0.0.0.0 に設定(127.0.0.1 では不可)。Dockerfile で ENV HOSTNAME="0.0.0.0" を設定
2) ポートマッピングが正しいか:docker run -p 3000:3000
3) コンテナが実行中か:docker ps
4) コンテナログを確認:docker logs <container-id>
5) コンテナ内部をテスト:docker exec -it <container-id> wget -O- http://localhost:3000

5分で読めます · 公開日: 2025年12月20日 · 更新日: 2026年6月8日

関連記事

コメント

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