すべての開発者がいつか直面する問題
同僚のプロジェクトをクローンして実行すると、大量のエラーが表示される。「自分のマシンでは動くのに」 — 数えきれないほどのプロジェクトを台無しにしてきた言葉だ。根本原因はほぼ常に環境の違いで、Pythonのバージョン違い、システムライブラリの不足、OSのパスの差異、依存関係の競合などが挙げられる。
Dockerは出荷するものを変えることでこの問題を解決する。コードだけを渡して相手のマシンに頼るのではなく、OSライブラリ、ランタイム、依存関係、設定ファイルをまるごとポータブルな単位にまとめたコンテナとして渡すのだ。12のプロジェクトにわたって3年間Dockerを本番環境で使ってきた経験から言えば、これは本当に役立つツールの一つだ。
クイックスタート — 5分でDockerを動かす
理論に入る前に、まずDockerをインストールして動作確認してみよう。
Linuxへのインストール(Ubuntu/Debian)
# 古いバージョンがあれば削除する
sudo apt-get remove docker docker-engine docker.io containerd runc
# 依存パッケージをインストール
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
# DockerのオフィシャルGPGキーを追加
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Dockerのリポジトリを追加
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Docker Engineをインストール
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudoなしでDockerを実行する(任意だが推奨)
sudo usermod -aG docker $USER
newgrp docker
インストールの確認
docker --version
# Docker version 26.x.x, build ...
docker run hello-world
Dockerからのメッセージが表示されたら準備完了だ。hello-worldコマンドはDocker Hubからイメージをプルし、コンテナを起動して出力を表示した後、きれいに終了した。プル → 起動 → 終了。これがひとつのコマンドで表されたコンテナのライフサイクル全体だ。
Dockerの仕組み
コンテナと仮想マシンの違い
VMを使ってきた人の多くは、コンテナを単に小さなVMだと思いがちだ。しかし実際は違う — その差はサイズだけではなく、より根本的なところにある。
- VMはハードウェアを仮想化する。各VMは独自のOSカーネルを実行するため、起動に30〜60秒かかり、アプリを起動する前から1〜2GBのRAMを消費することもある。
- コンテナはホストOSのカーネルを共有する。Linuxのnamespaceとcgroupsを使ってプロセスを分離するため、起動は1秒未満で、メモリのオーバーヘッドも数MB程度だ。
わかりやすい例えを使うなら、VMはコンピュータの中で別のコンピュータを動かすようなものだ。コンテナはそのマシンを独占しているかのように振る舞うプロセスに近い。
Dockerの基本概念
- イメージ — 読み取り専用の設計図。OOPのクラスに相当する。
- コンテナ — イメージの実行インスタンス。そのクラスから生成されたオブジェクトのようなもの。
- Dockerfile — イメージを一行ずつ構築するためのレシピ。
- Docker Hub — イメージが公開されているパブリックレジストリ。コンテナ版npmと考えるとわかりやすい。
- ボリューム — コンテナの再起動後も消えない永続ストレージ。
よく使うDockerコマンド
# Docker HubからイメージをPullする
docker pull nginx:latest
# ダウンロード済みイメージの一覧
docker images
# コンテナを起動(バックグラウンド、ポートマッピング)
docker run -d -p 8080:80 --name my-nginx nginx
# 実行中のコンテナを確認
docker ps
# 停止中を含む全コンテナを確認
docker ps -a
# コンテナのログを表示
docker logs my-nginx
# 実行中のコンテナでシェルを開く
docker exec -it my-nginx bash
# コンテナを停止して削除
docker stop my-nginx
docker rm my-nginx
# イメージを削除
docker rmi nginx:latest
-p 8080:80フラグはコンテナ内のポート80をホストのポート8080にマッピングする。ブラウザでhttp://localhost:8080を開けば、nginxが起動している — docker run一発で、他のインストールは不要だ。
独自のDockerイメージを作る
既製のイメージをPullするだけでは限界がある。自分のアプリをパッケージングするところから、本当に面白くなる。
PythonアプリのDockerfileを書く
最小構成のFlaskアプリを用意しよう:
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Dockerからこんにちは!'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
# requirements.txt
flask==3.0.3
同じディレクトリにDockerfileを作成する:
# 公式のPython slimイメージをベースとして使用
FROM python:3.12-slim
# コンテナ内の作業ディレクトリを設定
WORKDIR /app
# 依存ファイルを先にコピー(レイヤーキャッシュの最適化)
COPY requirements.txt .
# 依存パッケージをインストール
RUN pip install --no-cache-dir -r requirements.txt
# アプリケーションコードをコピー
COPY app.py .
# アプリがリッスンするポートを公開
EXPOSE 5000
# コンテナ起動時に実行するコマンド
CMD ["python", "app.py"]
イメージをビルドして起動する
# イメージをビルド(.はカレントディレクトリをビルドコンテキストとして使用)
docker build -t my-flask-app:1.0 .
# 起動する
docker run -d -p 5000:5000 --name flask-demo my-flask-app:1.0
# テストする
curl http://localhost:5000
# Dockerからこんにちは!
Dockerボリュームでデータを永続化する
コンテナはエフェメラル(一時的)だ — コンテナを削除すると、内部に書き込んだものはすべて消える。データベースやステートフルなサービスには、データを永続化するためのボリュームが必要だ:
# 名前付きボリュームでPostgreSQLを起動
docker run -d \
--name postgres-db \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=myapp \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
# ボリューム一覧
docker volume ls
# ホスト上のボリュームの場所を確認
docker volume inspect pgdata
pgdataボリュームはdocker rm後も残る。同じ-v pgdata:...フラグでコンテナを再作成すれば、データはそのまま戻ってくる。
実際の現場で役立つTips
1. レイヤーキャッシュ — イメージをより速くビルドする
Dockerは各命令レイヤーをキャッシュする。アプリコードをコピーする前にrequirements.txtをコピーしておくと、コードだけが変更された場合、Dockerはキャッシュ済みの依存レイヤーを再利用してパッケージの再インストールをスキップする。40以上の依存パッケージを持つプロジェクトでは、ビルド時間が約90秒から5秒以下に短縮された。
2. .dockerignoreを活用する
.dockerignoreファイルはビルドコンテキストから不要なファイルを除外する:
__pycache__
*.pyc
.env
.git
venv/
*.log
ビルドコンテキストが小さいほどビルドが速くなる。それ以上に重要なのは、ローカルのシークレットや設定ファイルを誤ってイメージに焼き込むリスクを防げる点だ。
3. イメージにシークレットを含めない
APIキーやパスワードをDockerfileにハードコードするのはよくある危険なミスだ。代わりに実行時に渡すようにしよう:
docker run -e DATABASE_URL=postgres://user:pass@host/db my-app
またはバージョン管理外に置いた.envファイルを指定することもできる:
docker run --env-file .env my-app
4. マルチステージビルドでイメージを小さくする
GoやJavaのようなコンパイル言語では、マルチステージビルドを使うことで、ひとつのコンテナでコンパイルし、別のコンテナにバイナリだけを含めて出荷できる:
# ビルドステージ
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
# 最終ステージ — バイナリのみ、Goツールチェーンは不要
FROM debian:bookworm-slim
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]
これだけでGoイメージを約850MB(フルツールチェーン含む)から12〜20MBに縮小できる。たった数行追加するだけでそれだけの効果があるなら、やらない手はない。
5. 問題発生時にコンテナを調査する
# コンテナの詳細情報(ネットワーク、マウント、環境変数)
docker inspect my-container
# リアルタイムのリソース使用状況
docker stats
# デバッグのために停止中のコンテナでシェルを開く
docker run -it --entrypoint bash my-image
次のステップ
まずは単一コンテナに慣れてから、Docker Composeに挑戦しよう。docker-compose.ymlひとつにスタック全体(Webアプリ、PostgreSQL、Redis、メッセージキューなど)を定義し、docker compose up一発で全部起動できる。5つのターミナルタブで5つのdocker runコマンドを管理する手間はなくなる。
その先にはKubernetesがある。複数マシンにまたがってコンテナをスケールさせるための技術だ。ただし、急ぐ必要はない。Dockerの基礎だけでも、毎日使えば1週間で目に見えて生産性が上がる — それが強固な土台になる。

