はじめに
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 }}
まとめ
| 環境 | Dockerfile | compose | 用途 |
|---|---|---|---|
| 開発 | Dockerfile.dev | docker-compose.yml | ホットリロード、デバッグ |
| 本番 | Dockerfile | docker-compose.prod.yml | 最適化イメージ、マルチステージ |