これは何
Laravel 用 php-fpm イメージの Dockerfile。
(多少はフォーマット変わろうとも)色んなところでずっと使いまわししそうなのでメモ。
完全に個人の秘伝のタレ化するよりは情報公開したほうが自社にとっても利益があるだろうと判断(笑)
異論は無限に受け付けるので改善点などあればコメントください。
レシピ
Dockerfile
/docker/php-fpm/Dockerfile
FROM golang:1.15 as http2fcgi_build
# http2fcgi のビルド
RUN GO111MODULE=on go get -v -ldflags '-w -s' github.com/alash3al/http2fcgi@v1.0.0
FROM php:7.4-fpm-alpine as php_runtime
# Goバイナリが実行できるようにする
# https://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker
RUN mkdir /lib64 \
&& ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
# http2fcgi のインストール
COPY --from=http2fcgi_build /go/bin/http2fcgi /bin/http2fcgi
# Composer のインストール
COPY --from=composer:1 /usr/bin/composer /usr/bin/composer
RUN composer global require hirak/prestissimo
# Git と PHP 拡張のインストール
RUN set -eux \
&& apk add --update --no-cache git autoconf g++ libtool make libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev \
&& pecl install redis \
&& docker-php-ext-configure gd --with-jpeg=/usr \
&& docker-php-ext-configure opcache --enable-opcache \
&& docker-php-ext-install opcache bcmath pdo_mysql gd exif zip \
&& docker-php-ext-enable redis \
&& apk del autoconf g++ libtool make \
&& rm -rf /tmp/*
# 【オプション】 xdebug のインストール
# 必要に応じて PHP_DEBUGGER="xdebug" を与える
# 但し xdebug は非常に重いのでデフォルトで有効化しない
ARG PHP_DEBUGGER=""
RUN set -eux \
&& apk add --update --no-cache autoconf g++ libtool make \
&& if [ "$PHP_DEBUGGER" = "xdebug" ]; then \
pecl install xdebug; \
fi \
&& apk del autoconf g++ libtool make \
&& rm -rf /tmp/*
# 【オプション】 pcov のインストール
# 必要に応じて PHP_COVERAGE_DRIVER="pcov" を与える
ARG PHP_COVERAGE_DRIVER=""
RUN set -eux \
&& apk add --update --no-cache autoconf g++ libtool make \
&& if [ "$PHP_COVERAGE_DRIVER" = "pcov" ]; then \
pecl install pcov; \
docker-php-ext-enable pcov; \
echo "pcov.directory = /code/app" >> $PHP_INI_DIR/conf.d/docker-php-ext-pcov.ini; \
fi \
&& apk del autoconf g++ libtool make \
&& rm -rf /tmp/*
# Composer で利用する GitHub トークンの設定
# 必要に応じて GITHUB_TOKEN を与える
ARG GITHUB_TOKEN="****************************************"
RUN set -eux \
&& mkdir -p ~/.composer \
&& printf '{"github-oauth":{"github.com":"%s"}}' $GITHUB_TOKEN > ~/.composer/auth.json
WORKDIR /code
ENTRYPOINT ["php"]
CMD []
FROM PHP_RUNTIME as laravel_application_mounted
# アプリケーションコンテナのみで使用する起動スクリプトの設置
# 必要に応じて PHP_INIT_SCRIPT を与える
ARG PHP_INIT_SCRIPT="deploy"
COPY docker/php-fpm/scripts/$PHP_INIT_SCRIPT/*.sh /bin/
ENTRYPOINT []
CMD ["/bin/init.sh"]
FROM laravel_application_mounted as laravel_application_bundled
# Composer 依存パッケージ定義のコピー
COPY composer.* /code/
# Composer 依存パッケージをアプリケーションから分離して先にインストール(ビルド時間短縮のため)
RUN composer install --working-dir=/code --no-scripts --no-autoloader
# アプリケーションのコードをコピー(.dockerignore で vendor や .git は除外されている)
COPY . /code
# オートロードファイルの生成とストレージディレクトリのパーミッションの変更
RUN set -eux \
&& composer dump-autoload --working-dir=/code --no-scripts \
&& chmod -R a=rwX /code/storage
ポイント
-
Dockerfile
は1つだが,マルチステージビルドを使って以下のパターンを網羅している。何もARG
を与えずにビルドした場合はデプロイ向けになるようにしている。-
php_runtime
… PHP ランタイムのみ (ツール向け) laravel_application_mounted
… アプリケーションのソースコードをマウントして使用する (ローカル向け)laravel_application_bundled
… アプリケーションのソースコードをバンドルして使用する (デプロイ向け)
-
-
ビルド高速化のため,パッケージインストールは
composer.json
composer.lock
に変更があった場合にしか走らないように工夫している。-
composer.json
とcomposer.lock
をコンテナ内にコピー -
composer install --no-scripts --no-autoloader
で,スクリプト実行無しおよびオートローダ作成無しにして,vendor
ディレクトリへのファイル投入だけを目的として実行。 - アプリケーションのコードをマウント。この際
vendor
ディレクトリは除外されている。 -
composer dump-autoload
で後からオートローダを作成。
-
- パッケージインストール高速化のため, Composer の並列インストールプラグイン hirak/prestissimo を使用している。
- GitHub のプライベートリポジトリからパッケージをインストールできるように, 会社の共用アカウントで発行したこの目的専用の
GITHUB_TOKEN
をARG
のデフォルト値としてハードコーディング。 - エクステンションとして,さまざまな PHP アプリケーションで頻繁に必要になりそうな
opcache
bcmath
pdo_mysql
gd
exif
zip
あたりをカバー。必要に応じて追加と削除の余地あり。 - デバッガとして xdebug/xdebug を採用。
- カバレッジドライバとして krakjoe/pcov を採用。
-
OPCache のコントロールユーティリティ appstract/laravel-opcache および Golang 製の軽量 FastCGI リバースプロキシ alash3al/http2fcgi を採用。
- PHP 7.4 のプリロード対応が入った後は不要になるが,現状 Laravel ではまともに動かない。
- Golang バイナリのビルドのためにもマルチステージビルドを使用している。
-
composer install
の--no-dev
フラグは,require-dev
で入れたパッケージを継承してsrc
配下に配置しているとappstract/laravel-opcache
によるキャッシュ生成のタイミングで事故ってハイリスクなので,敢えて使っていない。開発用パッケージをバンドルしても並列インストールプラグインを入れていれば十分速いため,除外するメリットがあまりない。
起動スクリプト
/docker/php-fpm/scripts/local/init.sh
#!/bin/sh -eux
composer run-script env:init
{
while { http2fcgi --fcgi tcp://localhost:9000 \
--http localhost:8000 \
--root /code/public & }; \
sleep 1; \
! pgrep http2fcgi; do
echo >&2 'Waiting http2fcgi ready...'
done
} &
# -d でオプションを受け取れるようにする
exec php-fpm "$@"
/docker/php-fpm/scripts/ci/init.sh
#!/bin/sh -eux
composer run-script env:init
composer run-script post-autoload-dump
exec php-fpm
/docker/php-fpm/scripts/deploy/init.sh
#!/bin/sh -eux
composer run-script post-autoload-dump
php artisan config:cache
php artisan route:cache
{
while { http2fcgi --fcgi tcp://localhost:9000 \
--http localhost:8000 \
--root /code/public & }; \
sleep 1; \
! pgrep http2fcgi; do
echo >&2 'Waiting http2fcgi ready...'
done
php artisan opcache:compile
} &
exec php-fpm
/composer.json
{
"scripts": {
"post-install-cmd": [
"@composer env:init",
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
],
"env:init": "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
ポイント
- イメージの再利用性を高めるため, デプロイ環境でも「開発」「ステージング」「プロダクション」で分岐が発生しそうな処理に関しては,
CMD
実行時まで遅延させている。 - 3つの環境向けのスクリプトを網羅している。(下部のテーブルに詳細)
- ローカル環境
- CI 環境
- デプロイ環境
-
php-fpm
は接続準備完了状態になるまで少し遅延があるため,リバースプロキシhttp2fcgi
の起動を繰り返し試行させている。 -
CMD
実行時までファイルをコピーする操作が遅延されていてパーミッション周りが厄介であるため, 実行ユーザを変更せず敢えてroot
のままにしている。 PHP ファイルが動的に作成されうる WordPress などの CMS などを使用しない限り,コンテナ環境ではこれでも十分安全であると判断した。
機能 | ローカル | CI | デプロイ | (補足) |
---|---|---|---|---|
php-fpm 起動オプションの受け取り |
✔ | ローカル環境のみ, -d オプションで Xdebug を有効化できるようにする |
||
.env の生成 |
✔ | ✔ | ローカル環境では .env を自動生成する。CI 環境ではローカル用 .env と phpunit.xml の設定をマージして使用する想定のため,同じく自動生成する。デプロイ環境では Kubernetes ConfigMap から環境変数を実行時に注入するため,生成しない。 |
|
FastCGI リバースプロキシの起動 | ✔ | ✔ | CI 環境では CLI 実行のみで FastCGI のリクエストが飛んでこないため,実行しない。 ローカル環境では不具合調査のために Nginx を使用せずリクエストを送ることがあるため,実行する。 デプロイ環境では OPCache の能動的なキャッシュ生成のために必要なので実行する。 |
|
パッケージディスカバリなどの実行 | ✔ | ✔ | ローカル環境では composer install はビルド時には呼ばれないため,実行しない。CI 環境やデプロイ環境では実行する。 |
|
設定ファイルのキャッシュ生成 | ✔ | ローカル環境や CI 環境では開発中に設定項目を変更したり phpunit.xml からの環境変数取り込みがあるため,キャッシュしないデプロイ環境では都度新規でソースコードがチェックアウトされ,テストも不要なのでキャッシュする。 |
||
OPCache の能動的なキャッシュ生成 | ✔ | ローカル環境では開発中にソースを変更するため,実行しない。 CI 環境では FastCGI リバースプロキシが起動していないため,実行しない。 デプロイ環境では実行する。 |
docker-compose.yml
/docker-compose.yml
# ローカル用の基本形
version: '3.4'
services:
nginx:
image: nginx:latest
volumes:
- ./docker/nginx/conf.d/local.conf:/etc/nginx/conf.d/default.conf:ro
ports:
- "8000:80"
php-fpm:
build:
context: .
dockerfile: docker/php-fpm/Dockerfile
target: laravel_application_mounted
args:
PHP_DEBUGGER: xdebug
PHP_INIT_SCRIPT: local
volumes:
- ./docker/php-fpm/conf.d/local.ini:/usr/local/etc/php/conf.d/custom.ini:ro
- .:/code:cached
environment:
- PHP_IDE_CONFIG=serverName=<project>
/docker-compose.nfs.yml
# NFS を使用したい場合は docker-compose.override.yml にコピーして利用
# 他の拡張設定と共存する場合は以下のいずれかを採る
# - 拡張設定をマージして docker-compose.override.yml に書く
# - docker-compose -f docker-compose.yml -f docker-compose.nfs.yml のように複数 -f で並べる
version: '3.4'
services:
php-fpm:
volumes:
- code:/code
volumes:
code:
driver: local
driver_opts:
type: nfs
o: addr=host.docker.internal,actimeo=1,nolock
device: ":${PWD}"
/docker-compose.xdebug.yml
# xdebug を使用したい場合は docker-compose.override.yml にコピーして利用
# 他の拡張設定と共存する場合は以下のいずれかを採る
# - 拡張設定をマージして docker-compose.override.yml に書く
# - docker-compose -f docker-compose.yml -f docker-compose.xdebug.yml のように複数 -f で並べる
version: '3.4'
services:
php-fpm:
command:
- '/bin/init.sh'
- '-d'
- 'zend_extension=xdebug.so'
/docker-compose.ci.yml
# CI のテスト用に独立して利用
#
# - ボリュームマウントを無効化する方法が無いためオーバーライドは諦める
# - また Circle CI が target を使用できるバージョン 3.4 をサポートしていない
version: '3.2'
services:
php-fpm:
build:
context: .
dockerfile: docker/php-fpm/Dockerfile
args:
PHP_COVERAGE_DRIVER: pcov
PHP_INIT_SCRIPT: ci
/docker-compose.composer.yml
# ローカル環境における composer インストーラとして独立して利用
version: '3.4'
services:
composer:
build:
context: .
dockerfile: docker/php-fpm/Dockerfile
target: php_runtime
volumes:
- ./docker/php-fpm/conf.d/local.ini:/usr/local/etc/php/conf.d/custom.ini:ro
- .:/code:delegated
entrypoint: ['composer']
command: []
/d
#!/bin/sh
docker-compose exec php-fpm "$@"
/xdebug
#!/bin/sh
docker-compose exec php-fpm php -dzend_extension=xdebug.so "$@"
/composer
#!/bin/sh
docker-compose -f docker-compose.composer.yml run --rm composer "$@"
ポイント
- デプロイ環境では Kubernetes を使用しているため, Docker Compose の使用はローカル環境と CI 環境のみを対象としている。
- Docker for Mac でのパフォーマンスチューニングのために, NFS で設定をオーバーライドする選択肢を与えている。
-
xdebug
を有効化する選択肢を与えている。- SAPI が
php-fpm
である場合は, NFS 同様にオーバーライドして起動する。 - SAPI が
php-cli
である場合は,./d
の代わりに./xdebug
を使うことで有効化してコマンドを実行する。
- SAPI が
-
pcov
は CI 環境でのみ有効化する。 - ローカルでは
LARAVEL_APPLICATION_MOUNTED
をターゲットにしているため,composer install
をアプリケーションイメージビルド中に実行しない。その代わり,以下の選択肢のいずれかをホスト側で採る必要がある。- ホスト側に PHP 7.4 環境を構築し,
composer install --ignore-platform-reqs
を実行する - ホスト側で
./composer install
を実行する
- ホスト側に PHP 7.4 環境を構築し,
設定ファイルなど
デプロイ環境には Kubernetes ConfigMap を使用しているため,以下にはローカル環境用のものしか用意していません。 Nginx の設定ファイルは API サーバ向けに index.php
決め打ちになっております。
/docker/php-fpm/php-fpm.d/zzz-custom.conf
[www]
access.log = /dev/null
/docker/php-fpm/php-ini.d/custom.ini
; general
display_errors = 0
log_errors = 1
; xdebug
xdebug.idekey = PHPSTORM
xdebug.remote_autostart = 1
xdebug.remote_enable = 1
xdebug.remote_host = host.docker.internal
; opcache
opcache.enable = 0
/docker/nginx/conf.d/local.conf
server {
listen 80 default_server;
server_name _;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
add_header Cache-Control "no-store" always;
charset utf-8;
location /nginx_status {
stub_status;
access_log off;
}
error_page 400 404 405 =500 @40*_json;
location @40*_json {
default_type application/json;
return 500 '{"code":"GATEWAY_ERROR","message":"Gateway Error: Server unexpectedly tried to return 4xx error"}';
}
error_page 500 @500_json;
location @500_json {
default_type application/json;
return 500 '{"code":"GATEWAY_ERROR","message":"Gateway Error: 500 Internal Server Error"}';
}
error_page 502 @502_json;
location @502_json {
default_type application/json;
return 502 '{"code":"GATEWAY_ERROR","message":"Gateway Error: 502 Bad Gateway"}';
}
error_page 503 @503_json;
location @503_json {
default_type application/json;
return 503 '{"code":"GATEWAY_ERROR","message":"Gateway Error: 503 Service Temporarily Unavailable"}';
}
error_page 504 @504_json;
location @504_json {
default_type application/json;
return 504 '{"code":"GATEWAY_ERROR","message":"Gateway Error: 504 Gateway Timeout"}';
}
location / {
include fastcgi_params;
fastcgi_pass php-fpm:9000;
fastcgi_param SCRIPT_NAME index.php;
fastcgi_param SCRIPT_FILENAME /code/public/index.php;
fastcgi_buffering off;
}
}