PHP Slim 環境構築(11) NgnixとPHP-FPMは同じ夢を見るか

# PHP Slim 環境構築(11) NgnixとPHP-FPMは同じ夢を見るか


## Introduction

[前回](https://qiita.com/dynamitecoolguy/items/fc3a60f7b494e7513c60)は、Dockerコンテナとして、NginxとPHP-FPMを別々のコンテナに構築していました。

今回は、NginxとPHP-FPMを同じDockerコンテナの中に格納してみます。

この一連のシリーズは、自分への備忘録が第一目的のため、かなり不親切です。
すみません・・・


## なぜ一つのコンテナに入れるのか

AWS ECSでの運用を考えたときに、nginxコンテナとphp-fpmコンテナの両方を管理するという事態を避けたいからです。その理由は、以下の二つです。

1. ずっと固定数で運用しつづけるのであればそれでも良いのですが、負荷に応じて増減する場合には調整が面倒であること。
2. nginxとphp-fpm間の通信自体も馬鹿にならないのではないかも? (未調査。AZが分かれた場合はどうなる? または、同一AZを保証する仕組みが必要)

なお、Dockerコンテナには一つのプロセスのみを入れるべしというベストプラクティスには反することになりますが、メリットの方が大きい気がします
(実際にパフォーマンスやコストなどを測定・検討したわけではありません)。


## 変更点

前回までは、nginxコンテナとphp-fpmコンテナを構築していました。
今回は、nginxコンテナ(リバースプロキシ用)と、nginx+php-fpmコンテナ(バックエンド用)を構築することにします。なお、バックエンド用のnginx+php-fpmの二つのプロセス間の通信はunixドメインソケットを使用します。


### ソースツリー

今回の変更箇所です。

```
$(PROJECTROOT)
  /compose
    /web_front
      local.conf
    /web_hoge
      Dockerfile
      nginx.conf                (新規)
      nginx-server.conf         (local.confから改名)
      nginx-supervisor.conf     (新規)
      php.ini
      php-fpm.conf              (新規)
      php-fpm-supervisor.conf   (新規)
      supervisord.conf          (新規)
```

### local.conf

fastcgi用のnginxがweb_hogeに移動したので、このnginxの役割はreverse proxyのみです。
サーバ名がhoge.localhostのリクエストは、web_hogeコンテナに振り分けます。

```conf:/compose/web_front/local.conf
ssl_certificate /etc/certs/server.crt;
ssl_certificate_key /etc/certs/server.key;

ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

server {
  listen 3128;
  listen 8443 ssl;
  server_name hoge.localhost;
  access_log /dev/stdout;
  error_log /dev/stderr;

  proxy_set_header Host            $host;
  proxy_set_header X-Real-IP       $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  location / {
    proxy_pass http://web_hoge:3128/;
  }
}

server {
  listen 3128 default_server;
  listen 8443 ssl default_server;
  server_name _;

  location / {
    return 404;
  }
}
```


### Dockerfile

大幅に書き換わっています。主な変更点は以下の通りです。

* ベースイメージがphp:7.3.9-fpm-alpine3.10からalpine3.10.2に変更
* phpおよびそのモジュールのインストールは、peclやdocker-php-ext-enableなどではなく、apkコマンドに変わっています
* タイムゾーンを明示的にUTCにセット
* supervisorとその設定のインストール
* php-fpm用ユーザ(www-data:www-data, uid=1000, gid=1000)と、nginx用ユーザ(nginx:www-data, uid=1001, gid=1000)を追加しています

```dockerfile:/compose/web_hoge/Dockerfile
FROM alpine:3.10.2

ARG environment

RUN apk upgrade --update \
  && apk --no-cache --virtual .build-deps add make g++ gcc re2c autoconf curl \
  && apk --no-cache add gettext-dev libzip-dev curl-dev

RUN apk --no-cache add supervisor tzdata nginx php7 php7-fpm \
     php7-gettext php7-mbstring php7-zip php7-ctype php7-json php7-bcmath php7-sockets php7-curl php7-simplexml \
     php7-pecl-apcu php7-pecl-igbinary php7-pecl-msgpack

# PDO(MySQL)
RUN apk --no-cache add mariadb-dev php7-pdo_mysql

# PDO(PostgreSQL)
RUN apk --no-cache add postgresql-dev php7-pdo_pgsql

# YAML (2.0.4)
RUN apk --no-cache add yaml-dev php7-pecl-yaml

# install phpize
RUN apk --no-cache add php7-dev

# redis (5.0.2) extension=redis.so
RUN curl -fsSL https://github.com/phpredis/phpredis/archive/5.0.2.tar.gz -o redis.tar.gz \
  && mkdir -p /usr/src/php/ext/redis \
  && tar xzf redis.tar.gz -C /usr/src/php/ext/redis --strip-components=1 \
  && rm redis.tar.gz \
  && cd /usr/src/php/ext/redis \
  && phpize \
  && ./configure --enable-redis-igbinary \
  && make \
  && make install \
  && echo "extension=redis.so" > /etc/php7/conf.d/80_redis.ini \
  && make distclean

# remove phpize
RUN apk del php7-dev

# xdebug(2.7.2) extension=xdebug.so
RUN if [ "${environment}" = "local" ]; then \
     apk --no-cache add php7-pecl-xdebug \
  && echo "zend_extension=xdebug.so" > /etc/php7/conf.d/xdebug.ini \
  && echo "[XDebug]" >> /etc/php7/conf.d/xdebug.ini \
  && echo "xdebug.remote_enable=1" >> /etc/php7/conf.d/xdebug.ini \
  && echo "xdebug.remote_connect_back=0" >> /etc/php7/conf.d/xdebug.ini \
  && echo "xdebug.remote_autostart=0" >> /etc/php7/conf.d/xdebug.ini \
  ; fi

# TZ
RUN cp /usr/share/zoneinfo/UTC /etc/localtime \
  && echo "UTC" > /etc/timezone

# supervisord
RUN mkdir -p /etc/supervisor.d
COPY supervisord.conf /etc/supervisord.conf

# settings
COPY settings.php /var/www/settings.php

# PHP
COPY php.ini /php.ini
COPY php-fpm.conf /php-fpm.conf
COPY php-fpm-supervisor.conf /php-fpm-supervisor.conf
RUN cp /php-fpm.conf /etc/php7/php-fpm.d/z-php.conf \
  && cp /php.ini /etc/php7/conf.d/config.ini \
  && cp /php-fpm-supervisor.conf /etc/supervisor.d/php-fpm.conf

# Nginx
COPY nginx.conf /nginx.conf
COPY nginx-server.conf /nginx-server.conf
COPY nginx-supervisor.conf /nginx-supervisor.conf
RUN cp /nginx.conf /etc/nginx/nginx.conf \
  && cp /nginx-server.conf /etc/nginx/conf.d/default.conf \
  && cp /nginx-supervisor.conf /etc/supervisor.d/nginx.conf

RUN apk del --purge .build-deps \
  && rm -rf /var/cache/apk/* \
  && rm -rf /tmp/*

RUN deluser nginx \
    && delgroup www-data \
    && addgroup -g 1000 -S www-data \
    && adduser -u 1000 -D -S -G www-data www-data \
    && adduser -u 1001 -D -S -G www-data nginx

EXPOSE 3128

ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
```

### nginx.conf

Nginxのメインの設定ファイル(/etc/nginx/nginx.conf)。
Nginxのdockerイメージから拝借し、以下の箇所を変更しました。

* Nginxのworker processの実行ユーザをnginx:www-dataに変更 (user nginx www-data;)
* pidファイルの出力先を指定 (pid /var/run/nginx.pid;)
* worker process数を1に固定　(コンテナの性能に合わせて調整が必要) (worker_processes 1;)
* error_logの出力先をSTDERRに指定 (error_log /dev/stderr warn;)
* access_logの出力を抑制 (#access_log)

```conf:/compose/web_hoge/nginx.conf
# /etc/nginx/nginx.conf

user nginx www-data;          ## OVERWRITTEN

pid /var/run/nginx.pid;       ## ADDED

# Set number of worker processes automatically based on number of CPU cores.
# worker_processes auto;
worker_processes 1;           ## OVERWRITTEN

# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;

# Configures default error logger.
# error_log /var/log/nginx/error.log warn;
error_log /dev/stderr warn;   ## OVERWRITTEN

# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;


events {
	# The maximum number of simultaneous connections that can be opened by
	# a worker process.
	worker_connections 1024;
}

http {
	# Includes mapping of file name extensions to MIME types of responses
	# and defines the default type.
	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	# Name servers used to resolve names of upstream servers into addresses.
	# It's also needed when using tcpsocket and udpsocket in Lua modules.
	#resolver 208.67.222.222 208.67.220.220;

	# Don't tell nginx version to clients.
	server_tokens off;

	# Specifies the maximum accepted body size of a client request, as
	# indicated by the request header Content-Length. If the stated content
	# length is greater than this size, then the client receives the HTTP
	# error code 413. Set to 0 to disable.
	client_max_body_size 1m;

	# Timeout for keep-alive connections. Server will close connections after
	# this time.
	keepalive_timeout 65;

	# Sendfile copies data between one FD and other from within the kernel,
	# which is more efficient than read() + write().
	sendfile on;

	# Don't buffer data-sends (disable Nagle algorithm).
	# Good for sending frequent small bursts of data in real time.
	tcp_nodelay on;

	# Causes nginx to attempt to send its HTTP response head in one packet,
	# instead of using partial frames.
	#tcp_nopush on;


	# Path of the file with Diffie-Hellman parameters for EDH ciphers.
	#ssl_dhparam /etc/ssl/nginx/dh2048.pem;

	# Specifies that our cipher suits should be preferred over client ciphers.
	ssl_prefer_server_ciphers on;

	# Enables a shared SSL cache with size that can hold around 8000 sessions.
	ssl_session_cache shared:SSL:2m;


	# Enable gzipping of responses.
	#gzip on;

	# Set the Vary HTTP header as defined in the RFC 2616.
	gzip_vary on;

	# Enable checking the existence of precompressed files.
	#gzip_static on;


	# Specifies the main log format.
	log_format main '$remote_addr - $remote_user [$time_local] "$request" '
			'$status $body_bytes_sent "$http_referer" '
			'"$http_user_agent" "$http_x_forwarded_for"';

	# Sets the path, format, and configuration for a buffered log write.
	#access_log /var/log/nginx/access.log main;  ## OVERWRITTEN


	# Includes virtual hosts configs.
	include /etc/nginx/conf.d/*.conf;
}
```

### nginx-server.conf

/etc/nginx/nginx.confから呼び出される個別設定ファイル(/etc/nginx/conf.d/default.conf)。

標準的なFastCGI用の設定です。
なお、このサーバがSSLリクエストを直接受けることは無いので、SSL関連の設定はしていません。
また、アクセスログ、エラーログは標準出力、標準エラー出力に出しています。

```conf:/compose/web_hoge/nginx-server.conf
server {
  listen 3128;
  server_name _;
  access_log /dev/stdout;
  error_log /dev/stderr warn;
  root /var/www/hoge/public;

  location = /favicon.ico {
    empty_gif;
    access_log off;
  }

  location / {
    try_files $uri $uri/ /index.php?$query_string;
    index     index.php;
  }

  location ~ \.php$ {
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass unix:/var/run/php-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param HTTPS $https;
  }
}
```

### nginx-supervisor.conf

supervisorサービス用の設定です(/etc/supervisor.d/nginx.conf)。
この設定によって、nginxがsupervisorによって呼び出されます。
また、nginxプロセス上の標準出力、標準エラー出力は、そのまま、Dockerコンテナの標準出力、標準エラー出力に出力しています。



```conf:/compose/web_hoge/nginx-supervisor.conf
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
priority=10
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
```

### php.ini

PHPの上書き用設定ファイルです(/etc/php7/conf.d/config.ini)。
php-fpmの子プロセスのメモリ上限(memory_limit)は、コンテナに合わせて調整が必要です。

```ini:/compose/web_hoge/php.ini
[PHP]
log_errors = On
display_startup_errors = On
error_reporting = E_ALL
upload_max_filesize = 100M
post_max_size = 128M
memory_limit = 256M
```

### php-fpm.conf

php-fpm用の上書き用の設定です。最後に読み込まれるようにファイル名を"z-php.conf"になっています。
php-fpmの子プロセスの個数(pm.max_children)は1にしていますが、nginxのworkerプロセスと同様に、コンテナに合わせて調整が必要です。

また、デフォルトの設定だと、php-fpmのpoolプロセスに環境変数が渡らないため、docker-compose-local.ymlで指定したXDEBUG_CONFIGとPHP_IDE_CONFIGがphp-fpmにセットされないという事態になります。したがって、"clear_env = no"を追加します。

```conf:/compose/web_hoge/php-fpm.conf
[global]
pid = /var/run/php-fpm.pid

[www]
listen = /var/run/php-fpm.sock
listen.owner = www-data
listen.group = www-data
access.log = /dev/null
user = www-data
group = www-data
pm = static
pm.max_children = 1
clear_env = no
```

### php-fpm.supervisor.conf

supervisorサービス用の設定です(/etc/supervisor.d/php-fpm.conf)。
この設定によって、php-fpmがsupervisorによって呼び出されます。
また、php-fpmプロセス上の標準出力、標準エラー出力は、そのまま、Dockerコンテナの標準出力、標準エラー出力に出力しています。

```conf:/compose/web_hoge/php-fpm.supervisor.conf
[program:php-fpm]
command=/usr/sbin/php-fpm7 --nodaemonize
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
```


### supervisord.conf

supervisorプロセス用の設定です。Dockerのpid=1プロセスになるので、daemon化しません。

```conf:/compose/web_hoge/supervisord.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisord.log
pidfile=/var/run/supervisord.pid

[include]
files=/etc/supervisor.d/*.conf
```

## 起動

以前作成したローカル環境と同じです。

```shell
$ docker-compose -f docker-compose-common.yml -f docker-compose-local.yml build
$ docker-compose -f docker-compose-common.yml -f docker-compose-local.yml up -d
```

## ここまでのソース

[こちら](https://github.com/dynamitecoolguy/slimtemplate/tree/chapter11)でどうぞ。

## 変更履歴

### 2019/10/16

* php-fpm.confにclear_env = noを追加した旨を追記