Skip to main content

PHPアプリケーション向けの本番環境対応Dockerfileを作成するためのベストプラクティス

著者
Headshot of James Walker

James Walker

blog feature multi namespace k8s controller

2023年8月22日

0 分で読めます

Dockerは、コード、依存関係、ランタイム環境を自己完結型の単位にまとめ、さまざまな環境で同じように実行できるコンテナ化プラットフォームです。

PHPアプリケーションをDocker化すると、PHPランタイム、Webサーバー、ソースコード、Composerの依存関係をコンテナにまとめられるため、デプロイが簡単になります。Dockerの使い始めは簡単です。しかし、本番環境で安全に利用するには、避けるべき落とし穴がいくつかあります。適切なベースイメージの選択、ワークロードに合ったPHPランタイムの選択、セキュリティを考慮したイメージの強化は、問題を防ぐうえで欠かせません。このチュートリアルでは、PHPアプリケーション向けの信頼性の高いDockerfileの書き方を解説します。

本番環境に対応したPHP用Dockerfileを作成する

この記事を読んでいる方は、PHPアプリの作成が済み、デプロイに向けてDocker化しようとしていることでしょう。パフォーマンス、セキュリティ、信頼性、開発のしやすさを高めるためのベストプラクティスを10個紹介します。

1. バージョンを指定したベースイメージを使う

Docker Communityは、サポート対象のすべてのPHPバージョン向けに公式Dockerイメージを公開しています。ベースとなるOS、言語のリリース、Webサーバーとの連携方法(ApacheやFastCGI Process Manager(FPM)など)を組み合わせた、複数のイメージバリアントが用意されています。

要件に最も合う特定のイメージを選ぶことが重要です。php:latestのような汎用タグは避けましょう。意図しない、コードを壊す変更にさらされたり、フル機能のOSにしか必要ないライブラリまで含まれたりする可能性があります。たとえば、現在php:apacheイメージにはPHP 8.2が含まれていますが、PHP 9.0のリリースと同時に切り替わります。php:8.2-apacheを選べば、最新のバージョンではなくなった後も、常にPHP 8.2を実行できます。

FROM php:8.2-apache
WORKDIR /var/www/html

COPY index.php .

依存関係のインストールに使うcomposerなど、ほかに依存するイメージにも同じ原則が当てはまります。マシン上で使用しているcomposerのメジャーバージョンに合うイメージタグを選びましょう。composer:latestではなく、composer:2を使います。

2. 環境変数で設定を管理する

Dockerコンテナはステートレスであることを前提に設計されています。コンテナの起動時に設定する環境変数を使ってアプリケーションを構成するのがおすすめです。設定ファイルに依存すると、Dockerボリュームを使って永続ストレージを常にセットアップする必要があるため、環境変数を使うほうが適しています。

docker runでコンテナを起動するとき、-eフラグを使って環境変数を設定できます。

$ docker run -d -e DATABASE_HOST=172.26.0.2 php-app:latest

PHPコードでは、getenv()関数を呼び出して変数の値を取得できます。

// 172.26.0.2
echo getenv("DATABASE_HOST");

これらは個々のコンテナで設定するランタイム変数です。Dockerfile内で変数を設定し、コンテナに自動的に渡したい場合もあります。その場合はENV命令を使います。

FROM php:8.2-apache
WORKDIR /var/www/html

ENV DATABASE_DRIVER=sqlite

COPY index.php .

環境変数はビルド時に設定し、ARGキーワードを使ってDockerfile内のほかの命令から参照することもできます。

FROM php:8.2-apache
WORKDIR /var/www/html

ARG DEFAULT_DATABASE_DRIVER

RUN echo '{"db":"$DEFAULT_DATABASE_DRIVER"}' > config.json
COPY index.php .

引数の値を設定するには、docker buildの実行時に--build-argフラグを使います。

$ docker build --build-arg DEFAULT_DATABASE_DRIVER=sqlite -t php-app:latest .

3. PHP FPMとNginx/Apacheを検討する

FPMは、ApacheやNginxなどのWebサーバーとPHPを連携させるための、別のPHP FastCGI実装です。FPMの代わりに、mod_phpを使ってApache Webサーバーを利用する方法もあります。Apacheを使う方法と比べて、FPMは高度なノンブロッキングのプロセス管理、異なるワーカープロセスのプールの作成、より柔軟なエラー処理を実現します。大規模なアプリでは、FPMによって大幅なパフォーマンス向上が得られることも少なくありません。

PHPのDockerベースイメージには、FPM版とmod_php版があります。

  • php:8.2-fpm:このイメージおよび同様のタグは、FPM接続を公開します。

  • php:8.2-apache:これらのイメージにはApacheが含まれており、mod_phpを使ってPHPリクエストを処理します。

Apacheを使っていてmod_phpで問題がない場合は、:apacheベースイメージを選びましょう。PHPコードを/var/www/htmlディレクトリに追加すれば、すぐに実行できます。

FPMを使うには、Nginxなどのサーバーを実行する別のコンテナが必要です。このサーバーをリバースプロキシとして設定し、PHPリクエストをPHPコンテナ内のFPMプロセスが公開するポートに転送します。デフォルトのポート番号は9000です。

最大限のパフォーマンスを求め、FPMコンテナとその前段に置くWebサーバーの2つのコンテナを管理することに問題がなければ、FPMを使いましょう。Apacheベースのイメージは、追加のコンテナを設定したりFPMワーカープールを細かく調整したりせず、すばやく始めるのに適しています。

4. 本番環境では不要なデバッグログやエラー表示を無効にする

本番環境のアプリケーションでは、攻撃者にとって有用な情報を公開してはいけません。PHPのdisplay_errorsとdisplay_startup_errorsの設定を無効にして、処理されなかった例外がWebサイトに表示されないようにしましょう。

これらの値を変更するには、コンテナ内のphp.iniファイルを編集します。

まず、変更したファイルを作成します。

display_errors = Off
display_startup_errors = Off

次にDockerfileを調整し、php.iniをコンテナのファイルシステム内の適切な場所にコピーします。

FROM php:8.2-apache
WORKDIR /var/www/html

COPY php.ini /usr/local/etc/php/php.ini
COPY index.php .

本番環境で使うには、PHPのerror_reportingレベルの変更が必要な場合があります。Noticeや非推奨警告などの低レベルのエラーは、対処に役立たない情報でログをすぐに埋め尽くすことがあります。環境変数に応じて、スクリプトの起動時にこの値を動的に設定できます。

if (getenv("IS_PRODUCTION")) {
    // Disable error reports which are not an immediate issue
    error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
}

5. PHPコンテナをroot以外のユーザーで実行する

Dockerでは、コンテナはデフォルトでrootとして実行されます。コンテナ内で実行されるプロセスは、ホスト上のrootと同じ権限を持ちます。コンテナが侵害され、隔離を突破されると、悪意のあるプロセスがホスト上で任意のコマンドを実行できるようになります。

専用の非rootユーザーでコンテナを実行すると、こうしたリスクを軽減できます。Dockerfile内のUSER命令を使うと、その時点以降の実行ユーザーを変更できます。

FROM php:8.2-apache
WORKDIR /var/www/html
USER appuser

COPY index.php .

6. PHPコンテナのヘルスチェックを設定する

Dockerには、コンテナの障害を検知するヘルスチェック機能が組み込まれています。30秒ごとにヘルスチェックコマンドを自動実行するように設定できます。コマンドが終了コード0で終了すると、コンテナは正常と判断されます。一方、終了コード1で終了した場合は、アプリケーションに障害があることを示します。

DockerfileのHEALTHCHECK命令を使って、アプリケーションのヘルスチェックを設定できます。PHPサービスの場合は、curlを使ってアプリのエンドポイントにネットワークリクエストを送信できます。curlが0で終了した場合、HTTPステータスコードが成功を示す2xxのレスポンスを受信したことになります。

FROM php:8.2-apache
WORKDIR /var/www/html
USER appuser

HEALTHCHECK CMD curl --fail http://localhost/index.php || exit 1
COPY index.php .

コンテナのヘルス状態は、docker psコマンドの出力に表示されます。

$ docker ps
CONTAINER ID	IMAGE  		COMMAND              		CREATED    	STATUS
335889ed4698	php-demo-app		"docker-php-entrypoi…"   	2 mins ago 	Up 2 mins (healthy)

ヘルスチェックに失敗すると、コンテナの状態はunhealthyと表示されます。Kubernetesなどのコンテナオーケストレーターは、このシグナルを使って、障害が発生したコンテナを自動的に再起動または置き換えます。

7. 使用するポートを必要最小限にする

コンテナで不要なポートを公開するのは避けましょう。外部プロセスからアクセスする必要があるポートだけを公開します。これらのポートには、ほかのコンテナや一般ユーザーが接続します。

PHPアプリケーションでは、php:8.2-apacheのようにWebサーバーが含まれるイメージを使う場合、ポート80が該当します。FPMベースのイメージでは、別のWebサーバーコンテナが接続できるよう、ポート9000を公開します。FPMのポートをインターネットに公開してはいけません。

同様に、環境変数を通じて公開する情報にも注意しましょう。使っていない変数を削除して、情報漏えいのリスクを低減してください。docker inspectなどのツールや、コンテナ内の悪意のあるソフトウェアは、設定済みの変数すべてにアクセスできます。

8. 依存関係を定期的に更新する

新たなバグ修正やセキュリティ更新を適用するため、スタック内のすべてのコンポーネントを定期的に更新しましょう。対象には以下が含まれます。

  • Dockerイメージ内のベースOSの更新

  • 使用しているPHPバージョンと拡張機能の更新

  • プロジェクトの依存関係としてインストールしたComposerパッケージ

新たな脆弱性から保護するために、これらの更新は欠かせません。ビルドのたびに最新のベースイメージを取得できるよう、docker build --pullでイメージを再ビルドしましょう。これにより、OSパッケージやPHPリリースの更新を含む最新のベースイメージが取得されます。

また、依存関係に最新のパッチを適用するため、プロジェクトで定期的にcomposer updateを実行しましょう。Snykなどのツールを使えば、古くなった依存関係や脆弱性を含む依存関係を検出し、修正済みバージョンへの更新方法を確認できます。Composerでパッケージを更新したら、Dockerイメージを再ビルドし、本番環境へのデプロイに新しいバージョンを使いましょう。

9. コンテナのケイパビリティを必要最小限に制限する

Linuxのカーネル機能は、プロセスが実行できる、機密性の高い操作を制限します。Dockerはすでに、特権を制限した機能セットでコンテナを実行しますが、一般的なPHPのワークロードにはまだ過剰です。たとえば、デフォルトではコンテナ内のプロセスにファイル権限の変更、プロセスの終了、ユーザーIDの操作が許可されています。

docker runの--cap-addフラグと--cap-dropフラグを使って、コンテナの機能を追加・削除できます。最も安全なのは--cap-drop=allですべての機能を削除し、アプリに必要な機能だけを再び追加することです。

次の例では、コンテナにCHOWN機能だけを付与します。コンテナはファイルやフォルダの所有者を変更できますが、それ以外の特権操作は実行できません。

$ docker run -d --cap-drop=all --cap-add=CHOWN php-app:latest

10. ビルドと実行環境を分離するマルチステージビルドを使う

マルチステージビルドは、複雑なビルドパイプラインを簡単に記述・保守できる仕組みです。ビルドプロセスで複数のベースイメージを使いながら、出力を小さく、必要なものだけに絞ることができます。

ビルド中にComposerの依存関係をインストールするのが一般的なため、マルチステージ方式はPHPのDockerfileに最適です。PHPの公式DockerイメージにはComposerが含まれておらず、Composerのプロジェクトが独自にイメージを公開しています。マルチステージビルドを使えば、Dockerfile内でComposerバイナリを簡単に参照できます。

FROM php:8.2-apache
WORKDIR /var/www/html

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json .
COPY composer.lock .
RUN composer install --no-dev
RUN rm /usr/bin/composer

COPY index.php .

ビルドが完了すると、composer:2イメージは破棄されます。この方法なら、最終イメージのサイズを増やさずに、ビルド環境と実行環境を適切に分離できます。

Snykで脆弱性を検出・修正する

Dockerを使うと、PHPアプリケーションとホスト環境の間にある程度の隔離性を確保でき、セキュリティの向上につながります。しかし、コンテナイメージには、古いOSパッケージやComposerの依存関係が原因で、セキュリティ上の脆弱性が含まれていることがよくあります。

「開発者に愛され、セキュリティで信頼される」という見出し、セキュリティスキャンの画面、Snykの犬を描いたイラストが特徴のSnykウェブサイトのヒーロー画像

本番環境にデプロイする前に、イメージをスキャンして脆弱性を検出することが重要です。Snykを使えば、PHPのDockerイメージをスキャンして脆弱性を特定し、修正できます。Snyk Vulnerability Databaseには、一般的なすべてのOSや依存関係の記録が収録されており、Packagistで公開されているPHPパッケージも対象です。

Snykで脆弱性の特定を始めるには、簡単なPHPファイルを作成し、index.phpとして保存します。

<?php

echo "Hello World";

?>

次に、アプリケーション用のDockerfileを作成します。

FROM php:8.2-apache
WORKDIR /var/www/html

COPY index.php

Dockerを使ってイメージをビルドします。

$ docker build -t php-app:latest .

次にSnykのウェブサイトにアクセスし、無料で始めるをクリックしてアカウントを作成します。画面の案内に従ってセットアップし、Snyk CLIをインストールしてください。また、Node.jsとnpmもインストールする必要があります。

$ npm install -g snyk

次に、snyk authコマンドを実行します。新しいブラウザータブが開くので、SnykにログインしてCLIを認証してください。その後、ターミナルに戻ります。

イメージをスキャンするには、次のコマンドを実行します。

$ snyk container test php-app:latest

スキャンの完了まで少し時間がかかることがあります。完了すると、結果がターミナルに表示されます。検出されたすべての脆弱性について、該当するパッケージ、脆弱性のあるバージョン、詳細情報へのリンクが一覧で表示されます。

PHP Apache Dockerイメージに110件の脆弱性があることを示すターミナルレポート。重大な問題が2件含まれ、代替のベースイメージが推奨されています。

最後のサマリーには、検出された問題の合計数と、脆弱性の少ない代替ベースイメージが表示されます。

検出された脆弱性の修正

脆弱性が見つかった場合は、それぞれを評価し、対応が必要かどうかを判断してください。すべての脆弱性が、必ずしも利用環境に影響するとは限りません。まずは、重大度がクリティカル、高、高、中の問題から解決しましょう。

多くの場合、まずは更新済みのベースイメージを使ってイメージを再ビルドするのが最も効果的です。php:8.2-fpmなどのコミュニティで広く利用されているイメージは、脆弱性の修正を含む新しいバージョンが定期的に公開されています。イメージを再ビルドしたら、再度スキャンしてスコアへの影響を確認してください。

レポートには、低レベルのオペレーティングシステムコンポーネントに多数の脆弱性が記載される場合があります。攻撃対象領域を全体的に縮小するため、より小さく、最小限のベースイメージを検討してください。Alpine Linuxのphp:fpm-alpineなどのバリアントは含まれるパッケージが大幅に少なく、多くの場合、脆弱性も少なくなります。

執筆時点では、php:8.2-fpmイメージで、たとえば次のように98件の脆弱性が検出されています。

PHP 8.2のDockerイメージスキャン結果を示すターミナル出力。依存関係に98件の脆弱性が見つかり、その内訳は重大1件、高1件、中1件、低94件です。

php:8.2-fpm-alpineバリアントでは、オペレーティングシステムのパッケージが大幅に少ないため、脆弱性はわずか10件です。

PHP 8.2 Alpine Dockerイメージに、重大度がCritical、High、Medium、Lowの依存関係の脆弱性が10件含まれていることを示すターミナル出力

プロジェクトで修正できない脆弱性もあります。たとえば、オペレーティングシステムのパッケージに関する問題がワークロードに影響しない場合や、パッチが提供されていない場合です。Snykのレポートから脆弱性を除外するには、そのIDをsnyk ignoreコマンドに渡します。IDは脆弱性のURLの末尾で確認できます。たとえば、https://security.snyk.io/vuln/SNYK-ALPINE317-CURL-3320725の場合はSNYK-ALPINE317-CURL-3320725です。

$ snyk ignore --id=SNYK-ALPINE317-CURL-3320725

最後に、コンテナセキュリティは複数のコンポーネントで成り立つことを忘れないでください。セキュアなベースイメージ、最新かつサポート対象のPHPランタイム、依存関係とツールチェーンコンポーネントのパッチ適用済みバージョン、そしてアプリケーションコードが必要です。イメージを定期的にスキャンして脆弱性を確認することで、セキュリティ状況を把握できます。

まとめ

この記事を読み終えた方は、PHPアプリケーションを実行するための、本番環境対応のDockerイメージを作成できるようになっているはずです。このガイドでは、特定のベースイメージの選択、マルチステージビルドによる依存関係のインストール、本番環境にデプロイできるワークロードにするための脆弱性スキャンなど、Dockerfileの作成やビルドしたイメージの利用に関する重要なベストプラクティスを紹介しました。

DockerイメージとPHPに関するおすすめの方法をさらに知りたい方は、Dockerのドキュメントと、Snykブログのその他の記事をご覧ください。

Snykは、コードのセキュリティを担うすべての人のための、開発者に使いやすいセキュリティプラットフォームです。Dockerイメージをスキャンして、依存関係、オペレーティングシステムのパッケージ、PHPコードの脆弱性を検出できます。また、脆弱性を発生時に検出する静的解析を実行するIDEプラグインも提供しています。今すぐ登録して、PHPコンテナイメージの脆弱性を自動的に検出し、修正しましょう。

開発者ファーストのコンテナセキュリティ

Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。