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

中級 | 45分 で読める | 2025.12.02

公式ドキュメント

今回やること

この記事では、Dockerfile、マルチステージビルド、Docker Compose、CIでのイメージビルドまでを確認します。

Dockerは「動く環境を丸ごと固定する」道具ですが、秘密情報や不要ファイルをイメージに含めない設計が重要です。

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

実践メモ: COPY package*.json ./を先に行い、RUN npm ciの後にCOPY . .とすることで、レイヤーキャッシュを最大限活用できます。

マルチステージビルド

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

注意: NEXT_TELEMETRY_DISABLED=1を設定しないと、ビルド時にテレメトリデータが送信されます。CIやプロダクション環境では必ず無効化しましょう。

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:YOUR_DB_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: YOUR_DB_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

実践メモ: depends_onにcondition: service_healthyを指定すると、DBの準備完了を待ってからアプリが起動します。

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

注意: .envファイルをイメージに含めないよう、必ず.dockerignoreに追加してください

ヘルスチェック

# アプリケーション側
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"

ポイント: HEALTHCHECKを定義すると、Docker Composeのdepends_onやオーケストレーターがコンテナの健全性を判断できます。

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

動作確認

Docker化したら、次を確認します。

確認項目コマンド例見ること
ビルドdocker build -t myapp .イメージ作成が成功する
起動docker run -p 3000:3000 myappアプリにアクセスできる
ログdocker logs <container>起動エラーが出ていない
サイズdocker images myapp不自然に大きくない
秘密情報docker history myapptokenやpasswordを含めていない

ビルドが遅い場合は、COPY package*.json ./ と依存関係インストールの順番を見直します。

練習

小さなNode.jsアプリで次を試してください。

  1. Dockerfile を作る
  2. docker build でイメージを作る
  3. docker run で起動する
  4. .dockerignore を追加して不要ファイルを除外する
  5. マルチステージビルドでイメージサイズを比較する

次のステップ

コンテナの基本コマンドを先に確認したい場合は Dockerチートシート を参照してください。ビルド最適化は Dockerマルチステージビルド実践、CI連携は GitHub Actions CI/CD完全ガイド が次の候補です。

まとめ

この記事では、手順を追って動かしながら重要な確認ポイントを押さえることが大切です。まずは学習用の最小構成で動作確認し、本番利用する前にセキュリティ、権限、エラー処理、運用手順を見直してください。

参考リンク

参考リソース

← 一覧に戻る
PR
PR
PR
PR