devops

Docker実践ガイド - コンテナ化によるモダンな開発環境構築

2025.12.02

Dockerの基本概念

Dockerは、アプリケーションをコンテナとしてパッケージ化し、どの環境でも一貫した動作を保証するプラットフォームです。

従来の仮想化 vs コンテナ仮想化

flowchart TB
    subgraph VM["従来の仮想化 (VM)"]
        direction TB
        AppA1["App A"] --> GuestA["Guest OS"]
        AppB1["App B"] --> GuestB["Guest OS"]
        AppC1["App C"] --> GuestC["Guest OS"]
        GuestA & GuestB & GuestC --> Hypervisor["Hypervisor"]
        Hypervisor --> HostOS1["Host OS"]
    end

    subgraph Container["コンテナ仮想化 (Docker)"]
        direction TB
        AppA2["App A"] --> LibsA["Libs A"]
        AppB2["App B"] --> LibsB["Libs B"]
        AppC2["App C"] --> LibsC["Libs C"]
        LibsA & LibsB & LibsC --> DockerEngine["Docker Engine"]
        DockerEngine --> HostOS2["Host OS (共有)"]
    end

比較:

  • VM: 各アプリに完全なゲストOSが必要(重い)
  • コンテナ: ライブラリのみでホストOSを共有(軽量)

Dockerfileの基本

Node.jsアプリケーション

# Dockerfile
FROM node:20-alpine

WORKDIR /app

# 依存関係のインストール(キャッシュ活用)
COPY package*.json ./
RUN npm ci --only=production

# アプリケーションコードをコピー
COPY . .

# 非rootユーザーで実行
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
USER nextjs

EXPOSE 3000

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

マルチステージビルド

# Dockerfile.multi-stage
# =========================================
# Stage 1: 依存関係のインストール
# =========================================
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# =========================================
# Stage 2: ビルド
# =========================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 環境変数(ビルド時)
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

RUN npm run build

# =========================================
# Stage 3: 本番イメージ
# =========================================
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# セキュリティ: 非rootユーザー
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# 必要なファイルのみコピー
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

USER nextjs

EXPOSE 3000

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

Next.jsアプリケーション

# Dockerfile.nextjs
FROM node:20-alpine AS base

# Stage 1: 依存関係
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json pnpm-lock.yaml* ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile

# Stage 2: ビルド
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

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

RUN corepack enable pnpm && pnpm run build

# Stage 3: 本番
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

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"]

Docker Compose

開発環境

# docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

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

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  adminer:
    image: adminer
    ports:
      - "8080:8080"
    depends_on:
      - db

volumes:
  postgres_data:
  redis_data:

本番環境

# docker-compose.prod.yml
services:
  app:
    image: myapp:${VERSION:-latest}
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    deploy:
      resources:
        limits:
          memory: 1G

volumes:
  postgres_data:
    driver: local

Dockerfile最適化

Dockerfile最適化のポイント

1. レイヤーキャッシュの活用

方法コード
悪い例COPY . .RUN npm install
良い例COPY package*.json ./RUN npm ciCOPY . .

2. イメージサイズの最小化

ベースイメージサイズ
node:201.1GB
node:20-slim250MB
node:20-alpine140MB
マルチステージビルド最終50-100MB

3. セキュリティ

  • 非rootユーザーで実行
  • 不要なパッケージを含めない
  • シークレットをイメージに含めない
  • .dockerignore の活用

.dockerignore

# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
.env*
.env.local
.env.*.local
Dockerfile*
docker-compose*
.dockerignore
README.md
.next
.cache
coverage
.nyc_output
*.log
.DS_Store

ヘルスチェック

# アプリケーション側
FROM node:20-alpine

WORKDIR /app
COPY . .
RUN npm ci --only=production

# ヘルスチェックエンドポイント
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "server.js"]
// server.ts - ヘルスチェックエンドポイント
import express from 'express';

const app = express();

app.get('/health', async (req, res) => {
  try {
    // データベース接続チェック
    await db.query('SELECT 1');

    // Redisチェック
    await redis.ping();

    res.status(200).json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      checks: {
        database: 'ok',
        redis: 'ok',
      },
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message,
    });
  }
});

app.get('/ready', (req, res) => {
  // アプリケーションが準備完了かチェック
  if (appIsReady) {
    res.status(200).send('Ready');
  } else {
    res.status(503).send('Not Ready');
  }
});

ログ管理

// logger.ts - Docker向けロギング
import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  // Docker環境ではJSON形式が推奨
  transport:
    process.env.NODE_ENV === 'development'
      ? {
          target: 'pino-pretty',
          options: { colorize: true },
        }
      : undefined,
});

// 構造化ログ
logger.info({ userId: 123, action: 'login' }, 'User logged in');
logger.error({ err: error, requestId: 'abc-123' }, 'Request failed');

export default logger;
# docker-compose.yml - ログ設定
services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

CI/CD統合

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

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

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

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

    steps:
      - uses: actions/checkout@v4

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

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

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

よく使うコマンド

# イメージのビルド
docker build -t myapp:latest .

# コンテナの実行
docker run -d -p 3000:3000 --name myapp myapp:latest

# ログの確認
docker logs -f myapp

# コンテナに入る
docker exec -it myapp sh

# イメージサイズの確認
docker images myapp

# 未使用リソースの削除
docker system prune -a

# Docker Compose操作
docker compose up -d
docker compose down
docker compose logs -f app
docker compose exec app sh

参考リンク

← 一覧に戻る