docker-compose による nginx + HTTP/2 + PHP-FPM7 + MySQL 環境の構築方法

はじめに

Docker とは、コンテナ型の仮想環境技術です。シンプルかつ軽量な仕組みであることから、ここ数年でインフラエンジニアだけでなく web サービスエンジニアの間でも広がりをみせています。今でこそ Amazon EC2 Container Service ( AWS ECS ) 等の登場によって実サービスの運用にも使われていますが、元々は VirtualBox のようなローカルマシンでの動作確認や開発環境構築が主な用途でした。

当ブログでも以前に『docker-compose を使って WordPress テーマ開発環境を構築しよう』というエントリにて Docker を使った WordPress 開発環境を構築する手順をご紹介しました。今回は nginx + HTTP/2 + PHP-FPM7 + MySQL というよりモダンな構成の環境を構築する手順についてご紹介します。

構築までのステップ

  1. nginx だけのシンプルな静的 web サーバを構築
  2. PHP 環境を構築
  3. PHP と MySQL を接続
  4. HTTP/2 に対応させる

このようなチュートリアル形式で進めていきます。当エントリのサンプルコードは GitHub にて公開しておりますので、ぜひ併せてご参照ください。

Docker および docker-compose のインストール手順につきましては割愛します。こちらのエントリをご参照ください。

Step.1) nginx だけのシンプルな静的 web サーバを構築

まずは nginx だけを使って基本となるシンプルな静的 web サーバを構築するとしましょう。サンプルデータの構成は以下の通りです。

.
├── data/
│   └── html/           # 静的コンテンツ
│       ├── img.jpg
│       ├── index.html
│       ├── main.css
│       └── main.js
├── docker-compose.yml
└── web/                # nginx 設定ファイル
    └── default.conf

はじめに docker-compose.yml ファイルを作成し、Docker コンテナ情報を記述してインフラのアウトラインを定義します。

version: '3'
services:
  web:
    image: nginx:1.13.5-alpine
    ports:
      - "80:80"
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf
      - ./data/html:/var/www/html

『web サーバ』ということで、サービス名を web とします。サービスの設定に使用しているオプションは以下となります。

オプション設定 概要
image コンテナを起動する際のベースとなる Docker イメージを指定
ports 公開するポート番号を指定。ホスト側:コンテナ側という形式で指定することで、ホストマシン側のポートとコンテナ側のポートをマッピング。
volumes ホスト側:コンテナ側の形式で指定することで、ホスト側の任意のディレクトリ or ファイルをコンテナ内にマウントします。
ホスト側を省略すると Data Volumeという領域が Docker システムディレクトリ以下に自動生成されて、コンテナ内にマウントされる。

使用する Docker イメージは公式の nginx ですが、今回は Alpine という Linux OS がベースとなっているものを選定します。

捕捉: Alpine Linux について

Alpine Linux とは、非常に軽量な Linux ディストリビューションのひとつです。Docker 公式のベースイメージに採用されたことをきっかけに急速に認知されるようになりました。一般的に Linux ディストリビューションというと CentOS や Ubuntu などが有名ですが、これらは汎用的な用途のために様々な機能やアプリなどが搭載されています。しかしコンテナ用途で使うのであれば、それらは必ずしも必要とは限りません。Alpine Linux はコンテナ用途以外の機能をことごとく削ぎ落として徹底的に軽量化がなされています1)Vi すら搭載してません。。デフォルトの nginx イメージ ( latestタグ ) は Debian をベースとしていますが、それと比較するとその軽さは一目瞭然。

Linux ディストリビューション Tag Size
Debian :latest 108MB
Alpine Linux :alpine 15.5MB

なんと約7分の1という驚異のファイルサイズ。パッケージ管理システムが apt でなく apk であるなど扱いに若干の違いはありますが、普通に使うぶんにはそうそう困ることはありません。

nginx 設定ファイルを作成してコンテナにマウント

nginx の設定を記述します。デフォルトのイメージには殆ど何も定義されていないため、こちらで記述する必要があります。

server {
    listen 80;
    server_name _;
    root  /var/www/html;
    index index.html;
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;
}

最小構成とも言える非常にシンプルな設定です。portは HTTP 標準の 80 とし、server_nameは全ての名称にマッチさせるために _ とします。サーバのルートディレクトリは /var/www/htmlとし、 index.html をインデックスとします。この設定ファイルをコンテナ内の /etc/nginx/conf.d/default.conf としてマウントします。

volumes:
  - ./web/default.conf:/etc/nginx/conf.d/default.conf

/etc/nginx/conf.d/ディレクトリ以下に配置すると、/etc/nginx/nginx.conf が読み込まれて設定が有効化となります。

静的コンテンツをマウントしてコンテナ起動

nginx 設定ファイルと同じ要領で静的コンテンツをコンテナ内にマウントします。マウント先は default.conf で root に設定したディレクトリです。

./data/html:/var/www/code

ちなみにホストマシン側からマウントしたボリュームはコンテナ内と同期されるため、ホスト側で編集した内容はそのままコンテナ側に反映されます。

これで準備が整いました。ターミナルを起動し、docker-compose.yml のあるディレクトリへ移動して以下のコマンドを実行します。

docker-compose up -d

起動コマンドに -d オプションを付けることでコンテナをバックグラウンドで起動させておくことが出来ます ( デタッチモード ) 。これをしないとそのウィンドウ ( タブ ) 上で別の操作をすることが出来ないので、特別な理由がない限り付けておくと良いでしょう。

以下のようなログが出力されれば起動成功です。

Creating network "xxxxxxxx_default" with the default driver
Creating xxxxxxxx_web_1 ...
Creating xxxxxxxx_web_1 ... done

ブラウザから http://localhost にアクセスしてみましょう。以下のようなページが表示されるはずです。

Step.2) PHP 環境を構築

nginx から PHP を実行するには PHP-FPM というものを使います。FPM ( FastCGI Process Manager ) とは PHP の FastCGI 実装のひとつで、高負荷なサイトでの運用に適しているのが特徴です。CGI とはユーザからリクエストが来る度にプログラムを実行してプロセスの生成と破棄を行うものですが、リクエストの数が多ければそれだけ生成と破棄を繰り返す回数も多くなるため、パフォーマンスの悪化につながります。

これに対し FashCGI は初回リクエスト時に起動したプロセスをメモリ上に保持し、次回以降のリクエストに対してはその保持したプロセスを実行すします。よってプログラムの実行速度向上とサーバ不可の軽減が期待できます。nginx はこの FastCGI を通じて PHP を実行出来るので、こちらを使いましょうというわけです。

docker-compose を編集

PHP 用のサービスを定義します。いわゆるアプリケーションが動作するところなので、app というサービス名にします。

services:
  ⋮
  app:
    image: php:7.1.9-fpm-alpine
    volumes:
      - ./data/html:/var/www/html

nginx 同様、公式から Alpine ベースのイメージが提供されてますので、そちらを指定します。また、こちらにも コンテンツを volumes としてマウントします。

次に web サービス ( nginx ) にdepends_on というオプションを追記します。

services:
  web:
    image: nginx:1.13.5-alpine
    ports:
      - "80:80"
    depends_on:
      - app
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf
      - ./data/html:/var/www/html
  ⋮
オプション設定 概要
depends_on サービス間の依存関係を指定。依存関係のある順序に従ってサービスが起動するようになる。

nginx.conf を編集

nginx から PHP を実行するための設定を追記します。

server {
    listen 80;
    server_name _;
    root  /var/www/html;
    index index.php;
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;
    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(\.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

注目すべき点は fastcgi_pass です。通常であればここは接続 ( PHP が実行される ) 先のホスト名や IP アドレスを指定しますが、 Docker で構築する場合は対象となるサービス名がそのままホスト名となるので ( ※ 違う名前への変更も可能 ) 、今回の場合は app:9000 と指定すれば OK です2)ポート番号はデフォルトの 9000 で問題ないでしょう。

コンテンツに index.php を追加

PHP が正常に動作しているかを確認出来れば充分なので、Step.1 で使用した index.htmlindex.php に変更し、ファイルの最終行に以下を追記するだけにしておきます。

⋮
<?php
phpinfo();

コンテナ起動

準備が整いました。ターミナルから起動コマンドを実行します。今度は2つのコンテナが起動するはずです。

docker-compose up -d
Creating network "xxxxxxxx_default" with the default driver
Creating xxxxxxxx_app_1 ...
Creating xxxxxxxx_app_1 ... done
Creating xxxxxxxx_web_1 ...
Creating xxxxxxxx_web_1 ... done

ブラウザから http://localhost にアクセスしてみましょう。以下のように PHP 情報が表示されていれば成功です。

Step.3) PHP と MySQL を接続

データベースなしに web アプリケーションは語れません。そして WordPress のイメージもあってか、PHP と MySQL はもはや切っても切れない関係にあると言っても過言ではありません。ということで先ほどの構成に MySQL を追加して PHP から接続してみましょう。

docker-compose に DB サービスを追加

db というサービス名で定義します。

services:
  ⋮
  db:
    image: mysql:5.7.19
    env_file: .env
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql
      - ./db/initial.sql:/docker-entrypoint-initdb.d/initial.sql
volumes:
  db-data:

mysql:5.7.19 という公式のイメージを指定します。Docker はそのままですとコンテナの破棄と同時にデータも消失してしまいますので、volumes を使ってデータをホストマシン側で保存しておき、起動時にコンテナ内のデータ領域 ( /var/lib/mysql ) にマウントするようにします。ホスト側の管理方法は、上記のように docker-compose トップレベルの volumes 内に定義したものをコンテナ内にマウントするようにします。これはホストマシンの Docker システムディレクトリ配下でデータを管理するための記法です。こうすることで管理者が直接編集する必要のない volume を隠蔽するというメリットがあります。もちろんコンテンツデータと同じようにホスト側のパスを明記しても動作上なんら問題ありません。

/db/initial.sql は今回のデモ用に DB に初期データを流し込むための SQL です。

MySQL に接続するにはユーザ名やパスワードの設定が必要ですが、これらは docker-compose.yml に直接記述するのでなく .env という外部ファイルで環境変数として管理しておくのが良いです。

MYSQL_RANDOM_ROOT_PASSWORD=yes
MYSQL_DATABASE=step3
MYSQL_USER=db_user
MYSQL_PASSWORD=password

app サービスを編集

PHP から MySQL に接続ということで、app サービスは db サービスに依存します。よってそれを以下のように定義します。

services:
  ⋮
  app:
    build: ./app
    env_file: .env
    environment:
      DATABASE_HOST: db
    depends_on:
      - db
    volumes:
      - ./data/html:/var/www/html

MySQL に接続するための DB 情報が PHP にも必要となりますので、こちらでも.env を読み込みます。更に DBホスト名 ( DATABASE_HOST ) も必要なので、こちらも明記します。そしてサービス間の依存関係を depends_on で定義します。

Dockerfile でイメージを定義

公式のイメージそのままでは不足なので、それをベースに自前の Docker イメージを定義します。

FROM php:7.1.9-fpm-alpine
RUN docker-php-ext-install pdo_mysql mysqli mbstring

といっても定義内容は MySQL と接続するためのツールのインストールだけです。これを docker-compose.yml から読み込むために、 imageオプションを build: ./app オプションに変更します。

最後に data/html/index.php に DB へデータを INSERT するロジックを記述します。

コンテナ起動

準備が整いました。ターミナルから起動コマンドを実行します。DB も追加したので3つのコンテナが起動するはずです。

docker-compose up -d
Creating network "xxxxxxxx_default" with the default driver
Creating volume "xxxxxxxx_db-data" with default driver
⋮
Creating xxxxxxxx_db_1 ...
Creating xxxxxxxx_db_1 ... done
Creating xxxxxxxx_app_1 ...
Creating xxxxxxxx_app_1 ... done
Creating xxxxxxxx_web_1 ...
Creating xxxxxxxx_web_1 ... done

ブラウザから http://localhost にアクセスすると次のようなログが出力されてるかと思います。こちらはページをリロードする度に INSERT が実行されて数値が増えていきます。

array(2) {
  [0]=>
  string(1) "1"
  [1]=>
  string(19) "2017-09-17 10:11:45"
}

HTTP/2 に対応させる

HTTP/2 とは、 web サイトの通信を高速化するための次世代の仕組み ( 通信プロトコル ) です。パフォーマンスが向上するだけでなく、SSL 通信 ( HTTPS ) となるのでサイトをよりセキュアな状態にできます。近頃はGoogle が HTTPS 対応しているサイトの検索順位を優遇することを表明していることもあり、もはや web 製作において HTTP/2 対応は必須といえます。

最後にそんな HTTP/2 をお手軽に再現する方法をご紹介します。

docker-compose にポート番号を追加

HTTP/2 ( HTTPS ) は SSL 通信となるため、ポート番号 443 を追記します。

services:
  web:
    build: ./web
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app
    volumes:
      - ./web/default.conf:/etc/nginx/conf.d/default.conf
      - ./data/html:/var/www/html
  ⋮

app サービス同様、こちらも公式イメージをベースに自前の Docker イメージを定義します。image オプションを build: ./web に変更し、Dockerfile を作成します。

Dockerfile でイメージを定義

FROM nginx:1.13.5-alpine
# ツールをインストール
RUN apk --update add openssl
# ルートディレクトリを作成
RUN mkdir -p /var/www/html
# 自己証明書を発行
RUN openssl genrsa 2048 > server.key \
 && openssl req -new -key server.key -subj "/C=JP/ST=Tokyo/L=Chuo-ku/O=RMP Inc./OU=web/CN=localhost" > server.csr \
 && openssl x509 -in server.csr -days 3650 -req -signkey server.key > server.crt \
 && cp server.crt /etc/nginx/server.crt \
 && cp server.key /etc/nginx/server.key \
 && chmod 755 -R /var/www/html \
 && chmod 400 /etc/nginx/server.key

ここで主にすることは、HTTPS通信のための SSL 証明書発行です。HTTPS 通信はデータを暗号化して行いますので、それを実現するためにサーバ側に SSL証明書というものを設置しなくてはなりません。

通常であれば認証局と呼ばれる政府から認可を受けた業者から有料で発行してもらうものなのですが、個人の学習やちょっとした動作確認が目的であれば自前で発行して代用することも可能です3)けっして本番運用では使わないでください。。発行は openssl というツールを使ってコマンドラインから行います。

nginx.conf を編集

HTTP/2 を有効化させます。nginx は ver1.9.5 から HTTP/2 をサポートしているので、HTTP2 と同時に設定できます。

server {
    listen 80;
    server_name _;
    return 301 https://$host$request_uri;
}
server {
    listen 443 ssl http2;
    server_name _;
    root  /var/www/html;
    index index.php;
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log;
    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(\.+)$;
        fastcgi_pass app:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
    # SSL 暗号化
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_certificate     /etc/nginx/server.crt;
    ssl_certificate_key /etc/nginx/server.key;
    ssl_session_timeout 1d;
    ssl_session_cache   shared:SSL:50m;
}

listen ディレクティブにポート番号と ssl を指定し、さらに http2 と指定します。これだけです。次に SSL 暗号化のためのプロトコル設定や証明書を指定のディレクトリに配置する設定を行います。

また、http:// ( 80 ) でアクセスしてきたら https:// にリダイレクト ( 301 ) する設定も入れておくとユーザに親切なサイトとなります。

コンテナ起動

ターミナルから起動コマンド ( docker-compose up -d ) を実行し、https://localhost を開いて HTTP/2 に対応しているか確認してみましょう。今回は自己発行証明書を使用しているため、ブラウザから証明書の認証エラーとの警告が出されますが、詳細情報を表示しlocalhost にアクセスする をクリックして強制的にアクセスします。

Chrome などの Dev Tools を開き、Networkタブを開いてみましょう。Protocol カラムに h2 という値があれば HTTP/2 通信が出来ています。

以上で HTTP/2 対応は完了です。

締め

スケーラブル・メンテナブルな仮想環境の構築はもちろん、ローカルでの開発・検証用途にもその力を発揮するのが Docker です。複数人での開発作業で最初のハードルとなるのが、いかに開発環境の均一化を維持するかです。Docker であれば仮想環境の状態全てがコードで管理されるため、非常にメンテナンス性が高く齟齬の発生リスクを抑えられます。

また、インフラの構成をコードで管理するということは、全体像の俯瞰が容易であることを意味します。仮想環境なので作っては壊しを気軽に繰り返せるので学習も非常に捗るというメリットがあります。

脚注

脚注
1 Vi すら搭載してません。
2 ポート番号はデフォルトの 9000 で問題ないでしょう。
3 けっして本番運用では使わないでください。