この記事でわかること:Dockerコンテナ化の全体像
近年、アプリケーション開発の現場ではDockerを用いたコンテナ化が標準的な手法となっています。しかし「なんとなく聞いたことはあるけれど、実際に何ができるのかよくわからない」「Dockerfileは書いたことがあるが、Docker Composeまで使いこなせていない」という方も多いのではないでしょうか。
この記事では、Docker完全マスターコース(Udemy)の知識も参考にしながら、、Dockerの基本概念からDockerfile・Docker Composeの実践的な使い方、さらにはマルチステージビルドまでを体系的に解説します。バックエンド開発者やインフラエンジニアを目指す方が、コンテナ化技術を日常の開発フローに取り込めるようになることを目標としています。
Dockerを学ぶことで得られるメリット
- 環境差異の解消:「自分の環境では動いたのに本番では動かない」という問題を根本から防ぐことができます。
- 開発環境の素早いセットアップ:新しいメンバーがチームに加わったとき、コマンド数行で同一の開発環境を再現できます。
- 本番環境への安全なデプロイ:開発・ステージング・本番を同一のイメージで動かすことができ、デプロイリスクを大幅に低減できます。
- マイクロサービス構成の実現:複数のサービスをコンテナとして独立させ、スケールや障害対応を柔軟に行えます。
- CI/CDパイプラインとの統合:コンテナイメージを軸にしたビルド・テスト・デプロイの自動化が容易になります。
記事全体のロードマップ
この記事は以下の流れで進みます。順番に読むことで、Dockerの概念から実際の運用ノウハウまでを段階的に習得できます。
| ステップ | テーマ | 習得できること |
|---|---|---|
| Step 1 | Dockerの基本概念 | コンテナ・イメージ・レジストリの関係を理解する |
| Step 2 | Dockerfileの作成 | 自分でイメージをビルドし、コンテナを起動できる |
| Step 3 | Docker Compose | 複数コンテナを連携させてアプリ全体を管理できる |
| Step 4 | マルチステージビルド | 本番用に最適化した軽量イメージを作成できる |
対象読者と学習ゴール
この記事は主に以下の方を対象にしています。
- Dockerという名前は知っているが、実際に手を動かしたことがないバックエンド開発者
- インフラ構築の基礎を学んでいる初心者エンジニア
- Docker単体は使えるが、Docker Composeやマルチステージビルドに不安がある方
記事を最後まで読み終えたとき、「自分のアプリをDockerでコンテナ化し、Docker Composeを使って複数サービスを連携させながら、本番向けに最適化したイメージをビルドできる」という状態を目指します。
Dockerとは?コンテナ化の基本概念をわかりやすく解説
Dockerは、アプリケーションとその実行に必要なすべての依存関係(ライブラリ・設定ファイル・ランタイムなど)を「コンテナ」という単位でパッケージ化し、どの環境でも同じように動かすことができるプラットフォームです。
Dockerを理解するうえで最初につまずきやすいのが「仮想マシンとどう違うのか」という点です。以下で詳しく比較しながら、Dockerがどのような仕組みで動いているかを確認しましょう。
Dockerのアーキテクチャは大きく3つの要素で構成されています。
- Dockerデーモン(dockerd):ホストOS上でバックグラウンドで動作するサービスです。コンテナの作成・実行・停止といった実際の処理を担当します。
- Dockerクライアント(docker CLI):ユーザーがターミナルから入力するコマンドを受け付け、デーモンへAPIリクエストを送る役割を持ちます。
- レジストリ:イメージを保存・配布するリポジトリです。公開レジストリとしてDocker Hubが代表的で、プライベートレジストリも構築できます。
コンテナ化が解決する最も典型的な問題が「環境差異」です。開発者Aのノートパソコン(macOS)では動くのに、開発者BのPC(Windows)や本番のLinuxサーバーでは動かない、という経験は多くの開発者が一度は体験したことがあるでしょう。Dockerは「アプリの動作に必要なものをすべてコンテナ内に含める」ことでこの問題を解決します。
仮想マシン vs コンテナ:何が違うのか
仮想マシン(VM)とコンテナは、どちらも「アプリを隔離された環境で動かす」という目的を持ちますが、その仕組みは大きく異なります。
仮想マシン(ハイパーバイザー型)の構造:
ハイパーバイザー(VMwareやVirtualBoxなど)がホストOS上で動作し、その上に仮想的なハードウェアを提供します。各VMはそれぞれ独立したゲストOSを持ちます。つまり、1台のホストマシン上で「ゲストOSを複数起動している」状態になります。
コンテナ型の構造:
コンテナはホストOSのカーネルを共有します。ゲストOSを持つ必要がないため、必要なライブラリとアプリ本体だけを含めた軽量な実行環境を作ることができます。LinuxのNamespaceとcgroupsという機能を使って、プロセス・ファイルシステム・ネットワークを隔離しています。
| 比較項目 | 仮想マシン(VM) | コンテナ(Docker) |
|---|---|---|
| OSの構成 | ゲストOSを個別に持つ | ホストOSのカーネルを共有 |
| 起動速度 | 数分単位(OSのブートが必要) | 数秒以内(プロセス起動に近い) |
| リソース消費 | 大きい(OSごとにメモリ・CPUを消費) | 小さい(アプリとライブラリのみ) |
| ポータビリティ | イメージサイズが大きく移動しにくい | 軽量で配布・移動が容易 |
| 隔離レベル | 高い(ハードウェアレベルの隔離) | 中程度(OSカーネルは共有) |
| 主な用途 | 異なるOSの実行、完全な隔離が必要な場面 | アプリの開発・デプロイ・スケールアウト |
この比較からわかるとおり、コンテナは起動速度とリソース効率に優れており、アプリケーションの開発・デプロイに特に適しています。一方で、カーネルを共有するためセキュリティ要件が非常に厳しい環境では、VMと組み合わせて使われることもあります。
DockerのコアコンポーネントImage・Container・Registryを理解する
Dockerを使いこなすためには、Image・Container・Registryの3つのコンポーネントとその関係を正確に理解することが重要です。
Image(イメージ)とは:
Dockerイメージはアプリケーションの「設計図」あるいは「スナップショット」と考えるとわかりやすいです。OS・ランタイム・ライブラリ・アプリのコードなど、コンテナを起動するために必要なすべてのファイルが階層化されたレイヤー構造として格納されています。イメージ自体は読み取り専用であり、変更することはできません。
Container(コンテナ)とは:
コンテナはイメージを実際に実行した状態のものです。プログラムのたとえで言えば、Imageがクラス定義であり、Containerはそのインスタンスです。1つのイメージから複数のコンテナを同時に起動することができます。コンテナには書き込み可能な薄いレイヤーが追加され、実行中の状態変化はそのレイヤーに記録されます。コンテナを停止・削除しても、元のイメージは変化しません。
Registry(レジストリ)とは:
レジストリはDockerイメージを保存・共有するためのリポジトリサービスです。GitHubのコードリポジトリに相当するものと考えると理解しやすいでしょう。
- Docker Hub:DockerがデフォルトとするパブリックレジストリでNginx・MySQL・Node.jsなど公式イメージが多数公開されています。
- Amazon ECR / Google Artifact Registry:クラウドプロバイダーが提供するプライベートレジストリサービスです。
- 自社プライベートレジストリ:社内ネットワーク内に独自のレジストリを構築することも可能です。
イメージの取得・配布の流れは以下のとおりです。
- pull:
docker pull nginxのようにコマンドを実行すると、レジストリからローカルマシンにイメージがダウンロードされます。 - push:
docker push myusername/myapp:latestのようにコマンドを実行すると、ローカルで作成したイメージをレジストリにアップロードできます。 - tag:イメージにはタグ(バージョン番号など)を付けて管理します。タグを省略した場合は自動的に
latestが適用されます。
Dockerfile基礎:最初のイメージを自分で作る
Dockerfileは、Dockerイメージをどのように構築するかを記述したテキストファイルです。ベースとなるOSやランタイムの指定から、ファイルのコピー、コマンドの実行、ポートの公開まで、イメージ作成の手順をすべてこのファイルに記述します。
Dockerfileを正しく書けるようになることが、Dockerを活用する最初の重要なステップです。以下では基本的な命令の解説から実際のアプリケーションへの適用例まで順を追って説明します。
主要命令リファレンス:FROM・RUN・COPY・CMD・ENTR
depends_onとhealthcheckで起動順序と死活監視を制御する
depends_onだけでは起動完了を保証できない理由
Docker Composeで複数のサービスを扱う際、多くの開発者がまずdepends_onを使って起動順序を制御しようとします。しかし、depends_onが保証するのは「コンテナの起動順序」だけであり、「サービスが実際に利用可能な状態になったか」は関知しません。
たとえばPostgreSQLのコンテナが起動したとしても、データベースプロセスがリクエストを受け付けられる状態になるまでには数秒かかることがあります。その間にアプリケーションコンテナが接続を試みると、接続エラーが発生してアプリが異常終了してしまいます。これがdepends_onだけでは不十分な理由です。
healthcheckを使ってDBが準備完了してからアプリを起動するパターン
healthcheckとdepends_onのconditionオプションを組み合わせることで、サービスが本当に「ヘルシー」な状態になってから次のコンテナを起動させることができます。以下は代表的な設定例です。
| 項目 | 設定内容 | 説明 |
|---|---|---|
| test | pg_isready -U postgres | DBが接続可能かチェックするコマンド |
| interval | 5s | チェックの間隔 |
| timeout | 3s | タイムアウトまでの秒数 |
| retries | 5 | 失敗と判定するまでの試行回数 |
| condition | service_healthy | depends_on側でヘルシー状態を待つ条件 |
docker-compose.ymlでは、dbサービス側にhealthcheckを定義し、appサービスのdepends_onにcondition: service_healthyを設定します。これにより、DBが完全に起動し接続可能な状態になるまで、アプリコンテナの起動が待機されます。本記事執筆時点の情報です。最新情報は公式サイトでご確認ください。
マルチステージビルドで本番イメージを軽量化する
Dockerfileを書き始めた段階では、ビルドツールや開発用ライブラリを含んだまま本番イメージを作成してしまうことがよくあります。マルチステージビルドはこの問題を解決するDockerの重要な機能です。
マルチステージビルドの仕組みをビフォーアフターで図解
シングルステージビルドでは、コンパイラ・テストツール・開発依存パッケージがすべて最終イメージに含まれてしまいます。一方、マルチステージビルドでは複数のFROM命令を1つのDockerfileに記述し、最終ステージには必要なファイルだけをコピーします。
- シングルステージ(ビフォー):ビルドツール+ソースコード+成果物がすべて混在し、イメージサイズが数百MB〜1GBを超えることも
- マルチステージ(アフター):最終ステージには実行バイナリや静的ファイルのみが含まれ、数十MBに圧縮可能
ビルド用ステージと実行用ステージを分離してイメージサイズを削減
マルチステージビルドの基本的な考え方は「作るステージ」と「動かすステージ」を明確に分離することです。ビルドステージでは依存ライブラリのインストールやコンパイルを行い、実行ステージではその成果物だけを受け取ります。COPY --from=builderという記述で前のステージからファイルをコピーするのが基本パターンです。
Go・Node.js・Javaそれぞれの代表的なマルチステージビルド例
言語ごとにマルチステージビルドのパターンは異なります。代表的な3言語の概要を以下に示します。
| 言語 | ビルドステージのベースイメージ | 実行ステージのベースイメージ | コピーする成果物 |
|---|---|---|---|
| Go | golang:1.22-alpine | scratch または alpine | コンパイル済みバイナリ |
| Node.js | node:20-alpine | node:20-alpine(slim) | distディレクトリ・node_modules(本番用のみ) |
| Java | maven:3.9-eclipse-temurin-21 | eclipse-temurin:21-jre-alpine | jarファイル |
本記事執筆時点の情報です。最新情報は公式サイトでご確認ください。
なぜイメージサイズが重要なのか:セキュリティとパフォーマンスの観点
サイズが大きいほど攻撃対象面(アタックサーフェス)が増えるリスク
イメージサイズの削減はパフォーマンスだけでなく、セキュリティ上も重要な意味を持ちます。イメージに含まれるパッケージやバイナリが多いほど、脆弱性を含む可能性のあるコンポーネントも増加します。これを「アタックサーフェス(攻撃対象面)の拡大」と呼びます。
たとえば、シェル(bash/sh)がコンテナに含まれていると、万が一コンテナに侵入された際に攻撃者が対話的なコマンドを実行できてしまいます。scratchやdistrolessイメージを使えばシェルすら含まれないため、侵害時のリスクを大幅に低減できます。
CI/CDパイプラインのプッシュ・プル時間短縮の効果
イメージサイズの削減は開発効率にも直結します。CI/CDパイプラインでは毎回イメージのビルドとプッシュ・プルが発生するため、イメージが重いと以下のような問題が生じます。
- レジストリへのプッシュ時間が増加し、パイプライン全体の所要時間が延びる
- デプロイ先サーバーでのプル時間が長くなり、ダウンタイムやローリングアップデートの完了が遅れる
- コンテナレジストリのストレージコストが増加する
イメージサイズを数百MBから数十MBに削減するだけで、デプロイ時間を数分単位で短縮できるケースも珍しくありません。
実践:Node.jsアプリのマルチステージビルドDockerfileを書く
builderステージでnpm run buildを実行し成果物だけをコピーする手順
Node.jsアプリのマルチステージビルドでは、まずbuilderステージでソースコードの変換・バンドルを行い、次の実行ステージでは本番用の成果物だけを受け取ります。基本的な流れは次のとおりです。
- 第1ステージ(builder):
node:20-alpineをベースにnpm ciで依存解決、npm run buildでビルドを実行 - 第2ステージ(production):同じく
node:20-alpineをベースに、builderステージの/app/distと本番用node_modulesのみをコピー - 開発用devDependenciesはビルドステージにとどまり、最終イメージには含まれない
alpine系ベースイメージを使ってさらに軽量化するテクニック
alpine系イメージはBusyBoxとmusl libcをベースにした軽量Linuxで、通常のDebian/Ubuntu系イメージに比べてサイズが大幅に小さいのが特徴です。node:20-alpineはフルのnode:20と比べて約1/3のサイズに抑えられます。
ただし、alpine系にはmusl libcを使用しているため、glibc依存のネイティブモジュールが動作しないケースがあります。node-gypで構築されるネイティブアドオンを利用する場合は、ビルドステージで必要なビルドツール(alpine-sdk等)を追加するか、node:20-slimへの変更を検討してください。
Dockerのベストプラクティス:品質と保守性を高める書き方
Dockerを実際の開発・本番環境で使い続けるためには、単に動くDockerfileを書くだけでなく、保守性・セキュリティ・効率性を考慮した書き方を習慣づけることが重要です。ここでは押さえておくべき代表的なベストプラクティスを解説します。
- 1コンテナ1プロセス原則:1つのコンテナには1つの責務だけを持たせることで、スケーリング・ログ管理・障害切り分けが容易になる
- レイヤーキャッシュの最大活用:命令の順序を工夫してビルド時間を短縮する
- タグのバージョン固定:
latestタグは再現性がなく意図しないバージョンアップが起きるため、node:20.11.1-alpine3.19のように明示的にバージョンを指定する - 非rootユーザーでの実行:セキュリティリスクを低減するためにrootでプロセスを動かさない
レイヤーキャッシュを活かすDockerfileの命令順序
変更頻度の低い命令を上に、高い命令を下に配置するルール
Dockerのビルドキャッシュはレイヤー単位で機能します。あるレイヤーのキャッシュが無効化されると、それ以降のすべてのレイヤーが再実行されます。このため、変更頻度の高い命令が上部にあると毎回全レイヤーが再ビルドされてしまいます。
基本的な設計原則は「変更頻度が低い命令ほど上部に配置する」ことです。Node.jsアプリを例にすると、推奨順序は次のとおりです。
- ①ベースイメージの指定(FROM)
- ②OSレベルのパッケージインストール(RUN apt-get install など)
- ③依存定義ファイルのコピー(COPY package.json package-lock.json ./)
- ④依存ライブラリのインストール(RUN npm ci)
- ⑤ソースコードのコピー(COPY . .)← 最も変更頻度が高い
- ⑥ビルドコマンド(RUN npm run build)
まとめ
この記事では、Docker完全マスターコース(Udemy)の知識も参考にしながら、、Dockerの基本概念からDockerfileの書き方、Docker Composeによる複数サービスの管理、そしてdepends_onとhealthcheckを組み合わせた起動順序の制御まで、コンテナ化技術の全体像を体系的に解説しました。Dockerを活用することで、環境差異の解消・開発環境の迅速なセットアップ・安全なデプロイ・CI/CDパイプラインとの統合といった多くのメリットを、日常の開発フローに取り込むことができます。
特に重要なポイントをおさらいすると、以下のとおりです。
- Dockerfileの最適化:レイヤーキャッシュを意識した命令の順序と、マルチステージビルドの活用によって、イメージサイズの削減とビルド時間の短縮を同時に実現できます。
- Docker Composeによる複数サービスの統合管理:アプリケーション・データベース・キャッシュなど複数のコンテナを一元的に定義・起動でき、チーム開発における環境の再現性が大幅に向上します。
- depends_onとhealthcheckの組み合わせ:
depends_on単体では起動完了を保証できないため、healthcheckとcondition: service_healthyを併用することで、サービスが本当に利用可能な状態になってから次のコンテナを起動させる堅牢な構成を実現できます。 - 段階的な学習アプローチ:基礎概念の理解から始まり、Dockerfile・Docker Compose・マルチステージビルドと順を追って習得することで、実務レベルのコンテナ運用スキルを無理なく身につけることができます。
次のアクションとして、まず手元の既存プロジェクトに対してシンプルなDockerfileを作成し、docker buildとdocker runを実際に試してみることをお勧めします。基本操作に慣れたら、Docker Composeでデータベースとアプリケーションを連携させる構成に挑戦し、最終的にはマルチステージビルドを導入してイメージの軽量化まで取り組んでみてください。小さなステップを積み重ねることが、コンテナ技術を確実に習得する最短ルートです。
なお、DockerおよびDocker Composeのバージョンアップによって設定の書き方や利用可能なオプションが変わる場合があります。本記事執筆時点の情報です。最新情報は公式サイト(docs.docker.com)でご確認ください。
