「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
に保存する。
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 install
はmake
でビルドしたバイナリなどを規定のディレクトリに移動させるコマンドである。
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-developmentやphp.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を読んで欲しい。