すきま風

勉強したことのメモとか

PHP (CodeIgniter), Apache のProductをDocker Imageにする

業務でPHPアプリケーションを保守することになったのでとりまコンテナ化しました。 fpm × nginxで作りたかったけど業務上Apache縛りがあったのでモジュール版を使います。

2020/06/12 追記: fpm versionも書きました

ソフトウェアバージョン

software version
PHP 7.4.3
Apache 2.4
CodeIgniter 3.1.11

追加でやること

  • Redis を使えるようにする
  • LogはJsonにして標準出力にする
  • Apacheのconfファイルの設定値は環境変数で設定する
  • gzip圧縮する
  • root userでコンテナを動かさないようにする

Dockerfile

## module php
FROM php:7.4.3-apache-buster
## composer multi stage build
## コンテナ内で composer 叩きたい場合に設定する
COPY --from=composer:1.9.3 /usr/bin/composer /usr/bin/composer
## apache, phpの設定ファイルをcopyする
COPY ["php.ini-development", "apache2.conf", "000-default.conf", "ports.conf", "rewrite.conf", "deflate.conf", "fqdn.conf",  "environment", "/tmp/"]
## project directory copy
COPY ./sample-app /var/www/html/public/sample-app
RUN apt-get update \
    ## 必要なLibraryがあったら追加する
    && apt-get -y install vim unzip \
    ## redis install. PHP5.6の場合は4.3.0を入れる
    && pecl install redis-5.2.0 \
    && docker-php-ext-enable redis \
    ## config file move
    && mv /tmp/php.ini-development "$PHP_INI_DIR/php.ini-development" \
    && mv /tmp/apache2.conf /etc/apache2/apache2.conf \
    && mv /tmp/000-default.conf /etc/apache2/sites-available/000-default.conf \
    && mv /tmp/ports.conf /etc/apache2/ports.conf \
    && mv /tmp/rewrite.conf /etc/apache2/mods-available/rewrite.conf \
    && mv /tmp/deflate.conf /etc/apache2/mods-available/deflate.conf \
    && mv /tmp/environment /etc/environment \
    ## https://askubuntu.com/questions/256013/apache-error-could-not-reliably-determine-the-servers-fully-qualified-domain-n
    && mv /tmp/fqdn.conf /etc/apache2/conf-available/fqdn.conf \
    && a2enconf fqdn \
    ## apacheで環境変数を読み込む
    && echo "" >> /etc/apache2/envvars \
    && echo "## read environment file" >> /etc/apache2/envvars \
    && echo ". /etc/environment" >> /etc/apache2/envvars \
    ## rewrite.confを設定する
    && ln -s /etc/apache2/mods-available/rewrite.conf /etc/apache2/mods-enabled/rewrite.conf \
    && a2enmod rewrite \
    ## directory権限をwww-dataにしてpermission && SGID変更
    ## directory -> 770, file -> 660
    && chown -R www-data:www-data /var/www/html/public \
    && find /var/www/html/public -type d -exec chmod 770 {} \; \
    && find /var/www/html/public -type f -exec chmod 660 {} \; \
    && chmod -R 2770 /var/www/html/public \
    ## php.ini file mv
    && mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" \
    ## userを作成してwww-data groupに入れる
    && useradd webdev \
    && usermod -g www-data webdev
## rootで起動しないのでwell-known portは使わない
EXPOSE 9000
## webdevでdocker 起動
USER webdev

config files

000-default.conf

# root userで動かさないためにwell-known portを使わないようにする
<VirtualHost *:9000>
  ServerAdmin webmaster@localhost
  DocumentRoot /var/www/html/public/sample-app

  ErrorLog ${APACHE_LOG_DIR}/error.log
  CustomLog ${APACHE_LOG_DIR}/access.log json

  <Directory /var/www/html/public/sample-app>
    Options FollowSymlinks
    AllowOverride None

    Require all granted
  </Directory>
</VirtualHost>

apache2.conf

# 前略...
# ErrorLogFormatを Jsonにする
ErrorLogFormat "{\"time\":\"%{%Y-%m-%dT%T}t.%{usec_frac}t%{%z}t\", \"function\": \"[%-m:%l]\", \"process\": \"%P\", \"file\": \"%F\",  \"error\": \"%E\", \"message\": \"%M\"}"

# 中略...

# LogFormatを Jsonにする
LogFormat "{\"time\":\"%{%Y-%m-%dT%T}t.%{usec_frac}t%{%z}t\", \"remoteIP\":\"%a\", \"host\":\"%V\", \"requestPath\":\"%U\", \"query\":\"%q\", \"method\":\"%m\", \"status\":\"%>s\", \"userAgent\":\"%{User-agent}i\", \"referer\":\"%{Referer}i\"}" json
# 後略...

deflate.conf

# FilterProvider 使っています
<IfModule mod_deflate.c>
  DeflateCompressionLevel 1
  <IfModule mod_filter.c>
    FilterDeclare COMPRESS
    FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^text/#i"
    FilterProvider COMPRESS DEFLATE "%{CONTENT_TYPE} =~ m#^application/(atom\+xml|javascript|json|rss\+xml|xml|xhtml\+xml)#i"
    FilterChain COMPRESS
    FilterProtocol COMPRESS DEFLATE change=yes;byteranges=no
  </IfModule>
</IfModule>

environment

## apacheで利用する環境変数をexportする
export SAMPLE_APP_SERVER_NAME=${SAMPLE_APP_SERVER_NAME}

Dockerfileの以下の部分で/etc/environmentを読み込むようにしています。

&& echo "" >> /etc/apache2/envvars \
&& echo "## read environment file" >> /etc/apache2/envvars \
&& echo ". /etc/environment" >> /etc/apache2/envvars \

fqdn.conf

## 環境変数を読み込む
ServerName ${SAMPLE_APP_SERVER_NAME}

php.ini-development

[Date]
date.timezone = "Asia/Tokyo"

[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"

ports.conf

Listen 9000

<IfModule ssl_module>
    Listen 443
</IfModule>

<IfModule mod_gnutls.c>
    Listen 443
</IfModule>

rewrite.conf

# CodeIgniter用のrewrite. index.phpをurlに入れなくても良いようにする
<Directory /var/www/html/public/sample-app>
  <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php/$1 [L]
  </IfModule>
</Directory>

docker-compose.yaml

version: "3"
services:
  app:
    # 事前にbuildしておいたやつ
    image: sample-php-app:0.0.1-SNAPSHOT
    ports:
      - "9000:9000"
    environment:
      # apache env
      - SAMPLE_APP_SERVER_NAME=localhost:9000

PHPのLogもJsonで標準出力する

monologを入れて、CI_Log.phpのsubclass を作ります。 monologのJsonFormatterは日本語をescapeしてしまうので、encodeしないような拡張クラスを用意しています。

sample-app/application/core/MY_Log.php

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

use Monolog\Logger;
use Monolog\Handler\StreamHandler;

require_once dirname(__FILE__) . '/ExtendedJsonLogFormatter.php';

class MY_Log extends CI_Log {
    private $logger;

    public function __construct() {
        parent::__construct();

        $log_channel = 'API';
        $log_threshold_stdout = Logger::INFO;
        $formatter = new ExtendedJsonLogFormatter();
        $this->logger = new Logger($log_channel);

        try {
            $log_handler = new StreamHandler('php://stdout', $log_threshold_stdout);
            $log_handler->setFormatter($formatter);
            $this->logger->pushHandler($log_handler);
        } catch (Exception $e) {
            die($e->getMessage());
        }
    }

    public function write_log($level, $msg) {
        switch (strtoupper($level)) {
            case 'DEBUG':
                $this->logger->addDebug($msg);
                break;

            case 'INFO':
                $this->logger->addInfo($msg);
                break;

            case 'WARN':
                $this->logger->addWarning($msg);
                break;

            case 'ERROR':
                $this->logger->addError($msg);
                break;

            default:
                $this->logger->addError('log level error');
                break;
        }

        return TRUE;
    }
}

sample-app/application/core/ExtendedJsonLogFormatter.php

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

use Monolog\Formatter\JsonFormatter;

class ExtendedJsonLogFormatter extends JsonFormatter {
    public function __construct() {
        parent::__construct();
    }

    public function format(array $record) {
        return json_encode($record, JSON_UNESCAPED_UNICODE) . ($this->appendNewline ? "\n" : '');
    }
}