Webアプリケーションを開発していると、「ローカルでは正常に動くのに、サーバーにデプロイすると動かない」ということがある。例えば、手元のNode.jsは22なのにサーバーは18だったり、MySQLのバージョンが違ったり、新しいメンバーが開発環境を用意するだけで半日かかったりする。
Dockerは、こうした実行環境の差を小さくし、アプリケーションのビルド・配布・実行を標準化するための仕組みだ。ただし、「アプリをコンテナに入れる」だけでは使いこなせない。イメージとコンテナ、Dockerfile、ネットワーク、データの永続化、Composeの関係まで理解しておく必要がある。
この記事では基本概念からコマンド、Web開発でよく使う設定まで整理し、最後にNginx、Node.js+MySQL、Vue/React+Viteの3つの実践例を紹介する。
1. Dockerとは何か
Dockerは、コンテナ化されたアプリケーションをビルド・配布・実行するためのプラットフォームだ。プログラムと必要な依存関係をイメージ(Image)としてまとめ、それをもとにコンテナ(Container)を起動する。
従来のデプロイでは、サーバー側でNode.jsのインストール、依存パッケージの導入、環境変数の設定、コードの配置などを個別に行うことが多かった。
従来の方法
サーバー
├─ Node.jsをインストール
├─ 依存パッケージをインストール
├─ 環境変数を設定
├─ ソースコードを配置
└─ アプリケーションを起動
Dockerを使う場合
Dockerfile → docker build → Image
↓
Registry(必要に応じて)
↓
docker run
↓
Container
同じイメージを使えば、開発環境と本番環境の差を減らせる。ただし、すべての差がなくなるわけではない。CPUアーキテクチャ、外部サービス、設定、データベースのバージョンなどには引き続き注意が必要だ。
LinuxコンテナはLinuxカーネルを利用する。macOSやWindowsのDocker DesktopでLinuxコンテナを動かす場合、通常は内部のLinux仮想化環境を使用する。
2. Dockerと仮想マシンの違い
仮想マシンは仮想化されたハードウェア上で独立したゲストOSを動かす。一方、一般的なLinuxコンテナはホスト側のLinuxカーネルを共有する、隔離されたプロセスとして動作する。
| 比較項目 | Dockerコンテナ | 従来の仮想マシン |
|---|---|---|
| 分離する対象 | プロセスと実行環境 | 仮想コンピューター全体 |
| OSカーネル | 通常はホストのカーネルを共有 | ゲストOSが独自のカーネルを持つ |
| 起動時間 | 一般に短い | 一般に長い |
| リソース消費 | 比較的小さい | 比較的大きい |
| 主な用途 | アプリ配布・マイクロサービス・CI | OS単位の検証・異なるOSの実行 |
Dockerの動作を理解するうえで、Linuxの次の3つの仕組みは押さえておきたい。
- Namespaces:プロセスID、ネットワーク、マウントポイント、ユーザーIDなどの見え方を分離する。
- cgroups:CPUやメモリなどのリソース使用量を制御・計測する。
- レイヤー化されたファイルシステム:イメージの共通部分を再利用し、コンテナごとに書き込み可能なレイヤーを持たせる。LinuxではOverlayFSがよく使われる。
例えばCPUとメモリを制限してNginxを起動できる。
docker run -d --name limited-nginx --memory=256m --cpus=0.5 nginx:alpine
コンテナは軽量だが、仮想マシンとまったく同じセキュリティ境界を提供するわけではない。
3. Image・Container・Registryの関係
3.1 Image(イメージ)
イメージはコンテナを作成するための読み取り専用テンプレートで、アプリケーション、ライブラリ、ランタイム、設定などを含む。
docker pull nginx:alpine
docker images
nginxはイメージ名、alpineはタグ(Tag)だ。イメージ自体が実行されるわけではない。
3.2 Container(コンテナ)
コンテナはイメージから作られる実行インスタンス。同じイメージから複数のコンテナを起動できる。
docker run -d --name web1 nginx:alpine
docker run -d --name web2 nginx:alpine
それぞれ別のプロセス・書き込みレイヤー・ネットワーク空間を持つ。なお、この例ではホストにポートを公開していない。
3.3 Registry(レジストリ)
レジストリはイメージを保存・配布するサービス。Docker Hub、GitHub Container Registry、Amazon ECRなどがある。
- Docker Hub:https://hub.docker.com/
- GitHub Container Registry:https://github.com/features/packages
Dockerfile --build--> Image --run--> Container
|
push / pull
|
Registry
Dockerfileは設計手順、Imageはビルド成果物、Containerは実行インスタンスと捉えると分かりやすい。
4. Dockerのインストールと動作確認
macOS・WindowsではDocker Desktopが一般的。LinuxサーバーではDocker Engineを導入する場合が多い。
- Docker Desktop:https://www.docker.com/products/docker-desktop/
- Ubuntu向けDocker Engine:https://docs.docker.com/engine/install/ubuntu/
インストール後は次のコマンドで確認する。
docker --version
docker compose version
docker info
docker run hello-world
dockerはCLIクライアントであり、実際のコンテナ管理はDocker Engineのデーモンが行う。内部ではcontainerdやOCIランタイムなども関係する。
hello-worldはメッセージを出力すると終了するため、docker psに表示されなくても異常とは限らない。終了済みコンテナはdocker ps -aで確認できる。
5. 基本的なDockerコマンド
5.1 イメージの操作
# イメージの取得
docker pull nginx:alpine
# ローカルのイメージ一覧
docker images
# 詳細情報
docker image inspect nginx:alpine
# イメージの履歴
docker history nginx:alpine
# イメージ削除
docker rmi nginx:alpine
5.2 Nginxコンテナの起動
docker run -d --name my-nginx -p 8080:80 nginx:alpine
ブラウザで http://localhost:8080 を開く。
-d:バックグラウンドで実行する。--name:コンテナに名前を付ける。-p 8080:80:ホスト側8080番 → コンテナ側80番のポート公開を設定する。
ブラウザ → localhost:8080 → Dockerのポート公開 → Nginxコンテナ:80
別のNginxコンテナを-p 8081:80で起動することも可能。コンテナ内部では両方とも80番を利用できる。
5.3 コンテナのライフサイクル
| コマンド | 用途 |
|---|---|
docker ps | 実行中のコンテナ一覧 |
docker ps -a | 停止・終了済みを含む一覧 |
docker stop my-nginx | 停止 |
docker start my-nginx | 再起動ではなく停止済みコンテナを起動 |
docker restart my-nginx | コンテナを再起動 |
docker rm my-nginx | 停止済みコンテナを削除 |
docker logs -f my-nginx | ログを追跡 |
docker stats | CPU・メモリ使用量を確認 |
docker inspect my-nginx | 詳細設定を確認 |
コンテナの削除はイメージの削除とは別操作。ただし、コンテナの書き込みレイヤーにだけ保存したデータは削除時に失われる。
5.4 コンテナ内でコマンドを実行する
docker exec -it my-nginx sh
execは既存コンテナ内で新しいプロセスを実行する。-iは標準入力を維持し、-tは疑似端末を割り当てる。Alpine系のイメージにはbashがない場合があるため、shのほうが汎用的だ。distrolessなど、シェル自体を含まないイメージもある。
6. DockerfileでNode.jsアプリをイメージ化する
まずは外部ライブラリ不要の小さなHTTPサーバーを作る。
my-node-app/
├── package.json
├── package-lock.json
├── server.js
├── Dockerfile
└── .dockerignore
package.json:
{
"name": "docker-node-demo",
"version": "1.0.0",
"private": true,
"scripts": { "start": "node server.js" }
}
ロックファイルを作る。
npm install --package-lock-only
server.js:
const http = require('node:http');
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(JSON.stringify({
message: 'Hello Docker!',
timestamp: new Date().toISOString()
}));
});
server.listen(3000, '0.0.0.0', () => {
console.log('Server running on port 3000');
});
Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
| 命令 | 意味 |
|---|---|
FROM | ベースイメージを指定 |
WORKDIR | 作業ディレクトリを設定 |
COPY | ビルドコンテキストからファイルをコピー |
RUN | イメージのビルド中に実行 |
EXPOSE | コンテナが使う予定のポートを宣言。公開はしない |
CMD | コンテナ起動時のデフォルトコマンド |
ENTRYPOINT | 固定の実行プログラムなどを設定 |
USER | 実行ユーザーを指定 |
ENV / ARG | 環境変数 / ビルド引数 |
特にRUNとCMDの違いが重要だ。RUN npm ciはビルド時、CMD ["node", "server.js"]はコンテナ起動時に実行される。
ビルドと実行:
docker build -t my-node-app:1.0 .
docker run -d --name my-node-container -p 3000:3000 my-node-app:1.0
curl http://localhost:3000
末尾の.はビルドコンテキストを表す。また、サーバーを0.0.0.0で待ち受けるのは重要。コンテナ内の127.0.0.1だけで待ち受けると、公開ポート経由で接続できないことがある。
この例ではnode:22-alpineを使用しているが、実務ではサポート状況を確認し、検証済みのバージョンやダイジェストで固定する。Alpineはmusl libcを使用するため、ネイティブモジュールによってはglibc系イメージのほうが適する。
7. イメージレイヤー・ビルドキャッシュ・.dockerignore
次のDockerfileを考えてみる。
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
この書き方ではソースコードを1行変更するだけでもCOPY . .の結果が変わり、依存パッケージのインストールを再実行しやすくなる。
改善例:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
package.jsonやpackage-lock.jsonが変わらなければ、依存関係をインストールしたビルドキャッシュを再利用できる可能性が高くなる。
.dockerignoreも用意しておきたい。
node_modules
.git
.env
.env.*
coverage
npm-debug.log
.DS_Store
これにより不要なファイルや機密情報がビルドコンテキストに含まれにくくなる。なお、構築に必要なファイルまで除外しないように注意する。
8. データの永続化:Volume・Bind Mount・tmpfs
コンテナには書き込み可能なレイヤーがあるが、コンテナを削除するとそのレイヤーも失われる。DBのデータやアップロードファイルをそこだけに保存してはいけない。
| 方式 | 主な用途 | 特徴 |
|---|---|---|
| Volume | DBデータ・永続化データ | Dockerが管理 |
| Bind Mount | ソースコード・設定ファイル | ホスト上のパスを直接マウント |
| tmpfs | 一時的な機密データなど | 主にメモリ上に保持し、通常のディスク永続化を行わない |
8.1 Volumeの例
docker volume create mysql-data
docker run -d \
--name mysql-demo \
-e MYSQL_ROOT_PASSWORD=example123 \
-e MYSQL_DATABASE=sampledb \
-v mysql-data:/var/lib/mysql \
-p 127.0.0.1:3307:3306 \
mysql:8.4
mysql-dataというVolumeをコンテナ内の/var/lib/mysqlへマウントしている。コンテナを削除してもVolumeを削除しなければデータを保持できる。ただし、別バージョンのMySQLとの互換性は確認が必要だ。既存データディレクトリの再利用時に初期化用環境変数だけ変更しても、DB内の既存パスワードなどは自動的には更新されない。
docker rm -f mysql-demo
# 同じVolumeを付けた新しいコンテナを作成できる
Volumeはバックアップそのものではない。 本番DBはバックアップと復元テストが必要。
8.2 Bind Mountの例
docker run -d \
--name static-web \
-p 8080:80 \
--mount type=bind,source="$(pwd)/html",target=/usr/share/nginx/html,readonly \
nginx:alpine
カレントディレクトリ内のhtmlをNginxの公開ディレクトリへ読み取り専用でマウントする。ローカルのHTMLを変更すればコンテナ側にも反映される。
ただし、マウント先に元々存在したファイルはマウントによって隠れる。例えばホストのプロジェクト全体を/appにマウントすると、イメージ内の/app/node_modulesが見えなくなる場合がある。
9. Dockerネットワーク:コンテナ同士の通信
WebシステムではNginx、API、MySQLなどを別コンテナで動かすことが多い。
Browser → Nginx → API → MySQL
基本的な開発ではbridgeネットワークを押さえればよい。ほかにhost、none、複数ホスト向けのoverlayなどがある。
独自ネットワークを作成:
docker network create my-network
docker network ls
docker network inspect my-network
同じネットワークにMySQLとクライアントを起動する。
docker run -d \
--name my-mysql \
--network my-network \
-e MYSQL_ROOT_PASSWORD=example123 \
-e MYSQL_DATABASE=sampledb \
-v mysql-network-data:/var/lib/mysql \
mysql:8.4
docker run --rm -it \
--network my-network \
mysql:8.4 \
mysql -h my-mysql -u root -p
my-mysqlというコンテナ名をホスト名として使えるのがポイント。ユーザー定義のDockerネットワークでは名前解決機能が提供される。
9.1 localhostでハマりやすい理由
APIコンテナ内でlocalhostを指定すると、APIコンテナ自身を指す。別のMySQLコンテナには接続できない。
// 別コンテナのDBに接続するときの例
const dbHost = 'my-mysql';
同一ネットワーク内のコンテナ間通信に、ホスト側へのポート公開(ports)は必須ではない。DBを外部へ公開する必要がないなら、公開しないほうが安全だ。
Docker DesktopでホストOS側のサービスに接続する場合にはhost.docker.internalを使えることがあるが、Linux Engineでは設定が異なるため環境ごとの確認が必要だ。
10. Docker Compose:複数サービスをまとめて管理する
Nginx、API、MySQL、Redisを毎回長いdocker runコマンドで起動するのは大変だ。Composeでは、複数サービスの構成をYAMLで管理できる。
現在の基本コマンドはdocker compose。古い記事に出てくるdocker-composeとは区別しておきたい。
compose.yaml:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: example123
MYSQL_DATABASE: sampledb
volumes:
- db-data:/var/lib/mysql
volumes:
db-data:
起動・停止:
docker compose up -d
docker compose ps
docker compose down
downは通常、Composeが作成したコンテナやネットワークを削除するが、名前付きVolumeは原則残る。docker compose down -vはVolumeも削除するため、DBデータがある場合は要注意。
Composeでよく使う項目:
| 項目 | 役割 |
|---|---|
services | サービス定義 |
image | 使用するイメージ |
build | イメージのビルド設定 |
ports | ホストへのポート公開 |
environment | コンテナの環境変数 |
volumes | データ・ファイルのマウント |
networks | ネットワーク設定 |
depends_on | サービスの依存関係 |
healthcheck | 正常性確認 |
restart | 再起動ポリシー |
現在のCompose Specificationでは、古い設定例にあるトップレベルのversion: "3"は必須ではない。
10.1 depends_onだけではDBの準備完了を保証しない
DBのコンテナが起動しても、MySQL内部の初期化が完了したとは限らない。単純なdepends_onだけではAPIが接続に失敗する場合がある。
services:
app:
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: example123
healthcheck:
test:
- CMD-SHELL
- 'MYSQL_PWD="$${MYSQL_ROOT_PASSWORD}" mysqladmin ping -h 127.0.0.1 --silent'
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
$${...}はCompose側で先に変数展開されるのを避け、コンテナ内のシェルに変数を渡すための書き方だ。アプリ側の接続リトライも併用したい。
10.2 よく使うComposeコマンド
docker compose up -d # バックグラウンド起動
docker compose up -d --build # ビルドして起動
docker compose ps # 状態確認
docker compose logs -f # ログ追跡
docker compose logs -f app # appだけのログ
docker compose exec app sh # 起動中のappでシェル実行
docker compose run --rm app npm test # 一時コンテナでテスト
docker compose restart app # 再起動
docker compose config # 設定の検証・展開結果
docker compose down # 停止・削除
execは既存のコンテナで実行、runはサービス設定を使った一時コンテナを作るという違いがある。
11. 環境変数と機密情報の管理
環境ごとのDB接続先やポートをソースコードに直接書き込むのは避けたい。
.envの例:
APP_PORT=3000
DB_NAME=sampledb
DB_USER=appuser
DB_PASSWORD=local-dev-password
Compose側:
services:
app:
image: my-node-app:1.0
ports:
- "${APP_PORT}:3000"
environment:
DB_NAME: ${DB_NAME}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
また、.envを公開Gitリポジトリへコミットしないこと。本番のパスワードやAPIキーは適切なSecrets管理の仕組みを利用したい。
12. マルチステージビルドで本番イメージを小さくする
VueやReactの静的ファイルを配信するのに、本番コンテナへNode.jsや開発用依存パッケージをすべて残す必要はない。
ビルド用のNode.jsステージと、配信用のNginxステージを分ける。
# Stage 1: フロントエンドをビルド
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Nginxで静的ファイルを配信
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
ポイントはCOPY --from=builder。最終イメージには基本的にビルド成果物だけを持ち込める。
daemon off;はNginxをフォアグラウンドで動かす設定。通常、コンテナのライフサイクルはメインプロセスに結び付いている。
実践編:実際に動かしてみる
実践1:Docker+Nginxで静的サイトを公開する
最初はHTMLをNginxで配信するだけの構成から始めよう。難しいイメージ構築は必要ない。
手順1:ディレクトリを作る
mkdir docker-static-demo
cd docker-static-demo
mkdir html
docker-static-demo/
├── html/
│ └── index.html
└── compose.yaml
html/index.html:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Docker Nginx Demo</title>
</head>
<body>
<h1>Hello Docker!</h1>
<p>Dockerで公開した最初のWebサイトです。</p>
</body>
</html>
手順2:compose.yamlを作る
services:
web:
image: nginx:alpine
ports:
- "127.0.0.1:8080:80"
volumes:
- ./html:/usr/share/nginx/html:ro
restart: unless-stopped
./htmlがホストのディレクトリ、/usr/share/nginx/htmlがコンテナ内のディレクトリ。:roは読み取り専用という意味だ。
手順3:起動して確認する
docker compose up -d
http://localhost:8080へアクセスし、HTMLが表示されることを確認する。ローカルのindex.htmlを書き換えれば、再ビルドしなくても変更内容が反映される。
docker compose down
この例で覚えたいのはポート公開、Bind Mount、Compose、コンテナの起動と停止だ。
実践2:Node.js+Express+MySQLでユーザーAPIを作る
次は実際のWebアプリケーションに近い構成。GET /usersで一覧を取得し、POST /usersでユーザーを登録する。
使用する技術はExpress、mysql2、MySQL、Dockerfile、Compose、Volume、ヘルスチェックだ。
手順1:プロジェクトを準備する
mkdir docker-api-demo
cd docker-api-demo
npm init -y
npm install express mysql2
mkdir src
package.jsonのscriptsへ次を追加する(ほかの項目や依存パッケージは残す)。
{
"scripts": {
"start": "node src/server.js"
}
}
最終的な構成:
docker-api-demo/
├── src/
│ └── server.js
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
├── .env
├── .env.example
└── compose.yaml
手順2:APIを実装する
src/server.js:
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
app.use(express.json());
const pool = mysql.createPool({
host: process.env.DB_HOST || 'db',
port: Number(process.env.DB_PORT || 3306),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10
});
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.get('/users', async (req, res) => {
try {
const [rows] = await pool.query(
'SELECT id, name FROM users ORDER BY id'
);
res.json(rows);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to fetch users' });
}
});
app.post('/users', async (req, res) => {
const { name } = req.body || {};
if (typeof name !== 'string' || !name.trim() || name.length > 100) {
return res.status(400).json({ error: 'Invalid name' });
}
try {
const [result] = await pool.execute(
'INSERT INTO users (name) VALUES (?)',[name.trim()]
); res.status(201).json({ id: result.insertId, name: name.trim() }); } catch (error) { console.error(error); res.status(500).json({ error: ‘Failed to create user’ }); } }); async function initDatabase() { await pool.query(` CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL ) `); } async function startServer() { while (true) { try { await initDatabase(); break; } catch (error) { console.error(‘Database unavailable, retrying…’, error.message); await new Promise(resolve => setTimeout(resolve, 3000)); } } app.listen(3000, ‘0.0.0.0’, () => { console.log(‘API listening on port 3000’); }); } startServer();
ここには実務でも役立つ考え方が3つある。
- コネクションプール:HTTPリクエストごとに新しいDB接続を無制限に作らず、接続を再利用する。
- プレースホルダー付きSQL:
VALUES (?)を使ってパラメーターを渡し、文字列連結によるSQLインジェクションを避ける。 - 起動時の接続リトライ:DBコンテナが起動していてもMySQLの準備が終わっていない場合があるため、接続可能になるまで待つ。
今回のリトライ処理は初期化段階のみ。本番では切断後の再接続、リクエストのタイムアウト、正常終了処理なども考える必要がある。
手順3:Dockerfileを作る
FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]
Node.js公式イメージにはnodeユーザーが用意されている。root権限を使わないことで、権限を必要以上に広げずにアプリを動かせる。
.dockerignore:
node_modules
.git
.env
.env.*
npm-debug.log
coverage
手順4:ローカル用の環境変数を用意する
.env.example:
DB_NAME=sampledb
DB_USER=appuser
DB_PASSWORD=replace-with-local-password
MYSQL_ROOT_PASSWORD=replace-with-root-password
コピーして実際のパスワードを設定する。
cp .env.example .env
.gitignoreへ.envを追加し、パスワードをGitに登録しないこと。
手順5:compose.yamlを作る
services:
api:
build: .
ports:
- "127.0.0.1:3000:3000"
environment:
DB_HOST: db
DB_PORT: 3306
DB_USER: ${DB_USER:?DB_USER is required}
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
DB_NAME: ${DB_NAME:?DB_NAME is required}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Required}
MYSQL_DATABASE: ${DB_NAME:?Required}
MYSQL_USER: ${DB_USER:?Required}
MYSQL_PASSWORD: ${DB_PASSWORD:?Required}
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test:
- CMD-SHELL
- 'MYSQL_PWD="$${MYSQL_ROOT_PASSWORD}" mysqladmin ping -h 127.0.0.1 --silent'
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
restart: unless-stopped
volumes:
mysql-data:
この例ではnetworksを明示していない。Composeは通常、プロジェクト専用のデフォルトネットワークを作成し、サービス名で名前解決する。そのためAPIのDB接続先はDB_HOST=dbでよい。
MySQLの3306番ポートはホストへ公開していない。APIからしか使わないDBを、わざわざ外部に公開する必要はないためだ。
手順6:ビルド・起動・動作確認
docker compose up -d --build
docker compose ps
docker compose logs -f api
ユーザー登録:
curl -X POST http://localhost:3000/users \
-H 'Content-Type: application/json' \
-d '{"name":"Tanaka"}'
期待される結果:
{ "id": 1, "name": "Tanaka" }
ユーザー一覧:
curl http://localhost:3000/users
[
{ "id": 1, "name": "Tanaka" }
]
データ永続化の確認として一度コンテナを削除・再作成してみる。
docker compose down
docker compose up -d
curl http://localhost:3000/users
mysql-dataが残っているため、登録したデータも残るはずだ。ここでdocker compose down -vを実行するとVolumeも削除されるので注意する。
ブラウザ / curl
↓ localhost:3000
Express APIコンテナ(api:3000)
↓ Docker内部ネットワーク(db:3306)
MySQLコンテナ
↓
mysql-data(Volume)
この実装はあくまでローカル学習用だ。外部公開するには認証、HTTPS、権限設計、Secrets管理、バックアップ、監視などが必要になる。
実践3:Vue / React+ViteをNginxで公開する
フロントエンド開発では、Viteの開発サーバーで動かす場合と、ビルド済みファイルを本番配信する場合を分けて考えたい。
手順1:プロジェクト作成
npm create vite@latest docker-frontend-demo
cd docker-frontend-demo
npm install
画面の指示に従ってVueまたはReactを選ぶ。
docker-frontend-demo/
├── src/
├── public/
├── index.html
├── package.json
├── package-lock.json
├── vite.config.js
├── Dockerfile
├── nginx.conf
├── .dockerignore
└── compose.yaml
手順2:マルチステージDockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
手順3:Nginx設定
nginx.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
try_files $uri $uri/ /index.html;はVue RouterやReact RouterのHistoryモードで重要な設定。
例えば/users/123をブラウザで直接開くと、通常のNginxはそのパスに対応する物理ファイルを探す。ファイルが存在しなければ404になるが、SPAではindex.htmlへフォールバックさせ、画面遷移をフロントエンド側で処理させる。
なお、存在しない静的ファイルまでHTMLにフォールバックさせたくない場合は、静的ファイル向けのロケーション設定を別途調整する。
手順4:Composeで起動
.dockerignore:
node_modules
dist
.git
.env
.env.*
compose.yaml:
services:
frontend:
build: .
ports:
- "127.0.0.1:8080:80"
restart: unless-stopped
docker compose up -d --build
http://localhost:8080を開く。これはVite開発サーバーではなく、Nginxがdist内の静的ファイルを返している状態だ。ソースコードを修正したら、通常は再ビルドが必要になる。
14. 開発中のコード変更を自動反映するには?
本番向けの静的配信では修正ごとにビルドが必要だが、ローカル開発ではホットリロードしたい。
14.1 Bind Mount+Vite
services:
frontend:
image: node:22-alpine
working_dir: /app
ports:
- "127.0.0.1:5173:5173"
volumes:
- .:/app
- node_modules:/app/node_modules
command: sh -c "npm ci && npm run dev -- --host 0.0.0.0"
volumes:
node_modules:
docker compose up
コードの変更をBind Mount経由でコンテナへ反映させ、ViteのHMRを利用する。node_modulesを独立したVolumeにしているのは、ホスト側のディレクトリマウントでコンテナ内の依存パッケージが隠れるのを避けるためだ。
上記は理解しやすさを優先した例で、毎回npm ciが実行される。実際には依存インストールをイメージ構築時に行う方法も検討したい。
14.2 Compose Watch
Compose Watchを使うと、ファイル変更時の同期・再起動・再ビルドを宣言的に設定できる。
services:
app:
build: .
ports:
- "127.0.0.1:3000:3000"
develop:
watch:
- action: sync+restart
path: ./src
target: /app/src
- action: rebuild
path: ./package.json
docker compose up --watch
15. よくあるエラーと調べ方
15.1 コンテナが起動直後に終了する
docker ps -a
docker logs コンテナ名
まず終了ステータスとログを確認する。よくある原因は起動コマンドの誤り、設定ファイルの不足、依存ライブラリの問題、アプリケーションの例外など。
15.2 ポートがすでに使用されている
bind: address already in use
lsof -i :8080
docker ps
別のプログラムが8080番を使っているなら、Compose側を8081:80などに変更する。
15.3 APIからMySQLへ接続できない
Error: connect ECONNREFUSED 127.0.0.1:3306
コンテナ内のlocalhostが何を指すか確認する。ComposeならDBホスト名は通常サービス名のdb。また、DBの初期化完了前にAPIが接続していないかログを確認する。
docker compose ps
docker compose logs db
15.4 Dockerfileを変更したのに反映されない
既存コンテナを再起動するだけでは、新しいDockerfileでイメージを再ビルドしたことにはならない。
docker compose up -d --build
Bind Mountによってイメージ内のファイルが隠れていないかも確認する。
15.5 Apple Siliconでアーキテクチャが合わない
MシリーズMacでx86_64専用のイメージを使うと、exec format errorなどが発生する場合がある。
docker buildx imagetools inspect nginx:alpine
必要に応じて対応プラットフォームを指定できる。
docker run --platform linux/amd64 ...
ただしエミュレーションによって性能が低下することがある。arm64対応イメージがあるなら、通常はネイティブなものを優先したい。
15.6 Dockerがディスク容量を使いすぎる
docker system df
docker system prune
docker image prune -a
prune系のコマンドは削除対象を理解してから使う。特にdocker system prune --volumesなど、Volumeを含む削除操作には慎重になる必要がある。
16. 本番環境で意識したいセキュリティ
Dockerを使えば自動的に安全になるわけではない。最低限、次を確認したい。
- rootで実行しない:可能なら
USER nodeなど、必要最小限の権限で動作させる。 - 秘密情報をイメージに埋め込まない:DockerfileへDBパスワードやAPIキーを直書きしない。
- 不要なポートを公開しない:DBは内部ネットワークでのみ利用し、ローカル向けポートは
127.0.0.1に束縛する。 latest任せにしない:検証済みのバージョンやDigestを固定する。- Docker Socketを安易にマウントしない:
/var/run/docker.sockへのアクセスはホスト側Dockerの制御につながることがある。 - リソースを制限する:CPU・メモリ・ログ容量、ヘルスチェックや監視も考える。
- バックアップと復元手順を用意する:Volumeの存在だけではバックアップにならない。
例えばローカルのみでDBポートを公開する場合:
ports:
- "127.0.0.1:3307:3306"
CPUとメモリの制限例:
docker run -d --name web-limited --memory=512m --cpus=1 nginx:alpine
17. DockerとGit・CI/CD・Kubernetesの関係
Dockerを覚えると、GitHub ActionsなどのCI/CDともつながりやすくなる。
Gitへpush
↓
GitHub Actionsでテスト
↓
Dockerイメージをビルド
↓
GHCR / ECRなどのRegistryへpush
↓
サーバーがイメージをpullしてデプロイ
手作業でサーバーにログインしてgit pull・npm installを繰り返す方法より、デプロイ対象のイメージを明確にできる。ただしDBマイグレーションやロールバック、Secrets管理も同時に設計する必要がある。
Kubernetesは、コンテナ化されたアプリケーションを複数ノードで管理するためのオーケストレーションシステムだ。レプリカ管理、ローリングアップデート、サービスディスカバリーなどを担当する。
なお、現在のKubernetesがDocker Engineをコンテナランタイムとして必須としているわけではない。CRIを通じてcontainerdやCRI-Oなどを使用でき、Dockerで構築したOCI互換イメージも利用できる。
DockerとComposeを十分理解してからKubernetesへ進めばよいと思う。
18. 小さな実験:Node.jsのバージョンを使い分ける
DockerならローカルOSにそれぞれのNode.jsを直接インストールしなくても、異なるバージョンの実行環境を試せる。
docker run --rm node:18-alpine node --version
docker run --rm node:22-alpine node --version
カレントディレクトリにindex.jsがあれば、次のように実行できる。
docker run --rm \
-v "$(pwd):/app" \
-w /app \
node:22-alpine \
node index.js
-vはディレクトリのマウント、-wは作業ディレクトリを指定するオプション。macOS側のネイティブモジュールがLinuxコンテナでそのまま使えるとは限らないので、依存パッケージはコンテナ向けにインストールするほうが確実だ。
19. Dockerコマンド早見表
| コマンド | 用途 |
|---|---|
docker --version | CLIのバージョン確認 |
docker info | Docker環境情報 |
docker pull IMAGE | イメージを取得 |
docker images | ローカルイメージ一覧 |
docker build -t NAME . | イメージをビルド |
docker run IMAGE | コンテナを作成して起動 |
docker ps | 起動中コンテナ一覧 |
docker ps -a | 全コンテナ一覧 |
docker stop NAME | コンテナ停止 |
docker start NAME | 停止済みコンテナ起動 |
docker restart NAME | コンテナ再起動 |
docker rm NAME | 停止済みコンテナ削除 |
docker exec -it NAME sh | シェルを起動 |
docker logs -f NAME | ログを追跡 |
docker stats | リソース使用状況 |
docker inspect NAME | 詳細設定を確認 |
docker volume ls | Volume一覧 |
docker network ls | ネットワーク一覧 |
docker system df | ディスク使用量 |
docker compose up -d | Composeサービスを起動 |
docker compose up -d --build | ビルドして起動 |
docker compose ps | サービス状態 |
docker compose logs -f | サービスログ |
docker compose down | サービスを停止・削除 |
docker compose config | Compose設定を検証 |
20. 間違えやすいポイントを整理
docker runとdocker start:runは新しいコンテナを作って起動し、startは作成済みのコンテナを起動する。
docker execとdocker attach:execは新しいプロセスを実行し、attachは既存メインプロセスの入出力につなぐ。調査では通常execを使う。
docker buildとdocker compose build:前者は指定したビルドコンテキストから直接ビルドし、後者はComposeのサービス設定を使ってビルドする。
EXPOSEとports:EXPOSEはイメージの宣言、portsはホストへの公開。
restartとrebuild:コンテナの再起動ではDockerfileの変更は反映されない。イメージの再ビルドが必要。
TagとDigest:nginx:alpineのようなタグが指すイメージ内容は更新され得る。Digestを使えば特定の内容を識別できる。
downとdown -v:後者はVolumeまで削除し得るため、データ消失に注意する。
21. おすすめの学習順序
いきなりKubernetesへ進むより、小さなアプリで一通り実験するほうが理解しやすい。
- 第1段階:イメージとコンテナ —
hello-world、Nginx、ps、logs、execを試す。 - 第2段階:Dockerfile — Node.js APIをイメージ化し、
RUNとCMD、ビルドキャッシュを理解する。 - 第3段階:ネットワークと永続化 — MySQLへの接続とVolumeによるデータ保持を検証する。
- 第4段階:Composeとデプロイ — API+DBを管理し、ヘルスチェックやViteのマルチステージビルドを試す。
- 第5段階:CI/CD — GitHub Actionsなどでイメージのビルド・配布を自動化する。
22. 参考になる公式ドキュメント
- Docker Get Started — 基本概念とチュートリアル
- Dockerfile reference — Dockerfileの各命令
- Docker Build — ビルドとキャッシュ
- Docker Networking — ネットワーク
- Docker Volumes — データの永続化
- Docker Compose — 複数コンテナの管理
- Docker CLI reference — CLIコマンド一覧
- Build best practices — イメージ構築のベストプラクティス
まとめ
Dockerを理解するうえで中心になるのは、Image・Container・Dockerfile・Volume・Network・Composeの6つだ。
最初は「アプリケーションをコンテナの中で動かすためのツール」だと思っていた。しかし、実際に使ってみると、Dockerが標準化しているのは実行環境だけでなく、ビルドからテスト、配布、デプロイまでの流れでもあると分かる。
もちろんDockerを覚えたからといって、LinuxやNginx、Node.js、MySQLの知識が不要になるわけではない。例えばAPIがDBにつながらないとき、原因がポート公開なのか、コンテナのlocalhostの理解違いなのか、それともDBの準備未完了なのかを切り分けるには、それぞれの基礎知識が必要になる。
Webエンジニアなら、まず使い慣れたNode.jsやVueの小さなプロジェクトをコンテナ化し、次にMySQLを追加し、最後にComposeで一つのシステムとして管理してみるのがおすすめだ。
コマンドを暗記することよりも、「どこで何が動いていて、データと通信がどうつながっているか」を自分で説明できることが大切だと思う。
※ サンプルはローカル学習・開発環境向け。実際の本番運用では、認証・HTTPS・Secrets管理・ログ管理・監視・バックアップ・障害復旧などを別途設計する必要がある。

