Edited at

PHPの公式Dockerfileを読み解く

「Dockerfileをスラスラ書けるようになるにはどうすればいいか?」

自分の答えは、公式のDockerfileを読み解くことだと思う。公式のリポジトリには、DockerやLinuxに精通した人達の知恵が詰まっており、知らなかったことを多く知れる。

ということで今回は、php:7.3-apache-stretch という「Apache上でPHP7.3を動かす環境」が入ったDockerイメージのDockerfileを読解していく。

GitHubはここにある。php/7.3/stretch/apache · docker-library/php · GitHub


ベースイメージの指定(debian:stretch-slim)

FROM debian:stretch-slim

Dockerfileは全てFROMから始まり、ベースのイメージを設定する。ここではベースイメージにDebian v9(stretch)を使用している。

Linuxディストリビューションのトレンドによると、UbuntuとDebianが大きくシェアを占めている。UbuntuはDebianを元にしており、Debianとほぼ同じように扱える。debianの方が軽量でサーバー向けという位置付けになる。

stretchについては、Debianの各メジャーバージョンに付けられるコードネームのことであり、現在の最新安定バージョンである。他にも前バージョンのjessieや次バージョンのbusterがあり、他のバージョンも「DebianReleases - Debian Wiki」で確認できる。


パッケージの制御(/etc/apt/preferences.d)

# prevent Debian's PHP packages from being installed

# https://github.com/docker-library/php/pull/542
RUN set -eux; \
{ \
echo 'Package: php*'; \
echo 'Pin: release *'; \
echo 'Pin-Priority: -1'; \
} > /etc/apt/preferences.d/no-debian-php

Debianではパッケージ管理システムにAPT (Advanced Packaging Tool)を使い、APTのコマンドとしてapt-getが使われる。

しかし中にはソースからビルドしたものを使い、APTからはインストールして欲しくない時がある。その際に/etc/apt/preferences.dで、特定のパッケージのインストールを制御することができる。今回だとパッケージ名がphpで始まるパッケージは絶対にインストールしないという設定になっている。より詳しい設定はDebian(Ubuntu)で apt-get upgrade で自動更新したくない場合の対応が参考になる。

また最初にsetコマンドによってシェルに関する設定を行っている。-eオプションによって実行したコマンドが1つでもエラーになれば直ちに終了するようにし、-uオプションで未定義の変数などを使おうとすればエラーにするようにしている。-xオプションでは実行コマンドとその引数をトレースとして出力するようにしている。

このsetコマンドはDockerfileで頻出するコマンドであり、今回のDockerfileでも頻繁に使われている。

またLinuxコマンドにおける;&&の意味がわからなければ、Linuxコマンドを連続して使うには - Qiitaが参考になる。


phpizeに必要なパッケージ($PHPIZE_DEPS)

# dependencies required for running "phpize"

# (see persistent deps below)
ENV PHPIZE_DEPS \
autoconf \
dpkg-dev \
file \
g++ \
gcc \
libc-dev \
make \
pkg-config \
re2c

ここではPHPの拡張モジュールのビルドツールであるphpizeで必要なパッケージリストを環境変数に設定している。この環境変数は後ほどapt-getで使われる。

それぞれのパッケージについての説明を以下に記載する。


  • autoconf(configureというパッケージインストールスクリプトを作成するためのパッケージ)

  • dpkg-dev(APTより低水準なdebianのパッケージ管理システム)

  • file(ファイルの形式などを調べるfileコマンド)

  • g++(C++ コンパイラ)

  • gcc(C コンパイラ)

  • libc-dev(C言語 標準ライブラリ

  • make(Makefileというファイルを基にコンパイルを行うツール)

  • pkg-config(コンパイルする際に必要なライブラリの情報を取得するツール)

  • re2c(CとC++のための字句解析ツール)


パッケージのインストール(apt-get install)

# persistent / runtime deps

RUN apt-get update && apt-get install -y \
$PHPIZE_DEPS \
ca-certificates \
curl \
xz-utils \
--no-install-recommends && rm -r /var/lib/apt/lists/*

ここでは先ほどの$PHPIZE_DEPSとその他のパッケージをインストールしている。

apt-get updateはインストール可能なパッケージリストを更新するコマンドである。実際にパッケージのアップデートを行いたい場合はapt-get upgradeを用いる。

updateは、/etc/apt/sources.listに書かれているURLからインストール可能なパッケージを/var/lib/apt/listsに保存する。


/etc/apt/sources.list

deb http://deb.debian.org/debian stretch main

deb http://security.debian.org/debian-security stretch/updates main
deb http://deb.debian.org/debian stretch-updates main

apt-get install -yは列挙するパッケージをインストールするコマンドである。-yオプションが無い場合はインストール前に「インストールしていいですか」という類の確認が出るが、Dockerfileの場合はこういったインタラクティブなコマンド処理は行えないため、-yオプションによってスキップしている。

--no-install-recommendsオプションは、おすすめの関連パッケージをインストールさせないためのオプションである。余計なパッケージを入れられるとDockerのイメージサイズが大きくなるため、基本的にインストールしない方が良い。

インストール後は、rm -r /var/lib/apt/lists/*を行い、apt-get updateで取得したパッケージのソースを削除する。これらのソースはインストール後に使うことは無いため、イメージサイズの軽量化のために削除する。

今回登場したパッケージの説明も記載しておく。


  • ca-certificates(認証局(CA:Certification Authority)の証明書などを含んだパッケージ)

  • curl(httpを始めとした様々なプロトコルで通信を行うためのツール)

  • xz-utils(xzという圧縮フォーマットのファイルを作成・展開するためのツール)


PHP_INI_DIR

ENV PHP_INI_DIR /usr/local/etc/php

RUN set -eux; \
mkdir -p "$PHP_INI_DIR/conf.d"; \

PHPでは.iniファイルを設定ファイルとして扱う。これらの保存ディレクトリを作成する。$PHP_INI_DIRは、また後ほどPHPのビルド時に使用する。


Apache root directory

# allow running as an arbitrary user (https://github.com/docker-library/php/issues/743)

[ ! -d /var/www/html ]; \
mkdir -p /var/www/html; \
chown www-data:www-data /var/www/html; \
chmod 777 /var/www/html

ここではApacheをwww-dataユーザーから実行できるようにするための処理を行う。www-dataはApacheを実行する際のデフォルトの実行ユーザーである。

[ ! -d /var/www/html ]は、もし/var/www/htmlがディレクトリではなかったらという意味であり、真であれば続くコマンドが実行される。

chownによって/var/www/htmlの所有者をwww-dataにし、chmodによってどのユーザーでも/var/www/htmlを読み書きや実行してもいい、という権限にしている。


Apache2のインストール

ENV APACHE_CONFDIR /etc/apache2

ENV APACHE_ENVVARS $APACHE_CONFDIR/envvars

Apacheの設定ファイルのディレクトリなどを環境変数に設定する。これは後ほど利用する。

RUN set -eux; \

apt-get update; \
apt-get install -y --no-install-recommends apache2; \
rm -rf /var/lib/apt/lists/*; \

特に目新しいことはしておらず、apache2をapt-getによってインストールしているだけである。

sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \

ここのsedによる処理は正直よくわからない。

@taterenさんよりコメントを頂きました。

ここのsedの処理は、APACHE_RUN_USERが指定されていればwww-dataではなく、そちらを優先する処理のもようです。

    . "$APACHE_ENVVARS"; \

for dir in \
"$APACHE_LOCK_DIR" \
"$APACHE_RUN_DIR" \
"$APACHE_LOG_DIR" \
; do \
rm -rvf "$dir"; \
mkdir -p "$dir"; \
chown "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$dir"; \
# allow running as an arbitrary user (https://github.com/docker-library/php/issues/743)
chmod 777 "$dir"; \
done; \

Apacheで利用する各ディレクトリを再作成し、適切なパーミッションに設定している。

# delete the "index.html" that installing Apache drops in here

rm -rvf /var/www/html/*; \

Apacheインストール時に作成されるindex.htmlを削除している。

# logs should go to stdout / stderr

ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"

Apacheのログを標準出力・標準エラー出力に出力するように設定している。

lnコマンドによってリンクを作成することができる。-sオプションでハードリンクではなくシンボリックリンクにし、-fオプションで同じファイルがあった場合でも強制的に上書きし、最後に-Tオプションでリンク先をディレクトリではなく通常ファイルとして扱うようにしている。

chownの-Rオプションで再帰的に、そして--no-dereferenceオプションでシンボリックリンク自体の所有者を変更するようにしている。

# Apache + PHP requires preforking Apache for best results

RUN a2dismod mpm_event && a2enmod mpm_prefork

Apacheではa2dismodコマンドでモジュールを無効化したり、a2enmodコマンドで有効化したりできる。

そして今回の2つのモジュールは、ApacheのMPM (Multi Processing Module)というApacheの並行処理方法に関するモジュールのことである。

この辺りはマルチプロセスやマルチスレッドなどの知識が必要となる。その辺りはApache MPMとはなんぞやという話 - 備忘録の裏のチラシが参考になる。PHPはスレッドセーフな言語ではないため、マルチプロセスで動かさなければならない。そのためmpm_preforkをモジュールを利用して、リクエストを処理するApacheのプロセスをあらかじめforkするようなマルチプロセス処理形態にする。

# PHP files should be handled by PHP, and should be preferred over any other file type

RUN { \
echo '<FilesMatch \.php$>'; \
echo '\tSetHandler application/x-httpd-php'; \
echo '</FilesMatch>'; \
echo; \
echo 'DirectoryIndex disabled'; \
echo 'DirectoryIndex index.php index.html'; \
echo; \
echo '<Directory /var/www/>'; \
echo '\tOptions -Indexes'; \
echo '\tAllowOverride All'; \
echo '</Directory>'; \
} | tee "$APACHE_CONFDIR/conf-available/docker-php.conf" \
&& a2enconf docker-php

ここではApacheが.phpファイルを処理するための設定ファイルを作成している。

FilesMatchディレクティブで、リクエストファイルがphpファイルであればapplication/x-httpd-phpというハンドラで処理するようにしている。この辺りはApacheの話になってしまうので詳しい説明はしない。

また公式では設定ファイルを愚直にechoしているが、別ファイルに切り出してDockerのCOPYコマンドを使う方が簡潔になることもある。

ENV PHP_EXTRA_BUILD_DEPS apache2-dev

ENV PHP_EXTRA_CONFIGURE_ARGS --with-apxs2 --disable-cgi

後ほどPHPをビルドする際に利用する環境変数を設定している。


PHPのインストール


PHPのソースをダウンロード

# Apply stack smash protection to functions using local buffers and alloca()

# Make PHP's main executable position-independent (improves ASLR security mechanism, and has no performance impact on x86_64)
# Enable optimization (-O2)
# Enable linker optimization (this sorts the hash buckets to improve cache locality, and is non-default)
# Adds GNU HASH segments to generated executables (this is used if present, and is much faster than sysv hash; in this configuration, sysv hash is also generated)
# https://github.com/docker-library/php/issues/272
ENV PHP_CFLAGS="-fstack-protector-strong -fpic -fpie -O2"
ENV PHP_CPPFLAGS="$PHP_CFLAGS"
ENV PHP_LDFLAGS="-Wl,-O1 -Wl,--hash-style=both -pie"

コンパイルで用いられる最適化のオプション。正直よくわからないよな?俺もそう。

ENV GPG_KEYS CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D

ダウンロードしたPHPのソースが改ざんされていないかをチェックするには、gpg (GNU Privacy Guard)と呼ばれる暗号化ソフトウェアが使われる。この$GPG_KEYSは後ほどこのgpgによって使うフィンガープリント(ハッシュ関数で算出したハッシュ値)である。

ENV PHP_VERSION 7.3.1

ENV PHP_URL="https://secure.php.net/get/php-7.3.1.tar.xz/from/this/mirror" PHP_ASC_URL="https://secure.php.net/get/php-7.3.1.tar.xz.asc/from/this/mirror"
ENV PHP_SHA256="cfe93e40be0350cd53c4a579f52fe5d8faf9c6db047f650a4566a2276bf33362" PHP_MD5=""

PHPのバージョンと、PHPのダウンロードURL、そしてソースのハッシュ値を定義している。

これはPHP: Downloadsに記載されており、旧バージョンについてもPHP: Releasesから探すことができる。

RUN set -xe; \

\
fetchDeps=' \
wget \
'; \
if ! command -v gpg > /dev/null; then \
fetchDeps="$fetchDeps \
dirmngr \
gnupg \
"; \
fi; \
apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*; \

fetchDepsという変数にapt-get installするものを格納している。wgetはファイルをダウンロードする際に用いるツールである。

gpgコマンドが存在しなければfetchDepsにgnupgとdirmngr(証明書の管理ツール)を追加している。

    mkdir -p /usr/src; \

cd /usr/src; \
\
wget -O php.tar.xz "$PHP_URL"; \
\
if [ -n "$PHP_SHA256" ]; then \
echo "$PHP_SHA256 *php.tar.xz" | sha256sum -c -; \
fi; \
if [ -n "$PHP_MD5" ]; then \
echo "$PHP_MD5 *php.tar.xz" | md5sum -c -; \
fi; \

/usr/srcディレクトリを作成し移動している。このディレクトリは慣用的にソースを置く場所となっている。

次にwgetコマンドで$PHP_URLからPHPのソースをphp.tar.xzとして保存している。

$PHP_SHA256が設定されていれば、sha256sum -cによってソースのハッシュ値と比較する。もし違っていれば改ざんされている可能性があるため、エラーが返ってくる。これは$PHP_MD5でも同じである。

    if [ -n "$PHP_ASC_URL" ]; then \

wget -O php.tar.xz.asc "$PHP_ASC_URL"; \
export GNUPGHOME="$(mktemp -d)"; \
for key in $GPG_KEYS; do \
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \
done; \
gpg --batch --verify php.tar.xz.asc php.tar.xz; \
command -v gpgconf > /dev/null && gpgconf --kill all; \
rm -rf "$GNUPGHOME"; \
fi; \

こちらはgpg、すなわち電子署名を用いてソースが改ざんされていないかを確認する。基本的にハッシュ値の比較だけでもセキュリティ的に十分だが、PHPの公式であるPHP: Downloadsまで改ざんされているケースを想定し、さらに厳重に電子署名でチェックしている。gpgの一連の認証処理についてはここでは省略するため、詳しくは別途調べてほしい。

apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $fetchDeps

先ほどインストールした$fetchDepsはもう使わないため、apt-get purgeによりアンインストールしている。APT::AutoRemove::RecommendsImportantオプションをfalseにすることで、$fetchDepsの依存関係にあるパッケージを削除している。


PHPのビルド

COPY docker-php-source /usr/local/bin/

docker-php-sourceというシェルスクリプトをコピーしている。このシェルスクリプトはPHPのソースを解凍したり削除したりする処理をまとめている。

RUN set -eux; \

\
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends \
libcurl4-openssl-dev \
libedit-dev \
libsodium-dev \
libsqlite3-dev \
libssl-dev \
libxml2-dev \
zlib1g-dev \
${PHP_EXTRA_BUILD_DEPS:-} \
; \

apt-mark showmanualはAPTによって手動でインストールしたパッケージリストを取得するコマンドである。これをsavedAptMarkとして保持しておき、これからインストールするビルドにしか使わないパッケージ群だけを削除する際に用いる。

今回apt-get installするパッケージの簡単な説明を記載する。


  • libcurl4-openssl-dev(SSL/TLS通信に必要なパッケージ)

  • libedit-dev(改行処理や履歴に関するパッケージ?)

  • libsodium-dev(暗号化やハッシュ計算などを提供するパッケージ)

  • libsqlite3-dev(SQLiteという組み込み型DBのパッケージ)

  • libssl-dev(SSL/TLSの暗号化プロトコルに必要なパッケージ)

  • libxml2-dev(XMLを使うためのパッケージ)

  • zlib1g-dev(deflateと呼ばれる圧縮法を実装したライブラリ)

$PHP_EXTRA_BUILD_DEPSにはApacheのインストール時にapache2-devを設定している。apache2-devはApache上でPHPを動かすために用いられる。

sed -e 's/stretch/buster/g' /etc/apt/sources.list > /etc/apt/sources.list.d/buster.list;

sedは置換処理を行うコマンドであり、's/stretch/buster/g'と書くことで、stretchという文字列を全てbusterに置換することができる。そして置換後のテキストを/etc/apt/sources.list.d/buster.listにファイルとして保存することで、新たにパッケージのインストールを制御している。

busterとはstretchの次バージョンであり、2019年2月現在ではtesting状態となっている。stretchに存在しない新しいパッケージをインストールしたい場合、このようにパッケージのダウンロード先URLを追加する。

    { \

echo 'Package: *'; \
echo 'Pin: release n=buster'; \
echo 'Pin-Priority: -10'; \
echo; \
echo 'Package: libargon2*'; \
echo 'Pin: release n=buster'; \
echo 'Pin-Priority: 990'; \
} > /etc/apt/preferences.d/argon2-buster; \
apt-get update; \
apt-get install -y --no-install-recommends libargon2-dev; \

/etc/apt/preferences.dの設定は冒頭でも説明したので省略する。中身を要約すると、コードネームがbusterのパッケージはlibargon2*以外インストールしないという意味である。argon2はパスワードのハッシュ関数であり、PHP7.2から導入されている。

    rm -rf /var/lib/apt/lists/*; \

\
export \
CFLAGS="$PHP_CFLAGS" \
CPPFLAGS="$PHP_CPPFLAGS" \
LDFLAGS="$PHP_LDFLAGS" \
; \
docker-php-source extract; \
cd /usr/src/php; \
gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \
debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \
# https://bugs.php.net/bug.php?id=74125
if [ ! -d /usr/include/curl ]; then \
ln -sT "/usr/include/$debMultiarch/curl" /usr/local/include/curl; \
fi; \

ビルド時のオプションを環境変数や変数に設定したり、PHPのソースを解凍したりしている。

    ./configure \

--build="$gnuArch" \
--with-config-file-path="$PHP_INI_DIR" \
--with-config-file-scan-dir="$PHP_INI_DIR/conf.d" \
\
# make sure invalid --configure-flags are fatal errors intead of just warnings
--enable-option-checking=fatal \
\
# https://github.com/docker-library/php/issues/439
--with-mhash \
\
# --enable-ftp is included here because ftp_ssl_connect() needs ftp to be compiled statically (see https://github.com/docker-library/php/issues/236)
--enable-ftp \
# --enable-mbstring is included here because otherwise there's no way to get pecl to use it properly (see https://github.com/docker-library/php/issues/195)
--enable-mbstring \
# --enable-mysqlnd is included here because it's harder to compile after the fact than extensions are (since it's a plugin for several extensions, not an extension in itself)
--enable-mysqlnd \
# https://wiki.php.net/rfc/argon2_password_hash (7.2+)
--with-password-argon2 \
# https://wiki.php.net/rfc/libsodium
--with-sodium=shared \
\
--with-curl \
--with-libedit \
--with-openssl \
--with-zlib \
\
# bundled pcre does not support JIT on s390x
# https://manpages.debian.org/stretch/libpcre3-dev/pcrejit.3.en.html#AVAILABILITY_OF_JIT_SUPPORT
$(test "$gnuArch" = 's390x-linux-gnu' && echo '--without-pcre-jit') \
--with-libdir="lib/$debMultiarch" \
\
${PHP_EXTRA_CONFIGURE_ARGS:-} \
; \

長ったらしいが要はconfigureというシェルスクリプトを実行しているだけである。このconfigureスクリプトはPHPのソースに含まれており、インストールに必要なライブラリのチェックとMakefileの生成を行う。Makefileファイルはmakeというコンパイルを行うコマンドで用いる。

configureスクリプトの実行時に様々なオプションを指定している。--with-config-file-pathでPHPの設定ファイルディレクトリを指定したり、--with-opensslでPHPがOpenSSLをサポートできるようにしたりしている。

    make -j "$(nproc)"; \

make install; \
find /usr/local/bin /usr/local/sbin -type f -executable -exec strip --strip-all '{}' + || true; \
make clean; \

makeでPHPのコンパイルを行う。-jオプションでコンパイルを実行するジョブ数を指定することができ、nprocコマンドで取得できるCPU数をそのまま渡している。make installmakeでビルドしたバイナリなどを規定のディレクトリに移動させるコマンドである。

findコマンドはファイルやディレクトリを検索するコマンドであり、今回は実行可能なバイナリファイルを列挙して、それをstripコマンドの引数として渡している。strip --strip-allコマンドは渡されたファイルのシンボルテーブル(デバッグ用に使われるデータ)を全て削除し、実行ファイルのサイズを軽量化している。

make cleanコマンドは、コンパイル時に生成したもう使わないファイルなどを削除する。

# https://github.com/docker-library/php/issues/692 (copy default example "php.ini" files somewhere easily discoverable)

cp -v php.ini-* "$PHP_INI_DIR/"; \
\
cd /; \
docker-php-source delete; \

PHPのソースにはphp.ini-developmentphp.ini-productionといった初期iniファイルがあるので、それをコピーしている。あとはdocker-php-source deleteでPHPのソースを削除する。

# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies

apt-mark auto '.*' > /dev/null; \
[ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; \
find /usr/local -type f -executable -exec ldd '{}' ';' \
| awk '/=>/ { print $(NF-1) }' \
| sort -u \
| xargs -r dpkg-query --search \
| cut -d: -f1 \
| sort -u \
| xargs -r apt-mark manual \
; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \

ビルド時のみ必要だったパッケージをapt-get purgeで削除している。これもイメージ軽量化の技の一つ。

    php --version; \

\
# https://github.com/docker-library/php/issues/443
pecl update-channels; \
rm -rf /tmp/pear ~/.pearrc

phpのバージョンを表示し、PHPのインストールが正常にできているかを確認している。またpecl update-channelsでpeclのリポジトリを更新している。

COPY docker-php-ext-* docker-php-entrypoint /usr/local/bin/

docker-php-ext-*はPHPの拡張機能をインストールするためのスクリプトである。これは公式リポジトリ内で用意されている。自分たちが使うのは基本的にdocker-php-ext-installというスクリプトである。これはDockerfile内でRUN docker-php-ext-install curlのように使えば、PHPのcurl拡張機能がインストールされる。

# sodium was built as a shared module (so that it can be replaced later if so desired), so let's enable it too (https://github.com/docker-library/php/issues/598)

RUN docker-php-ext-enable sodium

sodiumというPHPにおける暗号ライブラリを有効化している。PHP7.2では標準で組み込まれているが、有効化するためにenableスクリプトを実行している。

ENTRYPOINT ["docker-php-entrypoint"]

##<autogenerated>##
COPY apache2-foreground /usr/local/bin/
WORKDIR /var/www/html

EXPOSE 80
CMD ["apache2-foreground"]
##</autogenerated>##

締めはApache2をフォアグラウンドで実行するスクリプトを実行している。


これにて完了

以上がPHPの公式Dockerfileの読解になる。

読むきっかけとなったのは、PHP5.3とApacheを動かすイメージが必要だったのが、公式から5.3が削除されており、そして有志が作ったものも少し古かったためである。だがこの読解を通して、無事にPHP5.3でビルドすることができた。

独自のDockerfileをスラスラと書けるようになりたい人は、ぜひ公式のDockerfileを読んで欲しい。