開発者のためのDocker:イメージ、コンテナ、Compose
プロジェクトをクローンしてセットアップ手順を実行したのに、システムライブラリの不足、データベースの起動失敗、Nodeバージョンの不一致などで行き詰まった経験はありませんか。Dockerは、そうした問題を根本から解消するために存在します。サービスをマシンに直接インストールする代わりに、一度定義すれば、どこでも同じように動作する隔離されたコンテナで実行できます。
このガイドでは、本当に必要な3つの概念、イメージ、コンテナ、Composeについて説明します。読みながら実行できるコマンドを交えた実践的な内容です。
イメージとコンテナ:メンタルモデル
イメージは読み取り専用のテンプレートです。ファイルシステムのスナップショットと、デフォルトコマンドや公開ポートなどのメタデータが含まれます。コンテナはそのイメージの実行中(または停止中)のインスタンスで、上に薄い書き込み可能なレイヤーがあります。
オブジェクト指向プログラミングの経験があれば、類推はほぼそのままです。イメージはクラス、コンテナはオブジェクトです。1つのイメージから多数のコンテナを起動でき、コンテナ内の変更はイメージを変更しません。
| 概念 | 内容 | 典型的なコマンド |
|---|---|---|
| イメージ | 不変のテンプレート | docker build, docker pull |
| コンテナ | 実行中のインスタンス | docker run, docker ps |
| ボリューム | 永続ストレージ | docker volume create |
| Compose | マルチコンテナ定義 | docker compose up |
Dockerfileで最初のイメージを作る
Dockerfileはレシピです。各命令がレイヤーを作成し、Dockerはレイヤーをキャッシュするため、変更がなければ再ビルドは高速です。以下は小さなNode.jsの例です:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]ここで重要な習慣が2つあります。まず、依存関係のマニフェストをソースの残りより先にコピーして、アプリケーションコードだけが変更されたときにnpm ciレイヤーがキャッシュされるようにします。次に、alpineのようなスリムなベースイメージを選び、サイズと攻撃対象領域を減らします。
ビルドして実行します:
docker build -t myapp:1.0 .
docker run --rm -p 3000:3000 myapp:1.0-p 3000:3000フラグは、ホストのポート3000をコンテナのポート3000にマッピングします。--rmはコンテナ終了時に削除するので、開発中にマシンをきれいに保てます。
日常的なコンテナコマンド
これらで日常作業のほとんどをカバーできます:
docker ps— 実行中のコンテナを一覧表示。-aを付けると停止中も含む。docker logs -f <container>— アプリケーション出力をストリーム表示。docker exec -it <container> sh— 実行中のコンテナ内でシェルを開く。docker stop <container>— グレースフルシャットダウン。docker images— ローカルイメージとサイズを一覧表示。docker system prune— 未使用のコンテナ、ネットワーク、ダングリングイメージを削除。
コンテナがすぐに終了する場合は、まずログを確認します。よくある原因は、CMDの誤り、環境変数の欠落、コンテナ内でプロセスが0.0.0.0ではなく127.0.0.1にバインドしていることです。
Composeが存在する理由
実際のアプリケーションは通常、複数のプロセスを必要とします:API、データベース、場合によってはキャッシュ。長いフラグリストを持つ3つのdocker runコマンドを実行するのはエラーが起きやすいです。Docker Composeを使えば、すべてのサービスを1つのYAMLファイルで宣言し、まとめて起動できます。
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:APIがlocalhostではなくdbに接続していることに注目してください。Composeはプライベートネットワークを作成し、各サービスはサービス名で到達可能です。これは初心者が最も混乱しやすい点です。
すべてを起動するにはdocker compose up、バックグラウンドで実行するにはdocker compose up -dを使います。コード変更後はdocker compose up --buildで再ビルドします。停止するにはdocker compose down、名前付きボリュームも削除したい場合は-vを追加します。
ボリューム、バインドマウント、ライブリロード
コンテナは一時的で、内部のデータは削除時に消えます。これを解決する2つの仕組みがあります:
- 名前付きボリューム — Dockerが管理し、データベースファイルに最適。コンテナ再起動後も永続化され、
volumes:セクションで宣言します。 - バインドマウント — ホストディレクトリをコンテナにマッピングし、開発中のソースコードに最適。
services:
api:
build: .
volumes:
- ./src:/app/srcバインドマウントを使うと、ホストでファイルを編集するとコンテナ内に反映されます。ファイルウォッチャー(nodemon、uvicorn --reload、air)と組み合わせれば、イメージを再ビルドせずにライブリロードが得られます。
実践的な開発ワークフロー
- アプリ用のDockerfileを書き、最小限に保つ。
- アプリと依存関係を含む
docker-compose.ymlを追加する。 - ソースコードにはバインドマウント、データには名前付きボリュームを使う。
- シークレットはファイルに含めず、
.envファイルを使い${VAR}で変数を参照する。 - ビルドを高速化するため、
node_modules、.git、ビルド出力を含む.dockerignoreを追加する。 docker compose up --buildを実行して反復する。
サービスが後で確認したいログを生成する場合、コンテナログは単なるテキストであることを覚えておいてください。nginxログアナライザーのようなサーバーログを解析するツールは、ホストに何もインストールせずにログをパイプまたはコピーできるため、コンテナと相性が良いです。
よくある落とし穴
- 肥大化したイメージ:マルチステージビルドを使い、1つのステージでコンパイルし、成果物だけをスリムなランタイムイメージにコピーする。
- すべてを再ビルド:Dockerfileの命令を変更頻度の低い順から高い順に並べる。
- ハードコードされたホスト:Compose内ではサービス名を使い、外部では環境変数を使う。
- クリーンアップの無視:ダングリングイメージと停止したコンテナが蓄積します。定期的に
docker system pruneを実行する。 - rootとして実行:本番イメージでは非rootの
USERを追加する。
FAQ
イメージとコンテナの違いは何ですか?
イメージはファイルシステムとメタデータを含む不変のテンプレートです。コンテナはそのイメージの実行インスタンスで、書き込み可能なレイヤーが追加されています。1つのイメージから多数のコンテナを起動できます。
単一のサービスにDocker Composeは必要ですか?
いいえ。1つのコンテナにはdocker runコマンド1つで十分です。Composeは複数のサービス、共有ネットワーク、または1か所で宣言したい環境変数がある場合に価値を発揮します。
コンテナ同士はどうやって通信しますか?
Composeはすべてのサービスを共有ネットワークに配置し、各サービスはサービス名で到達可能です。その名前をホスト名として使います。例えばlocalhostの代わりにdb:5432です。
Dockerは少しの初期投資で報われます。イメージ、コンテナ、Composeが理解できれば、チームメイトのオンボーディングやマシンの切り替えはセットアップの儀式ではなく、1つのコマンドになります。