Laravelの実行環境をDockerで構築する

# 概要
docker-composeを使ってLaravelの実行環境を構築します。
作成するコンテナは**PHP-FPM、Nginx、MySQL**の3つです。

# 環境
Docker：19.03　ホストOS：Ubuntu 18.04（Vagrant）
Laravel：6.1.0　PHP-FPM：7.3　PHP：7.3.10　 Nginx：1.17.4　MySQL：8.0
＊Dockerおよびdocker-composeはホストOS（Vagrant上のUbuntu）上にインストールされているものとします。

# ディレクトリ構成
docker
|－　docker-compose.yml
|－　laravel
|－　mysql　
|　　　|－　Dockerfile
|　　　|－　my.cnf
|　　　∟　sql
|　　　　　∟　init.sql
|－　nginx
|　　　|－　Dockerfile　
|　　　∟　　default.conf
∟　　php
　　　|－　Dockerfile　
　　　∟　　local.ini

# 構築手順
## １．Laravelとパッケージのインストール
初めに、ホストOS上にLaravelとパッケージをインストールします。コンテナにマウントする形でLaravelのファイルのコピーを行います。
### ・Laravelのインストール
最上位のdockerディレクトリ内で以下のコマンドを実行します。gitがない場合、先にインストールしてください。

```
git clone https://github.com/laravel/laravel.git laravel
```
dockerディレクトリにlaravelというディレクトリが作成され、Git HubからLaravelのプロジェクトがクローンされます。

### ・パッケージのインストール
作成されたlaravelディレクトリに移動して、コンテナから**composer install**を実行します。

```
cd laravel
docker run --rm --interactive --tty --volume $PWD:/app composer install
```

このコマンドは、Composerイメージからコンテナを一時的に作成し、Laravelに必要なパッケージをインストールします。

Laravelディレクトリをコンテナ内にマウントすることで、Composerコンテナが**composer.json**ファイルを読み込んで利用できるようになっています。コンテナは実行後に消滅しますが、ディレクトリをマウントしているためホストOS上にインストールしたパッケージが残ります。公式のComposerイメージで紹介されている使用法です。

[公式Composerイメージ](https://hub.docker.com/_/composer)

＊ComposerイメージがホストOS上にない場合、イメージがdocker hubよりpullされます。

**＊注意**：Composer実行中に以下のエラーが発生する場合があります。
``proc_open(): fork failed errors``
これはホストのメモリ不足によるものです。VMのメモリが1024MBだと発生するのでメモリを増やしましょう。

## ２．docker-compose.yml作成

続いて、以下のようなdocker-compose.ymlを作成します。

```docker-compose.yml

version: '3'

services:

  php:
    build: ./php
    image: app_php:7.3-fpm
    container_name: php
    restart: unless-stopped
    working_dir: /var/www
    volumes:
      - ./php/local.ini:/usr/local/etc/php/conf.d/local.ini
      - ./laravel:/var/www
    networks:
      - app-network

  nginx:
    build: ./nginx
    image: app_nginx:1.17.4
    container_name: nginx
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./laravel:/var/www
    networks:
      - app-network
    depends_on:
      - php

  mysql:
    build: ./mysql
    image: app_mysql:8.0
    container_name: mysql
    restart: always    
    ports:
      - 3306:3306    
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: pass
      MYSQL_USER: user
      MYSQL_PASSWORD: pass
      TZ: 'Asia/Tokyo'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - mysql-volume:/var/lib/mysql/
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql-volume:
    driver: local
```

### ・PHPコンテナ
**・image**
通常、imageはコンテナで使用するイメージを指定しますが、「build」を使ってDockerfileからコンテナを起動する場合、docker-composeで作成するイメージにイメージ名とタグ名を付けることができます。ここでは「app_php」というイメージ名、「7.3-fpm」というタグ名を付与しています。

**・volumes**
./php/local.ini:/usr/local/etc/php/conf.d/local.ini
ホスト上のphpディレクトリ内の**local.ini**を、コンテナ内の「/usr/local/etc/php/conf.d/local.ini」にコピーします。こうすることで、コンテナ内のデフォルトのphp.iniを上書きすることができます。php.iniファイル自体は上書きされませんが、phpinfo()で情報を出力すると上書きされていることを確認できます。local.iniは後に作成します。

./laravel:/var/www
ホスト上でインストールしたlaravelディレクトリを、コンテナの「**/var/www**」とマウントします。こうすることで、ドキュメントルートを「/var/www/public」とすることができます。
開発環境であれば問題ありませんが、データをホストOSにマウントするのは推奨されていないため、本番で使う時はDockerボリュームを作成してマウントすることを検討しましょう。

**＊注意**
セキュリティを向上させるため、ディレクトリをリードオンリーでマウントすることが推奨されていますが、Laravelではファイルシステムをリードオンリーにするとエラーが発生します。

./laravel:/var/www:<font color="Red">ro</font>（リードオンリー）　→　エラーが発生します。

### ・Nginxコンテナ
**・volumes**
./laravel:/var/www
Nginxコンテナでもlaravelディレクトリをマウントします。こうすることで、Nginxが受け付けたリクエストをLaravelで返すことができます。

### ・MySQLコンテナ
**・command**
--default-authentication-plugin=mysql_native_password
上記のコマンドでMySQLの認証方式を変更しています。MySQLはバージョン8.0から認証方式を変更しており、「caching_sha2_password」がデフォルトとなっています。PHPのMySQL接続ライブラリが未対応のため、認証方式を「**mysql_native_password**」に戻します。

**・volumes**
./mysql/sql:/docker-entrypoint-initdb.d
コンテナ内の「docker-entrypoint-initdb.d」以下にSQLを配置することで、初期設定などを行うことができます。

mysql-volume:/var/lib/mysql/
MySQLのデータをDockerのボリュームにマウントして永続化します。Dockerのボリュームはコンテナを削除しても残ります。そのため、コンテナに変更を加える場合、volumeを削除しなければenvironment等の設定を変更しても反映されないので気を付けましょう。

**・environment**
任意のユーザーとパスワードを設定してください。

**＊注意**
Dockerにおける永続化データの保存には、データ専用コンテナを作成する方法がありますが、docker-composeのバージョン３からvolumes_fromオプションが削除されているため使用することができません。

## ３．Dockerfileの作成
次に、各コンテナを生成するDockerfileを作成します。
###・PHPコンテナのDockerfile
phpディレクトリ内に以下のようなDockerfileを作成します。

```dockerfile:Dockerfile
FROM php:7.3-fpm

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    default-mysql-client \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    libmcrypt-dev \
    libmemcached-dev \
    libzip-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    curl

# Clear cache
RUN apt-get clean && rm -rf /var/lib/apt/lists/*

# Install extensions
RUN docker-php-ext-install pdo_mysql exif bcmath && \
    docker-php-ext-configure zip --with-libzip && \
    docker-php-ext-install zip && \
    docker-php-ext-configure gd \
    --with-gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-png-dir=/usr/include/ && \
    docker-php-ext-install gd

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Add user for laravel application
RUN groupadd -g 1000 php && \
    useradd -u 1000 -ms /bin/bash -g php laravel

# Change current user to laravel
USER laravel

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]

```
**・ベースイメージ**
公式イメージである「**php:7.3-fpm**」をベースイメージとしています。php:7.3-fpmのベースイメージは「**debian:buster-slim**」となっています。LaravelのプログラミングはPHPコンテナに入って行いますが、debianの上で行っていることを頭に入れておきましょう。

**・ライブラリ、エクステンションのインストール**
PHPおよびLaravelの実行に必要なライブラリ、エクステンションをインストールしています。

default-mysql-client
debian:buster-slimはmariadbをデフォルトとしており、mariadb-clientが使用されます。そのため、mysql-clientはインストールできません。その代わり、mysqlを利用するために「default-mysql-client」というものが用意されているので、そちらをインストールしています。

bcmath
Laravelを実行するためのサーバー要件としていくつかのエクステンションが必要ですが、多くはPHP本体に含まれています。ただし、bcmathが含まれていないため、別途「docker-php-ext-install」でインストールしています。

[Laravelのサーバー要件](https://laravel.com/docs/5.8/installation)

**・ユーザーの作成**
rootユーザーでDockerを操作するのはセキュリティ上好ましくないため、laravelというユーザーを作成しています。

### ・NginxコンテナのDockerfile
nginxディレクトリ内に以下のDokcerfileを作成します。

```dockerfile:Dockerfile
FROM nginx:1.17.4
COPY default.conf /etc/nginx/conf.d/default.conf
```

イメージはdocker-compose.ymlに指定することもできますが、docker-composeで作成するイメージにイメージ名を付与することができるため、docker-composeのbuildからイメージを作成するようにしています。また、後に作成する**default.conf**をコンテナ内にコピーしています。

### ・MySQLコンテナのDockerfile
mysqlディレクトリ内に以下のDokcerfileを作成します。

```dockerfile:Dockerfile
FROM mysql:8.0
COPY my.cnf /etc/mysql/conf.d/my.cnf
RUN chmod 644 /etc/mysql/conf.d/my.cnf
CMD ["mysqld"]
EXPOSE 3306
```

コンテナ内の「/etc/mysql/conf.d/**my.cnf**」に設定ファイルをコピーすることで、デフォルトのmy.cnfを上書きすることができます。
Windowsを使用している場合、VirtualBox経由でマウントしているディレクトリ/ファイルはすべてパーミッションが777 になります。MySQLは777の.cnfファイルを読み込みません。そのため、ファイルのパーミッションを変更しています。

## ４．設定ファイルの作成
次に、各コンテナで使用する設定ファイルを作成します。
###・PHPの設定ファイル
phpディレクトリ内に**local.ini**という設定ファイルを作成します。

```local.ini
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,
memory_limit = 256M
post_max_size = 100M
upload_max_filesize = 100M
date.timezone = "Asia/Tokyo"
mysqlnd.collect_memory_statistics = Off
mbstring.language = Japanese
mbstring.internal_encoding = UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off
mbstring.detect_order = auto
```

デフォルトのphp.iniが上書きされるので、変更が必要な項目のみ記載します。設定はLaravelの公式の開発環境であるHomesteadやLaradockを参考にしています。Homesteadではセキュリティの関係上、pcntl系の関数を使用しないようにしているようです。「memory_limit」や「post_max_size」などは、必要に応じて適宜変更してください。

### ・Nginxの設定ファイル
nginxディレクトリ内に**default.conf**という設定ファイルを作成します。

```default.conf
server {
    listen 80;
    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.html index.htm index.php;

    charset utf-8;

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```

この設定はLaravelの公式ドキュメントの設定です。公式の設定では「fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;」となっているところを「**fastcgi_pass php:9000;**」と変更しています。コンテナ間で通信が行えるようにするため、php7.2-fpm.sock（Unixドメインソケット）を使うのではなく、PHPコンテナの9000番のポートを指定（php:9000）してTCP通信を行います。

[Laravel公式ドキュメントのNginx設定](https://laravel.com/docs/6.x/deployment)

### ・MySQLの設定ファイル
mysqlディレクトリ内に**my.conf**という設定ファイルを作成します。

```my.conf
[mysql]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4
skip-character-set-client-handshake

[mysqldump]
default-character-set=utf8mb4

[client]
default-character-set=utf8mb4
```
こちらもデフォルトのmy.cnfを上書きできるため、変更する項目のみ記載します。日本語を使えるようにする設定です。

また、mysql/sqlディレクトリ内に以下の**init.sql**というファイルを作成します。

```init.sql
GRANT ALL PRIVILEGES ON *.* to user@"%";
FLUSH PRIVILEGES ;
```

こちらはコンテナが起動する際に実行されるコマンドです。作成したユーザーに権限を与えます。

## ５．コンテナの起動とMySQLの接続確認

### ・コンテナの起動

docker-compose.yml、Dockerfile、設定ファイルを作成した後、コンテナを起動します。
コンテナを起動する前に、Laravelの設定ファイルを「**.env**」として作成します。その後、docker-compose.ymlファイルのあるdockerディレクトリに移動します。

```
cp .env.example .env
cd ..
```
docker-composeでコンテナを起動します。以下のコマンドを実行すると、各コンテナのイメージも作成されます。途中で「debconf: delaying package configuration, since apt-utils is not installed」という警告が出ますが、単なる警告なので問題ありません。すべての処理が完了するまで数分かかります。

```
docker-compose up -d
```
処理が完了したら、以下のコマンドで確認します。

```
docker-compose ps
```

以下のようにコンテナが立ち上がっていたら成功です。

```
Name               Command               State                    Ports
-----------------------------------------------------------------------------------------
mysql   docker-entrypoint.sh --def ...   Up      0.0.0.0:3306->3306/tcp, 33060/tcp
nginx   nginx -g daemon off;             Up      0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
php     docker-php-entrypoint php-fpm    Up      9000/tcp
```

次に、Laravelのアプリケーションキーを設定します。PHPコンテナ内でphp artisanコマンドを実行します。

```
docker-compose exec php php artisan key:generate
```

Laravelの設定ファイル.envの「APP_KEY」にキーが設定されます。次のコマンドで設定をキャッシュに反映させます。

```
docker-compose exec php php artisan config:cache
```

ホストOSのIPアドレス（Vagrantfileに設定したIP）にブラウザからアクセスして以下の画面が表示されたら正常に動作しています。

![laravel.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283325/d058c571-2d8b-e7fc-88c3-e459a2b48571.png)

### ・MySQLの接続確認
最初に、Laravelのデータベースの設定を行います。**.env**ファイルを次のように設定します。

```
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=user
DB_PASSWORD=pass
```
DB_HOSTにはコンテナ名であるmysqlと記述します。続いて、config/**database.php**ファイルを設定します。「'strict' => false」は、「mysql_native_password」の認証方式を利用するための設定です。

```
'host' => env('DB_HOST', 'mysql'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'user'),
'password' => env('DB_PASSWORD', 'pass'),
'strict' => false,
```

以下のコマンドで設定をキャッシュに反映させます。

```
docker-compose exec php php artisan config:cache
```

PHPコンテナに入り、接続の確認をしていきます。

```
docker exec -it php /bin/bash
```

コンテナに入ったら、以下のコマンドでデータベースのマイグレーションを実行します。

```
php artisan migrate
```

続いてtinkerを起動します。

```
php artisan tinker
```

次のコマンドを実行して、マイグレーションを行ったデータが返ってきたらMySQLとの接続が成功しています。

```
\DB::table('migrations')->get();
```

「Ctrl + C」を押してtinkerを終了し、ロールバックを行います。

```
php artisan migrate:rollback
```

なお、PHPコンテナにログインすると、Dokcerfileで定義したユーザーとしてログインします。rootユーザーではないため、コマンドの実行が制限されています。rootとしてログインしたい場合は、以下のようにuオプションに0を指定します。

```
docker exec -it -u 0 php /bin/bash
```

以上でDockerによるLaravelの実行環境の構築は終了です。

# 参考にした記事

今回の環境構築は次の記事を参考にさせて頂きました。

[How To Set Up Laravel, Nginx, and MySQL with Docker Compose](https://www.digitalocean.com/community/tutorials/how-to-set-up-laravel-nginx-and-mysql-with-docker-compose)

[Dockerを使ってLaravel開発環境構築](https://qiita.com/A-Kira/items/1c55ef689c0f91420e81)

Dockerは非常に奥が深く、習得するのに時間がかかるツールと思いますが、Dockerfileやdocker-compose.ymlを自分で書くことで理解を深めていくことができます。