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 /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 /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /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 /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 /app/public ./public
# standalone出力を使用
COPY /app/.next/standalone ./
COPY /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 ci → COPY . . |
2. イメージサイズの最小化
| ベースイメージ | サイズ |
|---|---|
| node:20 | 1.1GB |
| node:20-slim | 250MB |
| node:20-alpine | 140MB |
| マルチステージビルド最終 | 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 \
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