Dockerを学び始めると、最初は「コンテナを起動できた!」で満足しがちですが、少し運用寄りのことをやろうとすると必ず壁になるのが Dockerイメージ です。
「なぜビルドが遅いのか」「なぜイメージが太るのか」「なぜキャッシュが効かないのか」「なぜ本番で動くはずが動かないのか」──これらの多くは、イメージの作り方と構造(レイヤー/キャッシュ/ビルドコンテキスト)を理解すると一気に解像度が上がります。
この記事では、Dockerイメージを “なんとなく作る” から “意図して作る” にステップアップするために、基礎から実践までを丁寧にまとめます。ハンズオンは「手元で動く」ことにこだわり、最後は 最適化(軽量化) と レジストリ運用(push/pull) まで繋げます。
Dockerイメージとは?(コンテナとの違いを腹落ちさせる)
Dockerの世界を一言で言うと、「アプリを動かすための環境を“成果物”として持ち運ぶ」 です。
ここでいう成果物が Dockerイメージ です。
- イメージ(image):実行に必要なファイル一式(OSの一部、ライブラリ、アプリ本体、設定など)をまとめた“読み取り専用”の塊
- コンテナ(container):そのイメージを元に起動した“実行中のプロセス” + “書き込み可能な層(コンテナの差分)”
つまり、イメージは設計図ではなく「実行できる形に固めたパッケージ」 に近いです。
コンテナは「そのパッケージを展開して動かしている状態」です。
この違いが分かってくると、「なぜコンテナを消してもイメージは残るのか」「なぜ同じイメージから複数コンテナを起動できるのか」も自然に理解できます。
Dockerイメージの構造:レイヤー(Layer)でできている
Dockerイメージを理解する上で最重要なのが レイヤー です。
Dockerfileの命令の多くは、実行されるたびに「新しいレイヤー」を作ります。
たとえば、以下のようなイメージのイメージ(語感…)です。
FROM node:20-slim(ベースとなるレイヤー)WORKDIR /app(設定のレイヤー)COPY package.json ...(ファイルを追加するレイヤー)RUN npm ci ...(依存をインストールしたレイヤー)COPY server.js ...(アプリ本体を追加するレイヤー)
この仕組みの何が嬉しいかというと、同じ操作は再利用できる(キャッシュが効く) ことです。
逆に言うと、キャッシュを壊すような書き方をすると、毎回最初から作り直しになりビルドが遅くなります。
ハンズオン:Dockerイメージを作って動かす(Node.js最小構成)
ここからは手を動かします。
「Dockerfileを書いて build して run する」という一連を体に入れましょう。
前提
- Docker Desktop(Windows/Mac)または LinuxにDockerが入っている
- ターミナル(PowerShell / Terminal / bash など)
1) サンプルアプリを用意
作業ディレクトリを作って、以下3つのファイルを置きます。
package.json
{
"name": "docker-image-handson",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^4.19.2" }
}
server.js
const express = require("express");
const app = express();
app.get("/", (_req, res) => res.send("Hello Docker Image!"));
app.get("/health", (_req, res) => res.json({ ok: true }));
app.listen(3000, () => console.log("listening on 3000"));
(任意)README.md を置いても良いですが、後で .dockerignore を学ぶときに「余計なファイルが混入し得る」ことが分かります。
2) Dockerfileを書く(キャッシュを意識した基本形)
Dockerfile
FROM node:20-slim
WORKDIR /app
# 依存関係だけ先にコピー → キャッシュが効きやすい
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
# アプリ本体をコピー
COPY server.js ./
EXPOSE 3000
CMD ["npm", "start"]
ここで大事なのは COPYの順序 です。
もし最初に COPY . . と全部コピーしてしまうと、ソースのちょっとした変更でも「レイヤーが変わった」と判定され、npm ci が毎回実行されがちです。これは体感でかなり遅くなります。
依存 → アプリ の順番に分けるのは、Dockerfileの定番テクニックです。
3) ビルド(docker build)
docker build -t docker-image-handson:1.0 .
-tはタグ付け(名前:バージョン).はビルドコンテキスト(このフォルダの中身をDockerが参照する範囲)
よくある誤解として、「Dockerfileだけ見てビルドされる」と思いがちですが、実際には ビルドコンテキスト がめちゃくちゃ重要です。
巨大なディレクトリを . にすると、その分ビルドが重くなります(次の章で .dockerignore を入れます)。
4) 起動(docker run)と動作確認
docker run --rm -p 3000:3000 docker-image-handson:1.0
-p 3000:3000はポートの転送(ホスト:コンテナ)--rmは終了後にコンテナを自動削除(作業が散らからない)
別ターミナルで動作確認:
curl http://localhost:3000/
curl http://localhost:3000/health
ブラウザで http://localhost:3000/ を開いてもOKです。
“イメージを観察する”と理解が一段上がる(便利コマンド)
動いたら終わり、ではなく、ここでイメージを観察します。
「どのレイヤーが重いか」「どんな設定で動くか」を見れるようになると、トラブル対応が速くなります。
イメージ一覧
docker images
レイヤー履歴(どの命令で容量が増えたか)
docker history docker-image-handson:1.0
詳細情報(環境変数、CMD、EXPOSE、レイヤーなど)
docker inspect docker-image-handson:1.0
inspect は情報量が多いので、慣れてきたら --format を使って必要な項目だけ抜くのもおすすめです(例:起動コマンドだけ見る等)。
ハンズオン:イメージが太る原因=ビルドコンテキスト(.dockerignore)
次に、現場でめちゃくちゃ効くのが .dockerignore です。
これを入れないと、node_modules や .git がビルド対象に入ってしまい、ビルドが遅い/容量が増える/意図しないファイルが混入する などの原因になります。
.dockerignore
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
この状態で再ビルドしてみてください:
docker build -t docker-image-handson:1.0 .
ビルドログの「転送されるコンテキストサイズ」が小さくなるのが分かるはずです。
ハンズオン:マルチステージビルド(本番で定番の軽量化)
アプリによっては、ビルドにしか使わないツール(コンパイラ、ビルド依存、テストツールなど)が増えて、イメージが大きくなります。
そのときに使うのが マルチステージビルド です。
ポイントはシンプルで、
- build用ステージ:必要なもの全部使ってビルドする
- runtime用ステージ:実行に必要な成果物だけコピーして動かす
という二段構成にすることです。
マルチステージ版Dockerfile
# ---- build stage ----
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY server.js ./
# ---- runtime stage ----
FROM node:20-slim
WORKDIR /app
COPY --from=build /app /app
EXPOSE 3000
CMD ["node", "server.js"]
ビルド:
docker build -t docker-image-handson:1.1 .
サイズ比較:
docker images | head
この例は小規模なので差が劇的ではないかもしれませんが、フロントエンドビルド(Vite/Next.js)やPythonでビルド依存があるとき、差が顕著に出ます。
タグ(tag)の設計:latestは便利だが危険もある
Dockerイメージのタグは「バージョン管理」の要です。
よくある運用例:
- 開発:
myapp:devやmyapp:latest - 本番:
myapp:1.0.3やmyapp:<git-sha>
latest は「最新っぽい」だけで、誰がいつ更新したlatestかが曖昧 になります。
CI/CDや本番では「再現性」が重要なので、固定タグ(SemVerやコミットSHA)を推奨します。
タグ付け例:
docker tag docker-image-handson:1.1 yourname/docker-image-handson:1.1
レジストリ(Docker Hub)にpush/pullして配布する
イメージの真価は「配布できる」ことです。
ローカルで作って終わりではなく、レジストリに載せるところまでやると一気に“使える知識”になります。
1) ログイン
docker login
2) push
docker push yourname/docker-image-handson:1.1
3) pull(別PCやサーバで)
docker pull yourname/docker-image-handson:1.1
運用で効く:掃除(prune)と移動(save/load)
Dockerを触り続けると、イメージやキャッシュが溜まってディスクを圧迫します。
使ってないイメージを削除
docker image prune
使ってないもの全部(慎重に)
docker system prune
オフライン環境へ渡す(save/load)
docker save -o handson.tar docker-image-handson:1.1
docker load -i handson.tar
よくあるハマりどころ(現場で起きがち)
キャッシュが効かない(毎回npm installが走る)
原因の多くはDockerfileの順序です。
依存解決(npm ci)より前に COPY . . をしていると、ファイル変更で依存レイヤーが壊れます。
package.jsonを先にコピー → npm ci → アプリコピー が鉄板です。
イメージが肥大化する
.dockerignoreがない- runtimeに不要なものを持ち込んでいる
- ビルド用ツールが入りっぱなし
→ .dockerignore + マルチステージ が基本解決策です。
秘密情報をDockerfileに書いてしまった
ENV API_KEY=... のように書くと、イメージに残る可能性があります。
秘密情報は 実行時に環境変数 や Secret管理 で渡すのが原則です。
次のステップ(この記事の続き候補)
- Dockerfileのベストプラクティス(USER/権限/最小権限)
- イメージ脆弱性スキャン(Trivy)とCI組み込み
- 署名(cosign)やSBOM(サプライチェーン対策)
- buildx で amd64/arm64 マルチアーキテクチャ対応
まとめ
Dockerイメージは、Docker運用の中心にある“成果物”です。
レイヤー構造とキャッシュを理解し、.dockerignore とマルチステージを使えるようになると、ビルドの速さ・軽さ・安全性 が一気に上がります。
コメントを残す