セキュリティ監査を乗り越えたShimaLinkは、今や沖縄県内で50を超えるクライアントを抱える成長中のプラットフォームになっていた。
しかし、成長は新しい問題を連れてくる。
Mika: 「あの、予約フォームの日付がおかしくて…昨日まで動いてたんですけど。」
あなた: 「え?こっちのPCでは普通に動いてるよ?」
あなたは自分のマシンで確認する。確かに正常に動いている。しかし、本番環境にデプロイしたバージョンではバグが発生していた。
あなた: 「ローカルでは動くのに、本番では動かない…何が違うんだろう?」
あなた: 「Node.jsのバージョンが違うみたいだ。本番は14で、うちのPCは18だった。」
Yuki: 「出たね、“It works on my machine” 問題。開発者なら誰もが経験する古典的な悩みだよ。」
あなた: 「全員の環境を揃えるのって、そんなに大変なんですか?」
Yuki: 「OSのバージョン、ライブラリのバージョン、環境変数…全部揃えるのは現実的じゃない。だから、“環境ごとアプリに同梱する” という発想が生まれたんだ。」
あなた: 「環境ごと同梱?」
Yuki: 「そう。Docker(ドッカー) というツールを使えば、アプリと動作環境をまるごとパッケージにできる。どのマシンで動かしても、まったく同じ結果が得られるんだ。」
「引っ越しの荷物みたいなものだよ」とYukiは続けた。「家具をバラバラに送ると組み立て方を忘れるけど、部屋ごと運べたら完璧でしょ?Dockerはそれをソフトウェアでやるんだ。」
ShimaLinkの”環境問題”を解決する旅が始まる。
なぜコンテナが必要なのか
「“自分のマシンでは動くんですけど…”は、エンジニア界の定番言い訳だよ。」 ——Yuki
「環境の違い」という悪夢
開発者のPCと本番サーバーでは、さまざまな違いがあります。
| 項目 | 開発環境 | 本番環境 |
|---|---|---|
| OS | macOS 14 | Ubuntu 22.04 |
| Node.js | v18.17.0 | v14.21.0 |
| データベース | SQLite | PostgreSQL |
| 環境変数 | .envファイル | サーバー設定 |
これらの違いが、予期しないバグの原因になります。
従来の解決策とその限界
仮想マシン(VM)
仮想マシンは、OS全体をエミュレーションする技術です。
┌─────────────────────────────┐
│ アプリケーション │
│ ライブラリ・依存関係 │
│ ゲストOS(Ubuntu等) │
│ ハイパーバイザー │
│ ホストOS(macOS等) │
│ ハードウェア │
└─────────────────────────────┘
- 起動に数分かかる
- ディスク容量を大量に消費(数GB〜数十GB)
- リソースのオーバーヘッドが大きい
コンテナ
コンテナは、OSのカーネルを共有し、アプリの実行環境だけを隔離します。
┌────────┐ ┌────────┐ ┌────────┐
│ アプリA │ │ アプリB │ │ アプリC │
│ 依存関係│ │ 依存関係│ │ 依存関係│
├────────┴─┴────────┴─┴────────┤
│ コンテナランタイム(Docker) │
│ ホストOS │
│ ハードウェア │
└─────────────────────────────────┘
- 起動は数秒
- 軽量(数十MB〜数百MB)
- 高速でリソース効率が良い
コンテナの3つのメリット
1. 環境の再現性
同じコンテナイメージを使えば、どのマシンでもまったく同じ環境が再現されます。
2. 分離性
コンテナ同士は隔離されているため、あるアプリの問題が他に波及しません。
3. ポータビリティ
「一度作れば、どこでも動く」——ローカルPC、テストサーバー、本番環境、クラウド、すべてで同じコンテナが動きます。
ShimaLinkでの活用シーン
開発者のPC → テスト環境 → 本番環境
│ │ │
同じDockerイメージを使用
│ │ │
結果が同じ! 結果が同じ! 結果が同じ!
Yuki: 「引っ越しの荷物を考えてみて。家具をバラバラに送ると配置がズレるでしょ?でも部屋ごとコンテナ船で運べたら、配置は完璧。Dockerは、ソフトウェアの”コンテナ船”なんだよ。」
ポイント
- 「自分のマシンでは動く」問題はコンテナで解決できる
- コンテナは仮想マシンより軽量で高速
- 環境の再現性・分離性・ポータビリティが3大メリット
- 開発・テスト・本番の環境差異をなくすことが目的
Docker の基本 — イメージ、コンテナ、Dockerfile
「Docker には3つの重要な概念がある。レシピ、料理、キッチンだよ。」 ——Yuki
Docker の3つの基本概念
| 概念 | 比喩 | 説明 |
|---|---|---|
| Dockerfile | レシピ | イメージの作り方を書いた設計図 |
| イメージ | 料理の完成品 | 実行に必要なすべてが入ったパッケージ |
| コンテナ | 実際に食べる一皿 | イメージから起動した実行インスタンス |
Dockerfile を書いてみよう
ShimaLinkのWebアプリ用Dockerfileを作成します。
# ベースイメージ — Node.js 18のAlpine Linux版(軽量)
FROM node:18-alpine
# 作業ディレクトリを設定
WORKDIR /app
# 依存関係ファイルを先にコピー(キャッシュ活用)
COPY package.json package-lock.json ./
# 依存関係をインストール
RUN npm ci
# アプリケーションコードをコピー
COPY . .
# アプリが使うポートを宣言
EXPOSE 3000
# アプリを起動するコマンド
CMD ["npm", "start"]
各命令の意味
| 命令 | 役割 |
|---|---|
FROM | ベースとなるイメージを指定する |
WORKDIR | コンテナ内の作業ディレクトリを設定する |
COPY | ホストのファイルをコンテナにコピーする |
RUN | ビルド時にコマンドを実行する |
EXPOSE | コンテナが使用するポートを宣言する |
CMD | コンテナ起動時に実行するコマンドを指定する |
イメージをビルドする
# Dockerfileからイメージをビルド
docker build -t shimalink-web:1.0 .
# -t : タグ(名前:バージョン)を付ける
# . : Dockerfileがあるディレクトリ(カレント)
ビルドの流れ:
Dockerfile → docker build → イメージ(shimalink-web:1.0)
コンテナを起動する
# イメージからコンテナを起動
docker run -d -p 3000:3000 --name shimalink shimalink-web:1.0
# -d : バックグラウンドで実行(デタッチモード)
# -p : ホストの3000番ポートをコンテナの3000番に接続
# --name : コンテナに名前をつける
よく使う Docker コマンド
# 実行中のコンテナ一覧
docker ps
# すべてのコンテナ一覧(停止中も含む)
docker ps -a
# コンテナのログを表示
docker logs shimalink
# コンテナを停止
docker stop shimalink
# コンテナを削除
docker rm shimalink
# イメージの一覧
docker images
# イメージを削除
docker rmi shimalink-web:1.0
.dockerignore で不要ファイルを除外する
.gitignore と同様に、コンテナに含めたくないファイルを指定します。
node_modules
.git
.env
*.log
.DS_Store
Yuki: 「
node_modulesをコンテナに入れちゃうと、イメージが巨大になるし、OS依存のバイナリが混入する可能性がある。コンテナ内でnpm ciし直すのが正解だよ。」
ポイント
- Dockerfileはイメージの設計図、イメージはパッケージ、コンテナは実行インスタンス
FROMでベースイメージを選び、COPYとRUNで環境を構築するdocker buildでイメージを作り、docker runでコンテナを起動する.dockerignoreで不要なファイルをコンテナから除外する
Docker Compose — 複数コンテナのオーケストレーション
「コンテナ1つじゃ足りない。Web、API、DBをまとめて管理するのがComposeだよ。」 ——Yuki
なぜ Docker Compose が必要?
ShimaLinkは複数のサービスで構成されています。
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Web │──→│ API │──→│ DB │
│ (React) │ │ (Node) │ │(Postgres)│
│ Port:3000 │ │ Port:4000│ │ Port:5432│
└──────────┘ └──────────┘ └──────────┘
これらを個別に docker run するのは面倒でミスも起きやすい。Docker Composeなら、1つのファイルですべてを定義・管理できます。
docker-compose.yml を書く
version: "3.8"
services:
# データベース
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: shimalink
POSTGRES_USER: shimalink_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
ports:
- "5432:5432"
# APIサーバー
api:
build: ./api
environment:
DATABASE_URL: postgres://shimalink_user:${DB_PASSWORD}@db:5432/shimalink
NODE_ENV: production
ports:
- "4000:4000"
depends_on:
- db
# Webフロントエンド
web:
build: ./web
environment:
REACT_APP_API_URL: http://api:4000
ports:
- "3000:3000"
depends_on:
- api
volumes:
db-data:
各セクションの解説
| セクション | 役割 |
|---|---|
services | 各コンテナ(サービス)の定義 |
image | Docker Hubの既存イメージを使う |
build | Dockerfileからビルドする |
environment | 環境変数を設定する |
volumes | データの永続化(コンテナを消してもデータが残る) |
ports | ホスト:コンテナのポートマッピング |
depends_on | 起動順序の依存関係を指定する |
Docker Compose の基本コマンド
# すべてのサービスをビルドして起動
docker compose up -d
# サービスの状態を確認
docker compose ps
# ログを表示(全サービス)
docker compose logs
# 特定サービスのログを表示
docker compose logs api
# サービスを停止
docker compose down
# ボリュームも含めて完全削除
docker compose down -v
サービス間の通信
Docker Composeでは、サービス名がそのままホスト名として使えます。
// api/src/db.js
// "db" はComposeのサービス名
const connectionString = "postgres://user:pass@db:5432/shimalink";
// ^^
// サービス名がホスト名になる
これにより、IPアドレスをハードコードする必要がなくなります。
開発用の設定を追加する
開発時にはホットリロードが欲しいので、ボリュームマウントを使います。
# docker-compose.override.yml(開発用の上書き設定)
services:
api:
volumes:
- ./api/src:/app/src # コード変更を即反映
environment:
NODE_ENV: development
command: npm run dev # 開発用コマンドに変更
web:
volumes:
- ./web/src:/app/src
environment:
NODE_ENV: development
Yuki: 「
docker-compose.override.ymlは自動的に読み込まれるから、開発と本番で設定を分けられるんだ。」
ポイント
- Docker Composeは複数コンテナをまとめて管理するツール
docker-compose.ymlに全サービスの定義を書く- サービス名がコンテナ間のホスト名として使える
depends_onで起動順序を制御するvolumesでデータを永続化するdocker compose up -dですべて起動、docker compose downで停止
コンテナのベストプラクティス
「動くDockerfileを書くのは簡単。良いDockerfileを書くのは技術だよ。」 ——Yuki
1. イメージを軽量に保つ
Alpine ベースイメージを使う
# 悪い例: フルイメージ(約900MB)
FROM node:18
# 良い例: Alpineイメージ(約120MB)
FROM node:18-alpine
| ベースイメージ | サイズ |
|---|---|
node:18 | ~900MB |
node:18-slim | ~200MB |
node:18-alpine | ~120MB |
マルチステージビルド
ビルドに必要なツールを最終イメージに含めないテクニックです。
# ステージ1: ビルド
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ステージ2: 本番用(ビルド結果だけをコピー)
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
この手法でイメージサイズを大幅に削減できます。
2. レイヤーキャッシュを活用する
Dockerは各命令をレイヤーとしてキャッシュします。変更頻度の低いものを先に書くのがコツです。
# 良い例: 依存関係を先にインストール
COPY package.json package-lock.json ./
RUN npm ci
# ↑ package.jsonが変わらなければキャッシュが使われる
COPY . .
# ↑ ソースコードの変更はここだけ再実行される
# 悪い例: すべてを一度にコピー
COPY . .
RUN npm ci
# ↑ ソースコードが1行変わっただけで npm ci も再実行される
3. セキュリティを意識する
root ユーザーで実行しない
# ユーザーを作成して切り替える
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
機密情報をイメージに含めない
# 悪い例: パスワードをDockerfileに書く
ENV DB_PASSWORD=mysecretpassword
# 良い例: 実行時に環境変数で渡す
# docker run -e DB_PASSWORD=xxx shimalink-web
最新のベースイメージを使う
# バージョンを明示する(latestは避ける)
FROM node:18.19-alpine
4. ヘルスチェックを設定する
コンテナが正常に動いているか自動チェックする仕組みです。
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
# docker-compose.yml での設定
services:
api:
build: ./api
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:4000/health"]
interval: 30s
timeout: 3s
retries: 3
5. ログは標準出力に出す
// 悪い例: ファイルに書く
fs.writeFileSync('/var/log/app.log', message);
// 良い例: 標準出力に出す
console.log(message);
Dockerはコンテナの標準出力を自動収集するため、docker logs で確認できます。
ShimaLink の最終的な構成
shimalink/
├── web/
│ ├── Dockerfile
│ ├── .dockerignore
│ └── src/
├── api/
│ ├── Dockerfile
│ ├── .dockerignore
│ └── src/
├── docker-compose.yml
├── docker-compose.override.yml
└── .env
Yuki: 「ベストプラクティスを最初から完璧にする必要はない。まず動かして、少しずつ改善していけばいいんだよ。」
ポイント
- Alpineベースイメージとマルチステージビルドでイメージを軽量化する
- Dockerfileの命令順を工夫してレイヤーキャッシュを活用する
- rootユーザーを避け、機密情報をイメージに含めない
- ヘルスチェックでコンテナの死活監視を行う
- ログは標準出力に出してDockerに収集させる
ShimaLinkの全サービスがDockerコンテナ化された。開発環境、テスト環境、本番環境——すべてが同じDockerイメージから起動される。
$ docker compose up
[+] Running 3/3
✔ Container shimalink-db Started
✔ Container shimalink-api Started
✔ Container shimalink-web Startedあなた: 「これ、新しいメンバーが入ってきても docker compose up するだけで開発始められるってこと?」
Yuki: 「そう。README に手順をダラダラ書く必要もない。環境構築に丸一日かかる時代は終わったんだよ。」
Mika: 「あの予約フォームのバグ、もう起きなくなりましたね!」
あなた: 「環境の違いという変数が消えたから、コードの問題だけに集中できるようになりました。」
Yuki: 「コンテナ化は最初のステップ。でもまだ課題がある。今は手動でビルドして手動でデプロイしてるよね?」
あなた: 「うん…正直めんどくさい。デプロイのたびにヒヤヒヤするし。」
Yuki: 「だったら次は、コードをプッシュしたら自動でテストしてデプロイする仕組みを作ろう。CI/CDパイプラインっていうんだけど——」
次のチャプター: Chapter 17: 自動化の流れ — 手動デプロイの恐怖から解放される。GitHub Actionsで自動テスト・自動デプロイのパイプラインを構築する。
🧠 理解度チェック
Q1.コンテナと仮想マシン(VM)の最も大きな違いは何?
💡 Yukiが「部屋ごと運ぶ」と「家全体を運ぶ」の違いに例えた話を思い出そう。
Q2.Dockerfile の FROM 命令の役割は?
💡 ShimaLinkのDockerfileでは、FROM node:18-alpine をベースに使ったよね。
Q3.docker run -p 3000:4000 の意味は?
💡 ShimaLinkのWebサービスをブラウザからアクセスできるようにポート設定したときの話だ。
Q4.Docker Compose で depends_on を設定する理由は?
💡 ShimaLinkではAPIがデータベースに依存していたから、DBを先に起動する必要があったね。
Q5.Dockerfileでレイヤーキャッシュを効率的に使うには?
💡 Yukiが教えてくれた「ソースコードが1行変わるたびにnpm installが走ったら遅すぎる」という話だ。
Q6.Docker Compose でコンテナ間の通信にサービス名を使える理由は?
💡 ShimaLinkのAPI設定で、DATABASE_URLにサービス名「db」を使ったことを思い出そう。
Q7.コンテナでrootユーザーを避けるべき理由は?
💡 セキュリティ監査を乗り越えたShimaLinkだからこそ、コンテナでもセキュリティを意識しよう。
❓ よくある質問
「docker: command not found」と表示される
Dockerがインストールされていません。 **macOSの場合:** 1. [Docker Desktop](https://www.docker.com/products/docker-desktop/) をダウンロード 2. インストール後、Docker Desktopを起動 3. メニューバーにクジラのアイコンが表示されればOK ```bash # 確認 docker --version # → Docker version 24.x.x ``` **注意:** Docker Desktopが起動していないとコマンドが使えません。
docker buildで「no such file or directory」エラーが出る
Dockerfileが見つからない、またはCOPYするファイルが存在しません。 ```bash # Dockerfileがあるか確認 ls Dockerfile # Dockerfileがあるディレクトリで実行する cd /path/to/project docker build -t myapp . ``` **チェックポイント:** - Dockerfileの名前が正確か(大文字のDで始まる) - COPYするファイルが.dockerignoreで除外されていないか - ビルドコンテキスト(`.`)が正しいディレクトリか
コンテナが起動してもすぐ停止してしまう
コンテナ内のプロセスがすぐに終了している可能性があります。 ```bash # ログを確認して原因を調べる docker logs コンテナ名 # 対話モードで起動してデバッグ docker run -it myapp sh ``` **よくある原因:** - CMDのコマンドが間違っている - アプリがエラーでクラッシュしている - 必要な環境変数が設定されていない - ポートが既に使用されている
docker compose upで「port is already allocated」エラーが出る
指定したポートが既に使用されています。 ```bash # どのプロセスがポートを使っているか確認 lsof -i :3000 # そのプロセスを停止するか、別のポートを使う ``` **docker-compose.ymlでポートを変更する場合:** ```yaml services: web: ports: - "3001:3000" # ホスト側を3001に変更 ```
イメージとコンテナの違いがわからない
**料理に例えると分かりやすいです:** | 概念 | 料理の例 | 特徴 | |------|---------|------| | **Dockerfile** | レシピ | 作り方の手順書 | | **イメージ** | 冷凍食品 | いつでも同じ料理を再現できる | | **コンテナ** | 実際の料理 | 食べる(実行する)もの | - 1つのイメージから複数のコンテナを作れる - イメージは読み取り専用、コンテナは読み書き可能 - コンテナを削除してもイメージは残る
volumeとbind mountの違いは?
どちらもデータを永続化する方法ですが、用途が異なります。 | 種類 | 用途 | 例 | |------|------|----| | **Volume** | データの永続化(DB等) | `db-data:/var/lib/postgresql/data` | | **Bind Mount** | 開発時のコード同期 | `./src:/app/src` | **Volume:** Dockerが管理する領域にデータを保存 **Bind Mount:** ホストの特定ディレクトリをコンテナにマウント 開発時はBind Mountでコード変更を即反映、本番ではVolumeでデータを安全に保管します。
Docker Desktopが重くてPCが遅くなる
Docker Desktopのリソース設定を調整しましょう。 **設定方法:** 1. Docker Desktop → Settings → Resources 2. CPUとメモリの割り当てを調整 **推奨設定(8GB RAMのPCの場合):** - CPU: 2コア - Memory: 2〜3GB - Swap: 1GB **使わないときは停止する:** - メニューバーのDockerアイコン → Quit Docker Desktop **不要なイメージ・コンテナを削除:** ```bash docker system prune -a ```
「COPY failed: file not found」エラーの原因は?
COPYで指定したファイルがビルドコンテキスト内に見つかりません。 **よくある原因:** 1. **`.dockerignore`で除外されている** ``` # .dockerignoreを確認 node_modules # ← これは除外して正しい src/ # ← これを除外すると問題になる ``` 2. **パスが間違っている** ```dockerfile # Dockerfileからの相対パスで指定 COPY ./src /app/src # ← Dockerfileと同階層のsrc ``` 3. **ビルドコンテキストが間違っている** ```bash # "." がプロジェクトルートを指しているか確認 docker build -t myapp . ```
コンテナの中に入ってデバッグしたい
実行中のコンテナにシェルで接続できます。 ```bash # 実行中のコンテナに入る docker exec -it コンテナ名 sh # Composeのサービス名で入る docker compose exec api sh # コンテナ内でデバッグ ls /app cat /app/package.json env # 環境変数を確認 ``` **exitでコンテナから出る**(コンテナは停止しません) ```bash exit ```
docker compose upで特定のサービスだけ再起動したい
サービス名を指定して操作できます。 ```bash # 特定サービスだけ再ビルドして起動 docker compose up -d --build api # 特定サービスだけ再起動 docker compose restart api # 特定サービスだけ停止 docker compose stop web # 特定サービスのログだけ表示 docker compose logs -f api ``` `-f` オプションでリアルタイムにログを追跡できます。