2026年8月10日
2026年8月10日
Dockerセキュリティのベストプラクティス(イメージ・実行時・デーモン・ネットワーク)
Docker のコンテナはホストの Linux カーネルを共有して動きます。仮想マシンほど強く隔離されているわけではないので、初期設定のまま、root 権限で、広い権限を持たせて動かすと、コンテナ内の脆弱性がホストの乗っ取りにつながることがあります。
この記事では、Docker をサーバーで運用するときに押さえておきたい設定を、次の5つの観点で説明します。
- イメージを小さく新しく保ち、脆弱性を調べる
- Dockerfile でコンテナを root 以外のユーザーで動かす
docker runで権限と資源を絞る- Docker デーモンの設定(
daemon.json) - 公開ポート、ファイアウォール、Docker API の守り方
各設定は、2026年11月時点の Docker 公式ドキュメントで確認し、Docker Engine 29.9 で実際にコンテナを動かして効き目を確かめています。
よくある危険な状態
- ベースイメージが古く、既知の脆弱性(CVE)を含んだまま使われている
- コンテナ内のプロセスが root で動き、ホストの
/や/var/run/docker.sockをマウントしている -p 8080:80で公開したポートが、ファイアウォールで閉じたつもりでも外部から見えている- Docker API(2375番)が認証なしでネットワークに公開されている。公開された API は、コンテナを勝手に作られて暗号資産の採掘などに使われる被害が知られています
ステップ1:小さく新しいベースイメージを使い、脆弱性を調べる
含まれるパッケージが少ないほど、脆弱性の対象も減ります。公式イメージの slim 版や alpine 版を選び、タグはバージョンを明記します。
# バージョンを明記した slim 版を使う
docker pull python:3.12-slim
# Trivy で深刻度 HIGH・CRITICAL の脆弱性を調べる
trivy image --severity HIGH,CRITICAL python:3.12-slim
# Docker Scout でも調べられる
docker scout cves python:3.12-slim
latest のようなタグは、指す中身が後から変わります。本番では python:3.12-slim のようにバージョンを書くか、ダイジェスト(python:3.12-slim@sha256:…)で固定します。固定したら、ベースイメージの更新に合わせて定期的にビルドし直し、スキャンをかけ直してください。固定したまま放置すると、修正済みの脆弱性が残り続けます。
スキャンの使い方は TrivyとSnykでDockerイメージの脆弱性を調べる で詳しく説明しています。
ステップ2:Dockerfile で root 以外のユーザーを使う
USER を指定しないと、コンテナ内のプロセスは root で動きます。専用のユーザーを作って切り替えます。
FROM node:22-alpine
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
# アプリのファイルは root の所有のままにし、実行ユーザーには書き換えさせない
COPY --chown=root:root server.js .
USER app
EXPOSE 8080
CMD ["node", "server.js"]
docker build -t myapp:1.0 .
docker run --rm myapp:1.0 id
uid=100(app) のように root 以外が表示されれば切り替わっています(検証環境の表示)。コードを実行ユーザーの所有にしてしまうと、侵入されたときにアプリ自体を書き換えられるので、書き込みが必要なディレクトリだけを実行ユーザーに渡します。
root 以外のユーザーは 1024 番未満のポートで待ち受けられないので、アプリは 8080 番などで待ち受け、-p 80:8080 のように公開側で番号を変えます。詳しくは Dockerコンテナを非rootユーザーで動かす方法 を参照してください。
ステップ3:docker run で権限と資源を絞る
docker run -d --name web \
--cap-drop=ALL \
--read-only --tmpfs /tmp \
--security-opt no-new-privileges:true \
--pids-limit 100 --memory 256m \
-p 127.0.0.1:8080:8080 \
myapp:1.0
| オプション | 効果 |
|---|---|
--cap-drop=ALL |
Linux のケーパビリティ(root の権限を細かく分けたもの)をすべて外す。必要なものだけ --cap-add で戻す |
--read-only --tmpfs /tmp |
コンテナのファイルシステムを読み取り専用にし、一時ファイル用に /tmp だけ書き込めるようにする |
--security-opt no-new-privileges:true |
setuid のプログラムなどで権限が上がるのを防ぐ |
--pids-limit・--memory |
プロセス数とメモリの上限。暴走や fork 爆弾でホストが止まるのを防ぐ |
-p 127.0.0.1:8080:8080 |
ホストの 127.0.0.1 だけで公開する(手前のリバースプロキシから使う場合) |
検証では、この設定のコンテナで有効なケーパビリティが 0、NoNewPrivs が 1、/app への書き込みが「Read-only file system」で拒否され、/tmp には書き込めることを確認しました。8080 番のように 1024 以上のポートで待ち受けるアプリなら、NET_BIND_SERVICE を戻す必要もありません。
次のオプションは、コンテナとホストの隔離を大きく弱めます。使うのは理由がはっきりしている場合だけにし、使う理由を記録しておきます。
--privileged:ほぼすべての権限とデバイスへのアクセスを与える--pid=host・--network=host:ホストのプロセスやネットワークをそのまま共有する-v /var/run/docker.sock:/var/run/docker.sock:Docker デーモンを操作できる。ホストの root 権限を渡すのと同じです
ソケットのマウントが必要になる場面とその代わりの方法は、Dockerソケットを安全に扱う方法 にまとめています。資源の上限については Dockerコンテナのリソース制限 も参考にしてください。
ステップ4:Docker デーモンを設定する(daemon.json)
/etc/docker/daemon.json で、すべてのコンテナに効く既定値を決められます。既存のファイルがある場合は、上書きせずに項目を追加してください。
{
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"icc": false,
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}
}
| 項目 | 内容 |
|---|---|
live-restore |
デーモンの再起動中もコンテナを動かし続ける。Swarm モードとは併用できない |
userland-proxy |
false にすると、ポートの転送をユーザー空間のプロキシではなく iptables で行う |
no-new-privileges |
すべてのコンテナに no-new-privileges を既定で付ける |
icc |
false にすると、既定のブリッジネットワーク上のコンテナ同士の通信を止める(独自に作ったネットワークには効かない) |
log-opts |
ログファイルの大きさと世代数の上限。ログでディスクが埋まるのを防ぐ |
反映の前に、設定ファイルの書き方を確かめます。
sudo dockerd --validate --config-file /etc/docker/daemon.json
sudo systemctl restart docker
configuration OK と表示されれば書き方は正しいです(上の設定はこの方法で確認しました)。live-restore が無効の状態でデーモンを再起動すると、動いているコンテナも止まるので、作業時間に注意してください。
さらに隔離を強めたい場合は、Docker デーモン自体を一般ユーザーで動かす rootless モードや、コンテナの root をホストの一般ユーザーに対応させる userns-remap があります。どちらも、ボリュームの所有者や使えない機能が変わるので、公式ドキュメントで制約を確認してから試してください。
ステップ5:公開ポートとファイアウォールの関係
Docker の公式ドキュメントは、ポートの公開について次の点を注意しています。
-p 8080:80のように IP を書かずに公開すると、ホストのすべてのアドレスで公開される- 公開したポートへの通信は、UFW の設定を通る前に Docker のルールで転送されるため、UFW で閉じたつもりでも外部から届く
対策は次のとおりです。
- 外部に直接見せる必要がないコンテナ(データベース、管理画面、リバースプロキシの後ろのアプリ)は、
-p 127.0.0.1:8080:80のように 127.0.0.1 で公開する。データベースはそもそも-pで公開せず、Docker のネットワーク経由でだけつなぐ - 既定の公開先を 127.0.0.1 にしたい場合は、
daemon.jsonに"ip": "127.0.0.1"を書く(既定のブリッジネットワークに効く) - 公開したポートが外から見えるかは、別のマシンから接続して確かめる
# ホストで待ち受けているアドレスとポートを確認する
sudo ss -tlnp
ステップ6:Docker API を外に出さない
Docker API を TCP(2375番)で認証なしに公開すると、ネットワーク上の誰でもコンテナを作り、ホストのファイルをマウントできてしまいます。公開しないのが原則です。
# 2375・2376 番で待ち受けていないか確認する
sudo ss -tlnp | grep -E ":2375|:2376"
何も表示されなければ TCP では待ち受けていません。別のマシンから Docker を操作したい場合は、SSH 経由で接続する方法が簡単です。
# 手元のマシンから、SSH でログインできるサーバーの Docker を操作する
docker -H ssh://ユーザー名@サーバーのアドレス ps
TCP で公開するしかない場合は、公式ドキュメント「Protect the Docker daemon socket」の手順で、2376番にクライアント証明書を使う TLS の相互認証を設定し、ファイアウォールで接続元も限定します。詳しくは Docker APIへのリモートアクセスを安全に設定する を参照してください。
また、docker グループに入ったユーザーは、sudo なしでホストの root と同等の操作ができます。グループに入れるのは管理者だけにしてください。
秘密情報の扱い
- パスワードや API キーを
ENVで Dockerfile に書くと、イメージの履歴に残ります。docker run -eで渡した値もdocker inspectで見えます。 - Docker Compose の
secrets(ファイルを/run/secrets/にマウントする)や Swarm の Docker secrets、外部の秘密情報管理サービスを使い、イメージや環境変数に残さないようにします。 - ビルド時だけ必要な秘密情報は、
docker build --secretで渡すとイメージに残りません。
まとめ
- イメージは小さくバージョンを固定し、定期的にビルドし直してスキャンします。
- Dockerfile に
USERを書き、--cap-drop=ALL・--read-only・no-new-privileges・資源の上限を基本にします。--privilegedと docker.sock のマウントは避けます。 daemon.jsonはログの上限とno-new-privilegesなどを設定し、dockerd --validateで確かめてから反映します。- 公開ポートは UFW を通らないので、外に見せないコンテナは 127.0.0.1 で公開し、Docker API は TCP で公開せず SSH 経由で操作します。