Dockerでいい感じにPHP(Laravel)のローカル開発環境を作る

# 概要
Dockerで一からLaravelの環境を作ってみたかったんです。
流れを理解できるように記事として残します。
構成はNginx + php-fpm + (MySQL, PostgreSQL, Redis)
この記事では扱いませんが、XdebugとPhpStormの連携と言ったことも後々言及する予定です。

設定は[Github](https://github.com/iga-yamaguchi/php-docker-sample)にあがっています。

## この記事で学べること
- DockerとDocker Composeを使用して一からDockerのPHP(Laravel)環境を作る方法

## この記事で触れないこと
- Nginxやphp-fpmの細かい設定ファイルについて
- 各種パフォーマンスチューニング

さっそく始めていきます。

# Nginx
まずはNginxのコンテナを準備。
Nginxの公式イメージをそのまま使います。
下記のようなdocker-compose.ymlを作成します。

```yaml:docker-compose.yml
version: '2'

services:
  nginx:
    image: nginx
    container_name: "laravel-nginx"
    ports:
      - "8080:80"
```

nginxのコンテナを起動します。

```shell-session
$ docker-compose build # image作成
$ docker-compose up -d # コンテナ起動
```

コンテナが立ち上がっているか確認します。

```shell-session
$ docker-compose ps
    Name                   Command              State          Ports        
----------------------------------------------------------------------------
laravel-nginx   nginx -g daemon off;            Up      0.0.0.0:8080->80/tcp

```

[http://localhost:8080](http://localhost:8080)でアクセスしてNginxの初期画面が表示されれば完了です。

![Screenshot from 2017-10-31 16-35-58.png](https://qiita-image-store.s3.amazonaws.com/0/64362/af3a47d3-4b6f-d53c-de38-c94d1c68eabc.png)

# PHP
phpディレクトリを作成し、その中にDockerfileを作成します。

```docker:php/Dockerfile
FROM php:fpm
```

ここでdocker-compose.ymlから直接イメージを指定してもいいですが、後々DockerfileをカスタマイズするのでDockerfileを作成しましょう。

servicesの中にphp用の記述を追加していきます。

```yaml:docker-compose.yml
services:
  # ...
  # ...
  php:
    build: ./php
    container_name: "laravel-php"
```

さっき行ったようにビルド、立ち上げ、プロセスを確認するとPHPのコンテナが立ち上がっているのがわかります。

```shell-session
$ docker-compose build
$ docker-compose up -d
$ docker-compose ps
    Name                   Command              State          Ports        
----------------------------------------------------------------------------
laravel-nginx   nginx -g daemon off;            Up      0.0.0.0:8080->80/tcp
laravel-php     docker-php-entrypoint php-fpm   Up      9000/tcp  
```

外部からコンテナのコマンドを実行してみましょう。

```shell-session
$ docker-compose exec php php -v
PHP 7.1.10 (cli) (built: Oct 10 2017 01:30:46) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
```
phpというサービスに対してコマンドを流し込んでいます。
ここでは`php -v`です。
これでPHP自体は立ち上がったことが確認できます。

# ファイル共有
NginxとPHPのコンテナにローカルのファイルを共有していきます。

### まずはNginxから
srcディレクトリを作成し、配下にindex.htmlを置きましょう。

```html:src/index.html
<p>Nginx HTML</p>
```
2つのディレクトリとファイルをマッピングします。

- srcディレクトリをNginxのコンテナの/srcディレクトリに
- Nginxの設定ファイルを上書き

まずはdocker-compose.ymlから書き換えます。

```yaml:docker-compose.yml
  nginx:
    image: nginx
    container_name: "laravel-nginx"
    ports:
      - "8080:80"
    volumes:
      - ./src:/src
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
```

volumesを追加します。
次にNginxの設定ファイルです。

```nginx:default.conf
server {
    index index.php index.html;
    root /src;
}
```
default.confは最低限だけ記述しておきます。

コンテナの再起動を行いましょう。

```shell-session
$ docker-compose down
$ docker-compose up -d
```

ここではdocker-compose.ymlの書き換えなのでbuildは必要ありません。

これでindex.htmlの内容が表示されるはずです。

![Screenshot from 2017-10-31 18-46-22.png](https://qiita-image-store.s3.amazonaws.com/0/64362/dd8b79c8-3930-f9e1-c985-639846fe02e7.png)

## 次にPHP
まずsrcディレクトリにPHPの情報を吐き出すinfo.phpを作成しておきましょう。

```php:info.php
<?php
phpinfo();
```

Nginxと同様にローカルのsrcディレクトリとコンテナ内の/srcをマッピングします。

```yaml:docker-compose.yml
  php:
    build: ./php
    container_name: "laravel-php"
    volumes:
      - ./src:/src
```

Nginxの設定にphp-fpmの設定を追加します。

```nginx
server {
    index index.php index.html;
    root /src;

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

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php: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はサービス名です。
composeはVersion 2からはコンテナ内ではデフォルトでサービス名＝ホスト名として解決することができるようになりました。
docker-composeのVersion1ではlinksの設定が必要だったのですが、Version2からは不要です。
https://docs.docker.com/compose/compose-file/compose-versioning/#version-2

これで[http://localhost:8080/info.php](http://localhost:8080/info.php)にアクセスしていつもの画面が出てくれば成功です。

![Screenshot from 2017-10-31 19-54-24.png](https://qiita-image-store.s3.amazonaws.com/0/64362/4f06ad03-3f72-d2f0-002e-ba4faa0ca292.png)

ここで一旦PHPのコンテナはおいておいて、他のデータベースなどのコンテナの作成をしていきます。

# 各種データベース
ざっとMySQLとPostgreSQL、Redisをコンテナとして起動するようにしておきます。

```yaml:docker-compose.yml
  mysql:
    image: mysql
    container_name: "laravel-mysql"
    restart: always
    environment:
      MYSQL_DATABASE: root
      MYSQL_ROOT_PASSWORD: root
    ports: 
      - 13306:3306

  postgres:
    restart: always
    image: postgres:alpine
    container_name: "laravel-postgres"
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: root
    ports:
      - 15432:5432

  redis:
    image: redis:alpine
    container_name: "laravel-redis"
    ports: 
      - 16379:6379
```

各コマンドで動作しているか確認しておきます。

```shell-session
$ mysql -h 0.0.0.0 --port 13306 -u root -p

$ psql -h 0.0.0.0 -U root --port 15432

$ redis-cli -h 0.0.0.0 -p 16379 
```

# composer
公式サイトのダウンロードの手順に従ってphpのイメージに組み込んであげましょう。
php/Dockerfileを書き換えます。

```docker:php/Dockerfile
FROM php:fpm

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
  && php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
  && php composer-setup.php \
  && php -r "unlink('composer-setup.php');" \
  && mv composer.phar /usr/local/bin/composer
```



```shell-session
$ docker-compose exec php composer --version
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Composer version 1.5.2 2017-09-11 16:59:25
```

一旦composerのインストールが成功し、コンテナからcomposerコマンドを実行すると上記のような表示になるかと思います。
Dockerコンテナではrootでコマンドが実行されるので下記のようにDockerfileにCOMPOSER_ALLOW_SUPERUSERを追加することによって消すことができますので追記しておきましょう。
https://getcomposer.org/doc/03-cli.md#composer-allow-superuser

```docker:php/Dockerfile
ENV COMPOSER_ALLOW_SUPERUSER 1
```

```shell-session
$ docker-compose exec php composer --version
Composer version 1.5.2 2017-09-11 16:59:25
```

また、composerの各パッケージのダウンロードにはZIP拡張が使用されるので、Dockerfileのcomposerのインストールの前にZIP拡張のインストール処理も追記しておきましょう。

```docker:php/Dockerfile
FROM php:fpm

RUN apt-get update \ 
    && apt-get install -y zlib1g-dev \
    && docker-php-ext-install zip

# 以下composerのインストール
```

zip extensionを入れておかないと下記のようなエラーが発生するので注意です。

```shell-session
root@0c36413d6c4a:/var/www/html# composer global require 'phpunit/phpunit'
Changed current directory to /root/.composer
Using version ^6.4 for phpunit/phpunit
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 28 installs, 0 updates, 0 removals
    Failed to download sebastian/version from dist: The zip extension and unzip command are both missing, skipping.
A php.ini file does not exist. You will have to create one.
    Now trying to download from source
  - Installing sebastian/version (2.0.1): Cloning 99732be0dd

Installation failed, deleting ./composer.json.

                                                                                                                                         
  [RuntimeException]                                                                                                                     
  Failed to clone https://github.com/sebastianbergmann/version.git, git was not found, check that it is installed and in your PATH env.  
  sh: 1: git: not found      

```

composerのグローバルパッケージがインストールされるディレクトリも変更しておきます。

```docker:php/Dockerfile
ENV COMPOSER_HOME /composer
```
更にグローバルにインストールするパッケージに関してはPATHも通しておきます。

```docker:php/Dockerfile
ENV PATH $PATH:/composer/vendor/bin
```

composerのインストールはこれで完了です。

# Laravel
ここでLaravelのインストールです。
インストーラーをインストールしておきます。
最後のWORKDIRの前に下記の記述を追加してあげましょう。

```docker:php/Dockerfile
RUN composer global require "laravel/installer" 
```

再度イメージのビルドを行います。
これで下記コマンドでLaravelをインストールすることができるようになります。

```shell-session
$ docker-compose exec php laravel new sample
```

先にイメージ作成時にプロジェクトを作成しても良いのですが、今回はそれぞれコンテナ上に自由に作成できるようにしてみました。
上記のコマンドでは/src/sampleというディレクトリにLaravelがインストールされます。
ですので、Nginxの設定ファイルのルートディレクトリもLaravelの公開ディレクトリのパスに書き換えてあげましょう。

```nginx:default.conf
root /src/sample/public;
```

Nginxの再起動(設定の再読み込み)はサービスをrestartすることでできます。

```shell-session
$ docker-compose exec nginx restart
```

これで[localhost:8080](http://localhost:8080/)にアクセスするとLaravelのデフォルト画面が表示されます。

![Screenshot from 2017-11-06 02-22-57.png](https://qiita-image-store.s3.amazonaws.com/0/64362/e1dbc8bd-b808-dec9-3550-ba7a24ea0937.png)


# PHPの拡張
先にLaravelをインストールしましたが、実はこの状態だとまだPDOが使えません。
PDO拡張が使用しているPHPのイメージには入っていないためです。

```shell-session
$ docker-compose exec php php ./sample/artisan migrate

                                                                                     
  [Illuminate\Database\QueryException]                                               
  could not find driver (SQL: select * from information_schema.tables where table_s  
  chema = homestead and table_name = migrations)                                     
                                                                                     

                         
  [PDOException]         
  could not find driver  
```
ですので、php/Dockerfileの最後のWORKDIRの前に以下の記述を追加してあげましょう。

```docker:php/Dockerfile
RUN apt-get update \
    && apt-get install -y libpq-dev \
    && docker-php-ext-install pdo_mysql pdo_pgsql
```


PostgreSQLの拡張ではlibpq-devをインストールしないとこのようなエラーが出るので注意です。

```shell-session
configure: error: Cannot find libpq-fe.h. Please specify correct PostgreSQL installation path
```

インストールが完了して先ほどのコマンドを実行すると、PDOExeptionの内容がドライバエラーから接続エラーに変わります。

```shell-session
$ docker-compose exec php php ./sample/artisan migrate          

                                                                                     
  [Illuminate\Database\QueryException]                                               
  SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.  
  tables where table_schema = homestead and table_name = migrations)                 
                                                                                     

                                             
  [PDOException]                             
  SQLSTATE[HY000] [2002] Connection refused                   
```

.envにDB情報を記述してあげます。
今回はMySQLを使用してみます。
docker-compose.ymlで指定したサービス名、データベース、パスワードと入力していきます。
HOSTはdocker-composeで立てたネットワーク内ではサービス名=ホスト名として各コンテナのIPを保管してくれるのでそのままサービス名を記述します。

```ini:.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=root
DB_USERNAME=root
DB_PASSWORD=root
```

これで再度コマンドを叩いてみましょう。

```shell-session
$ docker-compose exec php php ./sample/artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table
```

正常にマイグレートが完了しました。
これで最低限のLaravelの環境が構築できました。

ちなみにここでは動作確認しませんが、Redisはこんな感じです。

```docker:php/Dockerfile
RUN pecl install -o -f redis \
    &&  rm -rf /tmp/pear \
    &&  docker-php-ext-enable redis
```

# Node.js
Laravel MixやWebpackなどで使うことになるのでNode.jsをインストールします。
また最後のWORKDIRの前に以下を記述しましょう。

```docker:php/Dockerfile
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \ 
    && apt-get update \
    && apt-get install -y nodejs
```

```shell-session
$ docker-compose exec php node -v              
v6.11.5
```

# 完成
一通りイメージ作成、コンテナの使用ができるようになりました。
実際使ってみると便利でいいです、Docker。

何か間違っている点や、こうしたほうが良いというところがあればコメントお願いします。