Documentation Next.js

はじめに

Next.js App Routerプロジェクトをコンテナ化することで、開発環境の再現性と本番環境との一貫性を確保できます。この記事では、開発用と本番用のDockerセットアップを詳しく解説します。

プロジェクト構成

my-nextjs-app/
├── .dockerignore
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── docker-compose.prod.yml
├── next.config.js
├── package.json
└── src/
    └── app/

.dockerignore

# .dockerignore
node_modules
.next
.git
.gitignore
*.md
.env*.local
.vscode
coverage
.turbo

開発環境用Dockerfile

# Dockerfile.dev
FROM node:20-alpine

# 作業ディレクトリ
WORKDIR /app

# pnpmをインストール(お好みのパッケージマネージャー)
RUN corepack enable && corepack prepare pnpm@latest --activate

# 依存関係のキャッシュ
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile

# ソースコードはボリュームマウントで共有

# ポート公開
EXPOSE 3000

# 開発サーバー起動
ENV HOSTNAME="0.0.0.0"
CMD ["pnpm", "dev"]

本番環境用Dockerfile(マルチステージビルド)

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

# パッケージマネージャーの設定
RUN corepack enable && corepack prepare pnpm@latest --activate

# 依存関係のインストール
COPY package.json pnpm-lock.yaml* ./
RUN pnpm install --frozen-lockfile

# ============================================
# Stage 2: ビルド
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 環境変数(ビルド時に必要なもの)
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

# Next.jsのテレメトリを無効化
ENV NEXT_TELEMETRY_DISABLED=1

# ビルド実行
RUN pnpm build

# ============================================
# Stage 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 ./
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"]

next.config.jsの設定

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // standaloneモードを有効化(Dockerイメージサイズ削減)
  output: 'standalone',

  // 実験的機能
  experimental: {
    // Server Actionsの最適化
    serverActions: {
      bodySizeLimit: '2mb',
    },
  },

  // 環境変数
  env: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },

  // 画像最適化の設定
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },
};

module.exports = nextConfig;

docker-compose.yml(開発環境)

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: nextjs-app
    ports:
      - '3000:3000'
    volumes:
      # ソースコードをマウント(ホットリロード用)
      - .:/app
      # node_modulesはコンテナ内のものを使用
      - /app/node_modules
      # .nextキャッシュを永続化
      - nextjs-cache:/app/.next
    environment:
      - NODE_ENV=development
      - WATCHPACK_POLLING=true
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    container_name: nextjs-db
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres']
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: nextjs-redis
    ports:
      - '6379:6379'
    volumes:
      - redis-data:/data
    command: redis-server --appendonly yes
    networks:
      - app-network

  # Prisma Studio(開発時のみ)
  prisma-studio:
    build:
      context: .
      dockerfile: Dockerfile.dev
    container_name: prisma-studio
    ports:
      - '5555:5555'
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
    command: pnpm prisma studio
    depends_on:
      - db
    networks:
      - app-network
    profiles:
      - tools

volumes:
  postgres-data:
  redis-data:
  nextjs-cache:

networks:
  app-network:
    driver: bridge

docker-compose.prod.yml(本番環境)

# docker-compose.prod.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
    container_name: nextjs-app-prod
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    depends_on:
      - db
      - redis
    restart: unless-stopped
    healthcheck:
      test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - app-network

  db:
    image: postgres:16-alpine
    container_name: nextjs-db-prod
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres-data-prod:/var/lib/postgresql/data
    restart: unless-stopped
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    container_name: nextjs-redis-prod
    volumes:
      - redis-data-prod:/data
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    restart: unless-stopped
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped
    networks:
      - app-network

volumes:
  postgres-data-prod:
  redis-data-prod:

networks:
  app-network:
    driver: bridge

ヘルスチェックAPI

// app/api/health/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  try {
    // データベース接続確認
    // await prisma.$queryRaw`SELECT 1`;

    return NextResponse.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json(
      {
        status: 'unhealthy',
        error: error instanceof Error ? error.message : 'Unknown error',
      },
      { status: 503 }
    );
  }
}

便利なMakefileコマンド

# Makefile
.PHONY: dev build up down logs shell db-shell migrate seed

# 開発環境
dev:
	docker-compose up -d

# 本番ビルド
build:
	docker-compose -f docker-compose.prod.yml build

# コンテナ起動
up:
	docker-compose up -d

# コンテナ停止
down:
	docker-compose down

# ログ表示
logs:
	docker-compose logs -f app

# シェルアクセス
shell:
	docker-compose exec app sh

# DBシェル
db-shell:
	docker-compose exec db psql -U postgres -d myapp

# マイグレーション実行
migrate:
	docker-compose exec app pnpm prisma migrate dev

# シード実行
seed:
	docker-compose exec app pnpm prisma db seed

# 全てクリーン
clean:
	docker-compose down -v --rmi all

# 本番デプロイ
deploy-prod:
	docker-compose -f docker-compose.prod.yml up -d --build

# Prisma Studio起動
studio:
	docker-compose --profile tools up -d prisma-studio

package.jsonスクリプト

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "docker:dev": "docker-compose up -d",
    "docker:build": "docker-compose -f docker-compose.prod.yml build",
    "docker:prod": "docker-compose -f docker-compose.prod.yml up -d",
    "docker:down": "docker-compose down",
    "docker:logs": "docker-compose logs -f",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev",
    "prisma:studio": "prisma studio"
  }
}

環境変数ファイル

# .env.development
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
NEXT_PUBLIC_API_URL=http://localhost:3000

# .env.production
DATABASE_URL=postgresql://user:password@db:5432/production_db
REDIS_URL=redis://:redis_password@redis:6379
NEXT_PUBLIC_API_URL=https://api.example.com

GitHub Actions CI/CD

# .github/workflows/docker.yml
name: Docker Build and Push

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }}

まとめ

環境Dockerfilecompose用途
開発Dockerfile.devdocker-compose.ymlホットリロード、デバッグ
本番Dockerfiledocker-compose.prod.yml最適化イメージ、マルチステージ

参考文献

円