AlpineLinux上のLaravelアプリケーションのphp5.6 -> 7.0の環境アップグレードでコケた話

現在私が携わっているプロジェクトでは、DockerコンテナとしてAlpineLinuxを用いて、PHP5.6 + Laravel5.3でアプリケーションの開発・運用をしています。

先日、PHP5.6のアクティブアップデート期間が終了しました

これに伴って本アプリケーションもPHP7に移行してみようとしたのですが、すごく微妙な落とし穴にハマってしまったので、Tipsとして書き残したいと思います。

AlpineLinuxにおけるPHP

AlpineLinuxを用いたPHPアプリケーション環境の構築には、まずイメージの選択肢があります。自分がわかる範囲では以下の2つです。

前者の場合はベースイメージにalpine以外にも選択肢があります。こちらはDockerfile上で指定されたバージョンのPHPのソースをダウンロードしてビルドしています。

後者はプレーンなOSイメージですが、公式のレポジトリでビルド済みのPHPが配布されているのでこれを利用します。もちろん自前でビルドすることもできます。

私の携わっているプロジェクトは後者を用いていますが、さいことPHPに特化するのであれば、PHPが公式にメンテナンスしている前者が良いかと思います。

Alpine公式レポジトリのPHPについて

現在、AlpineLinuxの公式レポジトリでは、PHP5.6(php)とPHP7.0(php7)がそれぞれ別パッケージとして配布されています。以前はこのPHP5.6と、各種拡張機能(php-openssl, php-pdo_mysql, etc.)をレポジトリからインストールして運用していました。以下は従来のDockerfileの一部です。apkはalpineLinuxにおけるパッケージマネージャのコマンドで、ubuntuでいうapt-get、centOSでいうyumのようなものです。

FROM alpine:3.3

RUN apk --no-cache add apache2 php-apache2 \
        php php-fpm php-json php-zlib php-xml php-pdo php-phar \
        php-openssl php-curl php-pdo_mysql php-mysql php-mysqli \
        php-gd php-ctype \
    && mkdir -p /run/apache2/ \
    && rm -rf /var/www/localhost/htdocs/index.html \
    && ln -sf /dev/stdout /var/log/apache2/access.log \
    && ln -sf /dev/stderr /var/log/apache2/error.log

各種拡張モジュールはphp-*という形式で提供されています。どうやらPHP7も同じようにphp7-*という形式であるようなので、Dockerfileを以下のように修正してみる!

FROM alpine:3.5

RUN apk --no-cache add apache2 php7-apache2 \
        php7 php7-fpm php7-json php7-zlib php7-xml php7-pdo php7-phar \
        php7-openssl php7-curl php7-pdo_mysql php7-mysqli \
        php7-gd php7-ctype \
    && mkdir -p /run/apache2/ \
    && rm -rf /var/www/localhost/htdocs/index.html \
    && ln -sf /dev/stdout /var/log/apache2/access.log \
    && ln -sf /dev/stderr /var/log/apache2/error.log \
    && ln -sf /usr/bin/php7 /usr/bin/php

…HTTP Error Code 500。。。(・・?

ひっそりと隠れていたmbstring

docker logsを通してapacheに出力されたエラーを見てみると、Mbstringの関数が未定義だということを言われている。そういえばmbstringはPHPの拡張モジュールでした。 Laravelの必要要件にもmbstringがあります。これが足りないのか…?

[Tue Mar 07 11:31:27.898014 2017] [:error] [pid 7] [client 172.17.0.1:51690]
PHP Fatal error:  Uncaught Error: Call to undefined function Symfony\\Polyfill\\Mbstring\\iconv_strpos() 
in /var/www/localhost/htdocs/vendor/symfony/polyfill-mbstring/Mbstring.php:358\nStack 
trace:
\n#0 /var/www/localhost/htdocs/vendor/symfony/polyfill-mbstring/bootstrap.php(32): Symfony\\Polyfill\\Mbstring\\Mbstring::mb_strpos(NULL, '/json', 0, 'UTF-8')
\n#1 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Support/Str.php(72): mb_strpos(NULL, '/json')
\n#2 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Http/Concerns/InteractsWithContentTypes.php(34): Illuminate\\Support\\Str::contains(NULL, Array)
\n#3 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Http/Request.php(312): Illuminate\\Http\\Request->isJson()
\n#4 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Http/Request.php(340): Illuminate\\Http\\Request->getInputSource()
\n#5 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Http/Request.php(59): Illuminate\\Http\\Request::createFromBase(Object(Illuminate\\Http\\R in /var/www/localhost/htdocs/vendor/symfony/polyfill-mbstring/Mbstring.php on line 358
172.17.0.1 - - [07/Mar/2017:11:31:27 +0000] "GET / HTTP/1.1" 500 - "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.90 Safari/537.36"
172.17.0.1 - - [07/Mar/2017:11:31:47 +0000] "-" 408 - "-" "-"

そこで、公式レポジトリを調べてみたところ、

  • PHP5.6(php)では、mbstringがメインパッケージにバンドルされていた

  • PHP7(php7)では、別パッケージとして配布されている

ことが判明。なるほど、動かないわけだ…

さらなる追い打ち、sessionモジュール

そこでphp7-mbstringを入れて再度イメージをビルドしてコンテナ作成、アクセスしてみると、今度は画面が真っ白に…やはり内部エラー(500)が返ってきていました。

もう一回エラーログを見ると、今度はSessionHandlerInterfaceがないと言われています。これはPHPの拡張モジュールで定義されているインターフェースのようですね。

PHP: SessionHandlerInterface - Manual

[Thu Mar 09 10:46:27.561882 2017] [:error] [pid 7] [client 172.17.0.1:49232] PHP Fatal error:  Interface 'SessionHandlerInterface' not found in /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Session/CookieSessionHandler.php on line 10
[Thu Mar 09 10:46:27.585695 2017] [:error] [pid 7] [client 172.17.0.1:49232] PHP Fatal error:  Uncaught TypeError: Argument 2 passed to Illuminate\\Session\\Store::__construct() must be an instance of SessionHandlerInterface, instance of Illuminate\\Session\\CookieSessionHandler given, called in /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Session/SessionManager.php on line 169 and defined in /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Session/Store.php:56\nStack 
trace:
\n#0 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Session/SessionManager.php(169): Illuminate\\Session\\Store->__construct('laravel_session', Object(Illuminate\\Session\\CookieSessionHandler))
\n#1 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Session/SessionManager.php(39): Illuminate\\Session\\SessionManager->buildSession(Object(Illuminate\\Session\\CookieSessionHandler))
\n#2 /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Support/Manager.php(87): Illuminate\\Session\\SessionManager->createCookieDriver()
\n#3 /var/www/localhost/htdocs/vendor/laravel/framew in /var/www/localhost/htdocs/vendor/laravel/framework/src/Illuminate/Session/Store.php on line 56
172.17.0.1 - - [09/Mar/2017:10:46:26 +0000] "GET / HTTP/1.1" 500 1 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.90 Safari/537.36"

このパッケージもやはりmbstringと同様、5.6まではメインパッケージで既に有効になっていたものが、別パッケージになっていたようです。

mbstringはまだしも、sessionはPHP使ってれば絶対使いそうな気がするので分けなくても良いような気がしますが…その辺りのメンテナの意図が気になるところです。

まとめ

上記を踏まえて、php7-mbstringとphp7-sessionパッケージを追加してビルドしたところ、ようやくうまくいきました。

500エラーはクライアントサイドの情報量が少ないので、サーバーサイドのログを探らなくてはいけないのでちょっと大変ですね。

FROM alpine:3.5

RUN apk --no-cache add apache2 php7-apache2 \
        php7 php7-fpm php7-json php7-zlib php7-xml php7-pdo php7-phar \
        .
        .
        .
        php7-mbstring php7-session \
    && mkdir -p /run/apache2/ \
    .
    .
    .