Dockerを使ったphp-fpm(+Laravel)とNginx環境の構築


背景

「LaravelってどうやってHTTPサーバと連携するんだ?」と思い、調査しましたが結構はまったので、自分用のメモも兼ねて記事を書きます。

やりたいこと

・Laravelを使ったWebアプリサーバを立てたい
・Dockerを使って、コマンド一発で楽にWebサーバとWebアプリサーバを立てたい

特にDockerを使ってWebサーバとWebアプリサーバを立てるのは、Kubernetesを使うのに必須だったりするので、鍛錬もかねて、実施しました。

システムの全体図

システムというほどのものではないですが、下記のようにDockerの環境を構築しました。

・Nginxのコンテナを立てて、80番と443番でHTTP/HTTPSのリクエストを受け付ける
・NginxのコンテナとWebアプリのコンテナは9000番で通信
・Webアプリのコンテナには、外部から直接アクセスはできない

負荷分散は今回は未実施です。
(勉強のためKubernetesやりたいので、取り組んだらまた投稿します。)

今回もdocker-compose(コンテナをまとめて管理できるツール)を使いました。

Composeファイル

ディレクトリ構造は下記のようにしました。

project/
  ├ nginx/
  │  └ dockerfile
  │  └ server.conf
  ├ php-fpm/
  │  └ dockerfile
  │  └ www.conf
  ├ web/
  │  └ Laravelアプリ/
  ├ docker-compose.yml

docker-compose.ymlは下記のようにしました。

docker-compose.yml

version: '3'
services:
    web:
        build: 
            context: ./nginx
        ports:
            - "80:80"
            - "443:443"
        volumes:
            - ./web/public:/etc/nginx/public
            - ../ssl/certs/:/etc/pki/tls/certs/
            - ../ssl/private/:/etc/pki/tls/private/
        depends_on:
            - app
        container_name: blog_web
    app:
        build: ./php-fpm
        volumes:
          - ./web:/var/www/
        container_name: blog_app

webがNginxコンテナで、appがphp-fpm+laravelのコンテナです。

Nginxコンテナは
・外部から、80番と443番を受け付けるようにし、appと通信できる
・publicディレクトリにおいてある、cssやjsを直接さわれるようにする(staticファイルはnginxから直接返す)
・sslの証明書と鍵は、アクセスの権限を調整して、マウントして見れる

php-fpm+Laravelコンテナは
・ソースをまるまるマウントする

という感じです。

Nginxのコンテナ

下記のように書くだけです。

Dockerfile

FROM nginx:latest

# 設定ファイルを指定の場所に置く
COPY ./server.conf /etc/nginx/nginx.conf


CMD ["nginx", "-g", "daemon off;"]

server.conf

user nginx;

worker_processes auto;
pid /var/run/nginx.pid;

events{
    worker_connections 2048;
    multi_accept on;
    use epoll;
}


http {
    charset UTF-8;
    # versionを表示しない
    server_tokens off;
    include /etc/nginx/mime.types;
    default_type text/plain;
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    ssl_protocols TLSv1.1 TLSv1.2;

    server {
            listen 80;
            server_name localhost;
            # 80番でアクセスしてきた人は、443番のリダイレクトを返す
            return 301 https://$host$request_uri;
    }
    server {
            listen 443;
            ssl on;
            server_name  localhost;
            # 証明書
            ssl_certificate /etc/pki/tls/certs/example.crt;
            # 秘密鍵
            ssl_certificate_key /etc/pki/tls/private/example.key;

            # rootディレクトリ
            root /var/www/public;

            add_header X-Frame-Options "SAMEORIGIN";
            add_header X-XSS-Protection "1; mode=block";
            add_header X-Content-Type-Options "nosniff";

            # indexファイルの指定
            index index.php index.html;

            charset utf-8;

            # アクセスしてきたパスに対応するファイルを返す
            location / {
                try_files $uri $uri/ /index.php?$query_string;
            }

            # staticファイルはnginxが直接返す

            # cssファイル
            location /css/ {
               alias public/css/;
            }
            # jsファイル
            location /js/ {
                alias public/js/;
            }
            # imageファイル
            location /images/ {
                alias public/images/;
            }
            # fontsファイル
            location /fonts/ {
                alias public/fonts/;
            }
            # faviconリクエストはログを残さない
            location = /favicon.ico { access_log off; log_not_found off; }

            # php-fpmとの連携
            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;
            }

            # 指定のパス以外へのアクセスを禁止する
            location ~ /\.(?!well-known).* {
                deny all;
            }

            # 証明書更新のパスへアクセスできるようにする
            location /.well-known/ {
                alias public/.well-known/;
            }

            # クローラがアクセスできるようにする
            location /robots.txt {
                alias public/robots.txt;
            }

            # サイトマップにアクセスできるようにする
            location /sitemap.xml {
                alias public/sitemap.xml;
            }
    }
}

まだわかっていない箇所もありますが、
Nginxのコンテナは、

fastcgi_pass  app:9000;

とすることで、Webアプリのコンテナのpublicディレクトリのindex.phpをたたきにいけるようにしています。
私はここの記載の仕方でちょっとはまりました。。。

cssファイルやjsファイルなどのstaticファイルは、Nginxコンテナがアプリのpublicディレクトリをマウントしていて、Nginxから直接返せるようにしています。
クローラが見に来れるようにrobots.txtとsitemap.xmlにアクセスできるようにしています。

php-fpm+Laravelコンテナ

dockerfileに入る前に、そもそもphp-fpmとは何ぞ???というのが自分の中であったので、調べました。
公式ドキュメントとか見ましたが、一番わかりやすかったのは他者様の記事

php-fpmとは

php向けのFastCGIで、メモリでキャッシュを保持することで高速にWebサーバ上でPHPを動作させるアプリケーション

ということです。
この他者様の記事ではコンテナ間の通信のことも書いてありましたが、今回Nginxとphp-fpmはTCPで通信しています。
次やる時はUNIXドメインソケットで通信しようかと思います。

さて、Dockerfileですが、下記のようにしました。

Dockerfile

FROM php:7.4.7-fpm

RUN apt update -y
RUN apt install -y libfcgi0ldbl curl git unzip wget vim

# nginxというユーザを作る
RUN useradd -m -s /bin/sh -u 1000 nginx

# 設定ファイルを指定の場所に置く
COPY ./www.conf /usr/local/etc/php-fpm.d/zzz-www.conf

# Composeインストール
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# nodejs 12インストール
RUN apt install -y npm
RUN npm install n -g
RUN n 12

# ユーザ変更
USER nginx

# 作業ディレクトリ
WORKDIR /var/www

VOLUME ["/var/run/php-fpm"]

www.conf

[www]
user = nginx
group = nginx
listen = 9000
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
request_terminate_timeout=30s

php-fpmの設定はwww.confの設定ファイルと公開されているDockerImageで設定は簡単にできました。
あとは、コンテナにComposerとnodejs12を入れたかったので、Dockerfile内でインストールするようにしました。
実際のnpmパッケージのインストールと、composerのビルドは、コンテナを立ち上げた後、コマンドで実行しています。

docker exec blog_app composer install --optimize-autoloader --no-dev
docker exec blog_app npm install
docker exec blog_app npm run production

このようにしたのは、マウント後にnpmパッケージのインストールやcomposerのビルドをしないと、ソースがない時にdockerビルドが走り、失敗してしまうからです。
dockerfile内で、ソースをとってくる処理を書いている場合は、dockerfile内でnpmパッケージのインストールやcomposerのビルドができます。

Composeのビルド

あとは、コンテナのビルド+コンテナ起動して、npmパッケージのインストールやcomposerのビルドして終了です。

docker-compose build

docker-compose up -d

ドメイン設定・SSL対応

ドメインはこちらで買いました。
DNSはAWSのものを使いたかったので、Route53を使用しました。
設定は下記です。

DNSの設定はこちらを参考しました。

最低限の設定は下記で、
・Aレコード:グローバルIPとホスト名の紐付け
・NSレコード:ドメインのDNSサーバ(ドメインを購入したサイトのネームサーバにこの値を登録します。)
・SOAレコード:上位のDNSサーバ
オプションで、
・CNAMEレコード:別ホスト名
・TXTレコード:今回はクローラの認証情報記載
を記載しています。

証明書の取得は、お金をかけずにやりたかったので、
他者様の記事を参考にbacmeでオレオレ証書を取得しました。

ホスト名とマシンの紐付けができていれば、やることは単純で、
bacmeをgit cloneしてきて、取得したホスト名で

./bacme -w /var/www/example/ example.com www.example.com

を実行します。wは、.well-knownディレクトリのパスです。
(80番でwell-knownのパスにアクセスできないと↑のコマンドは失敗します。アクセスできるようにNginxの設定を見直してみてください。)
成功すると、bacmeディレクトリ直下に証明書と秘密鍵ができますので、
Nginxのマウント先(/etc/pki/tls/certs/)に指定してください。

参考文献

nginx と PHP-FPM の仕組みをちゃんと理解しながら PHP の実行環境を構築する

調べなきゃ寝れない!と調べたら余計に寝れなくなったソケットの話

Dockerで構築したnginxをSSL化対応する

xdomain

次は、最低限のセキュリティ設定とKubernetesのことを調べます。