2026年5月27日

2026年5月27日

Dockerコンテナのセキュリティ強化ガイド:WordPressのcompose構成で見直す設定

Docker のコンテナはホストの OS カーネルを共有しているため、仮想マシンほど強く分離されてはいません。セキュリティを強化するには「イメージ」「コンテナの実行権限」「ネットワーク」「ホストと Docker デーモン」「運用」の5つの層で、それぞれ攻撃されたときの被害を小さくする設定を重ねます。この記事では、nginx・PHP-FPM(WordPress)・MySQL を docker compose で動かす構成を例に、どの設定を何のために入れるか、入れたときにどこで動かなくなりやすいかを説明します。

前提と進め方

ここでの設定は、本番に入れる前にステージング環境で試してください。権限を絞る設定は、足りない権限があるとコンテナが起動しない、ファイルが書けないといった形で問題が出ます。1項目ずつ追加し、docker compose logs でエラーを確認しながら進めると原因を追いやすくなります。

作業前には、データベースのダンプと wp-content(ボリュームやバインドマウントの中身)のバックアップを取っておきます。Docker Engine と Compose の機能や既定値はバージョンで変わることがあるため、記事の設定は例として、利用中のバージョンの公式ドキュメントと照らし合わせてください(2026年10月現在)。

層1:イメージを信頼できる状態に保つ

コンテナの中身はイメージで決まります。脆弱性のあるパッケージや不明な作成者のイメージを使うと、実行時の設定をいくら絞っても入口が残ります。

  • 公式イメージ(Docker Official Images)や、作成元が明確なイメージを使う
  • latest ではなくバージョンを含むタグで固定し、更新は意図して行う。より厳密にはダイジェスト(@sha256:...)で固定する
  • 定期的に再ビルド・再取得して、ベースイメージのセキュリティ更新を取り込む
  • イメージの脆弱性スキャナーで既知の脆弱性を確認する

スキャナーの代表例は Trivy です。CI に組み込むと、脆弱性のあるイメージを本番に出す前に止められます。

# 重大度の高いものだけ表示する例
trivy image --severity HIGH,CRITICAL wordpress:php8.3-fpm

タグ名は例です。利用できるタグは公式イメージのページで確認してください。

秘密情報をイメージや環境変数に直接書かない

Dockerfile の ENV や compose の environment: にデータベースのパスワードを書くと、docker inspect やイメージの履歴から読めてしまいます。公式の wordpress イメージと mysql イメージは、値をファイルから読む _FILE 付きの環境変数に対応しているため、compose の secrets と組み合わせます。

層2:コンテナの権限を最小にする

コンテナ内のプロセスが乗っ取られた場合に、できることを減らすための設定です。

  • privileged: true は使わない。ホストのデバイスや権限にほぼ制限なくアクセスできる状態になります
  • cap_drop: [ALL] で Linux ケーパビリティを外し、必要なものだけ cap_add で戻す
  • security_opt: [no-new-privileges:true] で、setuid などによる権限の上昇を防ぐ
  • 可能なコンテナは read_only: true にし、書き込みが必要な場所だけ tmpfs やボリュームにする
  • pids_limit やメモリ上限を設け、暴走やフォーク爆弾でホスト全体が止まるのを防ぐ
  • /var/run/docker.sock をコンテナにマウントしない。マウントすると、そのコンテナからホストの Docker を操作でき、実質的にホストの root 権限を渡すことになります

ケーパビリティをすべて外すと、公式の nginx イメージのように root で起動してからワーカーを一般ユーザーに切り替えるイメージは動かないことがあります。その場合はログに出る権限エラーを見て、CHOWN・SETUID・SETGID・NET_BIND_SERVICE などを必要な分だけ戻します。最初から一般ユーザーで動く派生イメージ(nginx なら nginxinc/nginx-unprivileged など)を使う方法もあります。

WordPress のコンテナは、プラグインやテーマの更新、メディアのアップロードでファイルを書き込みます。read_only: true にする場合は、WordPress のファイルを置くボリュームと /tmp を書き込み可能にしておく必要があり、イメージの起動処理が書き込みを行う場所もあるため、動作確認を丁寧に行ってください。管理画面からのコード変更を止めたいなら、wp-config.php の DISALLOW_FILE_MODS と組み合わせ、更新はイメージの再ビルドで行う運用にします。

層3:ネットワークで公開範囲を絞る

外部に公開するのは nginx だけにし、PHP-FPM とデータベースには ports: を書きません。同じ compose のネットワーク内ならサービス名で通信できるため、ホストにポートを公開する必要はありません。

ホストの nginx などからリバースプロキシする場合は、127.0.0.1:8080:80 のようにループバックだけに公開します。Docker が公開したポートは iptables のルールで直接通されるため、ufw で拒否していても外部から到達できる場合がある点に注意してください。データベース用のネットワークには internal: true を付けると、そのネットワークから外部への通信も遮断できます。

compose での設定例

ここまでの内容をまとめた例です。パスワードはホスト上のファイルに置き、パーミッションを 600 などに絞り、Git には含めません。

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - wp_files:/var/www/html:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    cap_drop: [ALL]
    cap_add: [CHOWN, SETUID, SETGID, NET_BIND_SERVICE]
    security_opt: ["no-new-privileges:true"]
    networks: [front]
    restart: unless-stopped

  wordpress:
    image: wordpress:php8.3-fpm
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wp
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    secrets: [db_password]
    volumes:
      - wp_files:/var/www/html
    security_opt: ["no-new-privileges:true"]
    pids_limit: 200
    mem_limit: 512m
    networks: [front, back]
    restart: unless-stopped

  db:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp
      MYSQL_PASSWORD_FILE: /run/secrets/db_password
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
    secrets: [db_password, db_root_password]
    volumes:
      - db_data:/var/lib/mysql
    security_opt: ["no-new-privileges:true"]
    mem_limit: 1g
    networks: [back]
    restart: unless-stopped

networks:
  front:
  back:
    internal: true

volumes:
  wp_files:
  db_data:

secrets:
  db_password:
    file: ./secrets/db_password.txt
  db_root_password:
    file: ./secrets/db_root_password.txt

イメージのタグ、メモリ上限の値は環境に合わせて変えてください。back ネットワークを internal: true にしているため、db コンテナは外部と通信できません。wordpress コンテナは front 経由で外部に出られるので、プラグインの更新やAPI連携は従来どおり動きます。

層4:ホストと Docker デーモンを守る

  • Docker Engine とホスト OS を更新し続ける。コンテナの分離はカーネルとランタイムに依存しているため、ここが古いと他の設定の効果が下がります
  • docker グループに入ったユーザーは実質的に root と同等の操作ができるため、所属ユーザーを最小限にする
  • Docker デーモンの API を TCP で公開しない。必要な場合は TLS のクライアント認証を設定する
  • 既定の seccomp プロファイルと AppArmor(または SELinux)のプロファイルを無効にしない。seccomp=unconfined は原因調査の一時的な手段にとどめます

さらに分離を強めたい場合は、Docker をルートレスモードで動かす方法や、/etc/docker/daemon.json の userns-remap でコンテナ内の root をホストの一般ユーザーに割り当てる方法があります。どちらもボリュームのファイル所有者や一部の機能に影響が出るため、既存環境に入れる前に検証してください。

層5:運用で気づける状態にする

ログはコンテナの外に残し、ディスクを使い切らないようにローテーションを設定します。json-file ドライバーなら max-size と max-file を指定できます。

設定の点検には、CIS Docker Benchmark の項目に沿ってホストとコンテナを確認する docker-bench-security のようなスクリプトが参考になります。結果のすべてが自分の環境に当てはまるわけではないため、項目ごとに意味を確認して判断します。

設定を入れたら、次の観点で動作を確認します。

  • docker compose ps ですべてのサービスが起動し、再起動を繰り返していない
  • サイトの表示、管理画面へのログイン、メディアのアップロード、プラグインの更新ができる
  • docker inspect の出力に平文のパスワードが含まれていない
  • 外部から nmap などで確認し、80/443 以外が開いていない

まとめ

  • Docker の分離は仮想マシンより弱いため、イメージ・権限・ネットワーク・ホスト・運用の層ごとに被害を小さくする設定を重ねます
  • イメージはタグやダイジェストで固定し、Trivy などで脆弱性を確認し、秘密情報は secrets と _FILE 変数で渡します
  • privileged と docker.sock のマウントは避け、cap_drop・no-new-privileges・リソース上限で権限を絞ります
  • 外部に公開するのは nginx だけにし、Docker の公開ポートが ufw を通らない場合がある点に注意します
  • 変更は1項目ずつステージングで試し、ログと実際の操作で動作を確認します

お気軽にご相談ください

お見積りへ お問い合わせへ
▲