Help us understand the problem. What is going on with this article?

商用環境でも使っている Laravel 用 php-fpm イメージの Dockerfile レシピ

これは何

Laravel 用 php-fpm イメージの Dockerfile。
(多少はフォーマット変わろうとも)色んなところでずっと使いまわししそうなのでメモ。

完全に個人の秘伝のタレ化するよりは情報公開したほうが自社にとっても利益があるだろうと判断(笑)
異論は無限に受け付けるので改善点などあればコメントください。

レシピ

Dockerfile

<project>/docker/php-fpm/Dockerfile
FROM golang:1.13 as HTTP2FCGI_BUILD

# http2fcgi のビルド
RUN apt update -y \
 && go get -v -ldflags '-w -s' github.com/alash3al/http2fcgi/...

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

# Git のインストール
RUN apk add --update --no-cache git

# Composer のインストール
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN set -eux \
 && composer global require hirak/prestissimo \
 && composer config -g repos.packagist composer https://packagist.jp

# 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" を与える
ARG PHP_DEBUGGER=""
RUN set -eux \
 && apk add --update --no-cache autoconf g++ libtool make \
 && if [ "$PHP_DEBUGGER" = "xdebug" ]; then \
      pecl install xdebug; \
      docker-php-ext-enable 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="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
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 に変更があった場合にしか走らないように工夫している。
    1. composer.jsoncomposer.lock をコンテナ内にコピー
    2. composer install --no-scripts --no-autoloader で,スクリプト実行無しおよびオートローダ作成無しにして, vendor ディレクトリへのファイル投入だけを目的として実行。
    3. アプリケーションのコードをマウント。この際 vendor ディレクトリは除外されている。
    4. composer dump-autoload で後からオートローダを作成。
  • パッケージインストール高速化のため, Composer の並列インストールプラグイン hirak/prestissimo および日本国内ミラー Packagist.JP を使用している。
    • 但し, Circle CI などでのビルドを考慮する場合はミラーではなく本家を参照したほうがいいかもしれない。ARG で分岐の余地あり。
  • GitHub のプライベートリポジトリからパッケージをインストールできるように, 会社の共用アカウントで発行したこの目的専用の GITHUB_TOKENARG のデフォルト値としてハードコーディング。
  • エクステンションとして,さまざまな 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 によるキャッシュ生成のタイミングで事故ってハイリスクなので,敢えて使っていない。開発用パッケージをバンドルしても並列インストールプラグインを入れていれば十分速いため,除外するメリットがあまりない。

起動スクリプト

<project>/docker/php-fpm/scripts/local/init.sh
#!/bin/sh -eux

composer run-script env:init

sleep 5 && http2fcgi --fcgi tcp://localhost:9000 \
                     --http localhost:8000 \
                     --root /code/public &

exec php-fpm
<project>/docker/php-fpm/scripts/ci/init.sh
#!/bin/sh -eux

composer run-script env:init
composer run-script post-autoload-dump

php artisan config:cache
php artisan route:cache

exec php-fpm
<project>/docker/php-fpm/scripts/deploy/init.sh
#!/bin/sh -eux

composer run-script post-autoload-dump

php artisan config:cache
php artisan route:cache

sleep 5 && http2fcgi --fcgi tcp://localhost:9000 \
                     --http localhost:8000 \
                     --root /code/public &

sleep 10 && php artisan opcache:compile &

exec php-fpm
<project>/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 の起動を少し遅延させている。
  • http2fcgi は接続準備完了状態になるまで少し遅延があるため, php artisan opcache:compile の実行を少し遅延させている。
  • CMD 実行時までファイルをコピーする操作が遅延されていてパーミッション周りが厄介であるため, 実行ユーザを変更せず敢えて root のままにしている。 PHP ファイルが動的に作成されうる WordPress などの CMS などを使用しない限り,コンテナ環境ではこれでも十分安全であると判断した。
機能 ローカル CI デプロイ (補足)
.env の生成 ローカル環境では .env を自動生成する。

CI 環境ではローカル用 .envphpunit.xml の設定をマージして使用する想定のため,同じく自動生成する。

デプロイ環境では Kubernetes ConfigMap から環境変数を実行時に注入するため,生成しない。
パッケージディスカバリなどの実行 ローカル環境では composer install はビルド時には呼ばれないため,実行しない。

CI 環境やデプロイ環境では実行する。
設定ファイルのキャッシュ生成 ローカル環境では開発中に設定項目を変更することがよくあるため,実行しない。

CI 環境やデプロイ環境では都度新規でソースコードがチェックアウトされるので,実行する。
FastCGI リバースプロキシの起動 CI 環境では CLI 実行のみで FastCGI のリクエストが飛んでこないため,実行しない。

ローカル環境では不具合調査のために Nginx を使用せずリクエストを送ることがあるため,実行する。

デプロイ環境では OPCache の能動的なキャッシュ生成のために必要なので実行する。
OPCache の能動的なキャッシュ生成 ローカル環境では開発中にソースを変更するため,実行しない。

CI 環境では FastCGI リバースプロキシが起動していないため,実行しない。

デプロイ環境では実行する。

docker-compose.yml

<project>/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>
<project>/docker-compose.nfs.yml
# NFS を使用したい場合は docker-compose.override.yml にコピーして利用

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}"
<project>/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
<project>/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: []
<project>/d
#!/bin/sh

docker-compose exec php-fpm "$@"
<project>/composer
#!/bin/sh

docker-compose -f docker-compose.composer.yml run --rm composer "$@"

ポイント

  • デプロイ環境では Kubernetes を使用しているため, Docker Compose の使用はローカル環境と CI 環境のみを対象としている。
  • Docker for Mac でのパフォーマンスチューニングのために, NFS で設定をオーバーライドする選択肢を与えている。
  • xdebug はローカル環境でのみ有効化する。
  • pcov は CI 環境でのみ有効化する。
  • ローカルでは LARAVEL_APPLICATION_MOUNTED をターゲットにしているため, composer install をアプリケーションイメージビルド中に実行しない。その代わり,以下の選択肢のいずれかをホスト側で採る必要がある。
    • ホスト側に PHP 7.4 環境を構築し, composer install --ignore-platform-reqs を実行する
    • ホスト側で ./composer install を実行する

設定ファイルなど

デプロイ環境には Kubernetes ConfigMap を使用しているため,以下にはローカル環境用のものしか用意していません。 Nginx の設定ファイルは API サーバ向けに index.php 決め打ちになっております。

<project>/docker/php-fpm/conf.d/local.ini
; xdebug
xdebug.idekey = PHPSTORM
xdebug.remote_autostart = 1
xdebug.remote_enable = 1
xdebug.remote_host = host.docker.internal

; opcache
opcache.enable=0
<project>/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";

    charset utf-8;

    location /nginx_status {
        stub_status;
        access_log  off;
    }

    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;
    }
}
mpyw
PHP(Laravel) / JavaScript(React/Redux/ReactNative/Vue) / MySQL あたりが得意分野なWeb系エンジニア。最近マンネリ化がひどいので Go / Kotlin / Rust / Swift あたりから何か掘り下げたいと思っている。Go は 2.x 出てから書きます。古い記事はそのまま参考にしないようにご注意ください
http://gravatar.com/mpyw
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした