DockerでLaravelの開発環境を構築してみよう（＆一緒に使うと便利なDockerイメージを紹介）

こんにちは！　ニアです。

最近、Laravelを使ったWebアプリ開発の勉強しているので、健忘録として、Docker上での開発環境の構築方法を記事に書きました。

さらに、Docker上のLaravelの開発環境と一緒に使うと便利なDockerイメージをいくつか紹介します。

# 1. docker-compose用ファイル作成

ここでは、**php-fpm** 7.3 + **nginx** + **MySQL** 5.7 + **Redis**の構成で構築します。

> ※執筆者のホスト環境
>
> * macOS 10.14 Mojave
> * Docker Desktop for Mac (2.1.0.1)
>    * docker (19.03.1)
>    * docker-compose (1.24.1)

## 1.1. ディレクトリ構成

`docker`フォルダにdocker-composeのファイル、`laravel`フォルダにLaravelのソースファイルが入る構成とします。

```tree:ディレクトリ構成
.
├── docker
│   ├── docker-compose.yml
│   ├── .env
│   ├── mysql
│   │   └── init
│   │       └── grant.sh
│   ├── nginx
│   │   ├── conf
│   │   │   └── laravel.conf
│   │   └── logs
│   └── php-fpm
│       └── Dockerfile
└── laravel
```

## 1.2. php-fpmコンテナ

### `Dockerfile`

Debian 10 (buster)をベースに、**php-fpm**と**composer**をインストールしたコンテナイメージを作成します。

```dockerfile:Dockerfile
FROM debian:buster-slim

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        apt-transport-https \
        curl \
        unzip \
        zlib1g-dev \
        php7.3 \
        php7.3-fpm \
            php7.3-mysql \
            php7.3-xml \
            php7.3-mbstring \
            php-oauth \
            php7.3-zip \
    && rm -rf /var/lib/apt/lists/* \
    # php-fpmのListenをソケットから9000/tcpに変更します。
    && sed -iE '/^listen/s/\/run\/php\/php7\.3-fpm\.sock/9000/g' /etc/php/7.3/fpm/pool.d/www.conf \
    && mkdir -p /var/www/html \
    && mkdir -p /run/php \
    && curl -sS https://getcomposer.org/installer | php \
    && mv composer.phar /usr/local/bin/composer

# composerをrootで実行できるようにします。
ENV COMPOSER_ALLOW_SUPERUSER 1

# Laravelのソースコードのフォルダを作業ディレクトリにします。
WORKDIR /var/www/html
EXPOSE 9000
# `docker-compose up`実行時、`/usr/sbin/php-fpm7.3 -F`で起動するようにします。
CMD [ "/usr/sbin/php-fpm7.3", "-F" ]
```

> ※DebianでPHP 7.3をインストールする場合 
>
> * Debian 10（buster）のデフォルトリポジトリでは、**PHP 7.3**なので、追加のリポジトリは不要です。
> * Debian 9（stretch）以前のデフォルトリポジトリでは、**PHP 7.0**以前なので、[DEB.SURY.ORG](https://deb.sury.org/)のリポジトリを追加する必要があります。

## 1.3. MySQLコンテナ

### `grant.sh`

後述の`docker-compose.yml`内のMySQLコンテナの環境変数で指定する、MySQLユーザー「`MYSQL_USER`」に権限を付与するスクリプトを作成します。

```bash:grant.sh
(
mysql -uroot -p$MYSQL_ROOT_PASSWORD <<EOF
    GRANT ALL ON *.* TO '$MYSQL_USER'@'%';
EOF
)
```

## 1.4. nginxコンテナ

### `laravel.conf`

Laravel用のnginxの設定ファイルを作成します。

```nginx:laravel.conf
log_format vhost '$host $remote_addr - $remote_user [$time_local] '
                '"$request" $status $body_bytes_sent '
                '"$http_referer" "$http_user_agent"';
proxy_set_header    Host                $http_host;
proxy_set_header    X-Real-IP           $remote_addr;
proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;

server {
    server_name     localhost;
    listen          80;
    # アクセスログとエラーログ
    access_log      /var/log/nginx/laravel_access.log vhost;
    error_log       /var/log/nginx/laravel_error.log;
    # ルートディレクトリは、{Laravelのルート}/publicです。
    root            /var/www/html/public;
    index           index.php index.html;

    # URL内の'index.php'を取り除きます。
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHPファイルへのリクエスト時、php-fpmコンテナに送信します。
    location ~ .php$ {
        fastcgi_pass php-fpm:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
        include fastcgi_params;
    }
}
```

## 1.5. `docker-compose.yml`

```yaml:docker-compose.yml
version: "3"
services:
  # コンテナ
  # php-fpm
  php-fpm:
    image: laravel-php-fpm:7
    build: ./php-fpm/
    depends_on:
      - mysql
      - redis
    volumes:
      # Laravelのソースファイル
      - ../laravel:/var/www/html

  # nginx
  nginx:
    image: nginx:mainline-alpine
    depends_on:
      - php-fpm
    ports:
      - 80:80
    volumes:
      # nginxの設定ファイル
      - ./nginx/conf/laravel.conf:/etc/nginx/conf.d/default.conf:ro
      # Laravelのソースファイル
      - ../laravel:/var/www/html
      # nginxのログ
      - ./nginx/logs:/var/log/nginx

  # MySQL
  mysql:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: password     # MySQLのrootパスワード
      # 以下に指定したDB名やユーザーは、MySQLコンテナの新規作成時に作成されます。
      MYSQL_DATABASE: laravel           # DB名
      MYSQL_USER: laravel_user          # ユーザー名
      MYSQL_PASSWORD: laravel_pass      # パスワード
    ports:
      - 3306:3306
    volumes:
      # このマウント先に入れたシェルスクリプトは、コンテナ作成後自動的に実行されます。
      - ./mysql/init:/docker-entrypoint-initdb.d:ro
      # MySQLのデータファイル
      - mysql-data:/var/lib/mysql

  # Redis
  redis:
    image: redis:alpine
    volumes:
      # Redisのデータファイル
      - redis-data:/data

# ネットワークはデフォルトのものを使用します。
#networks:
#  default:
#    driver: bridge

# データ用のボリューム
volumes: 
  mysql-data:
    driver: local
  redis-data:
    driver: local
```

### `.env`ファイル

`docker-compose`コマンドで使用する環境変数`COMPOSE_PROJECT_NAME`にプロジェクト名を設定します。
これを設定すると、`docker-compose`コマンドを実行した時に`COMPOSE_PROJECT_NAME`を使って、コンテナやネットワーク、ボリュームに名前を付けられます。

* コンテナ名: `${COMPOSE_PROJECT_NAME}_${サービス名}_${連番}`
* ネットワーク名: `${COMPOSE_PROJECT_NAME}_${networks直下に定義した識別子}`
* ボリューム名: `${COMPOSE_PROJECT_NAME}_${volumes直下に定義した識別子}`

```bash:.env
COMPOSE_PROJECT_NAME=laravel
```

# 2. Laravelのプロジェクトを新規作成

php-fpmコンテナから`composer create-project`コマンドを実行して、Laravelのプロジェクトを新規作成します。

```bash
# 先ほど作成したdocker-compose.ymlがあるディレクトリで実行します。
# （php-fpmにリンクしたMySQLとRedisコンテナはここでは不要なので、--no-depsオプションを付けます。）
$ docker-compose run --rm --no-deps php-fpm composer create-project laravel/laravel --prefer-dist .
# ※ネットワーク及び、MySQLとRedisコンテナのボリュームは、ここで作成されます。
Creating network "laravel_default" with driver "bridge"
Creating volume "laravel_mysql-data" with local driver
Creating volume "laravel_redis-data" with local driver

# 以降、composerコマンドの実行結果が出力されます。
```

さらにphp-fpmコンテナから、`composer require predis/predis`コマンドを実行して、**predis/predis**をインストールします。

```bash
docker-compose run --rm --no-deps php-fpm composer require predis/predis
```

## 2.1. `laravel/.env`を編集

Laravelのプロジェクトを作成したら、`laravel/.env`を編集し、データベースとRedisの接続情報を設定します。

```bash:laravel/.env
# MySQL
DB_CONNECTION=mysql
DB_HOST=mysql                   # MySQLコンテナのサービス名を指定します。
DB_PORT=3306
DB_DATABASE=laravel             # MySQLコンテナの環境変数「MYSQL_DATABASE」の値を指定します。
DB_USERNAME=laravel_user        # MySQLコンテナの環境変数「MYSQL_USER」の値を指定します。
DB_PASSWORD=laravel_pass        # MySQLコンテナの環境変数「MYSQL_PASSWORD」の値を指定します。
```

```bash:laravel/.env
# Redis
REDIS_HOST=redis                # Redisコンテナのサービス名を指定します。
REDIS_PASSWORD=null
REDIS_PORT=6379
```

# 3. コンテナの起動

`docker-compose up -d`コマンドでコンテナを作成・起動します。

```bash
$ docker-compose up -d
Creating laravel_mysql_1 ... done
Creating laravel_redis_1 ... done
Creating laravel_php-fpm_1 ... done
Creating laravel_nginx_1   ... done

# コンテナが起動しているか確認します。
$ docker-compose ps
      Name                     Command               State                 Ports
----------------------------------------------------------------------------------------------
laravel_mysql_1     docker-entrypoint.sh mysqld      Up      0.0.0.0:3306->3306/tcp, 33060/tcp
laravel_nginx_1     nginx -g daemon off;             Up      0.0.0.0:80->80/tcp
laravel_php-fpm_1   /usr/sbin/php-fpm7.3 -F          Up      9000/tcp
laravel_redis_1     docker-entrypoint.sh redis ...   Up      6379/tcp
```

これで、ブラウザから http://localhost にアクセスすると、Laravelのページが表示されます。

![laravel](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/38133724-dfd8-5739-13aa-a5f53e208f19.png)


## 3.3. データベースのMigration

php-fpmコンテナから`php artisan migrate`コマンドを実行します。

```bash
$ docker-compose exec php-fpm php artisan migrate
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (0.04 seconds)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (0.02 seconds)

# テーブルが作成されたことを確認
$ docker-compose exec mysql mysql -ularavel_user -ularavel_pass -Dlaravel -e 'show tables;'
+-------------------+
| Tables_in_laravel |
+-------------------+
| migrations        |
| password_resets   |
| users             |
+-------------------+
```

以上で、Laravelの開発環境を構築は完了です。お疲れさまでした！

> 上記で作成した、docker-composeのファイルは、GitHubに置いてあります。
> https://github.com/Nia-TN1012/docker-laravel
> 
> 注: 使用する時は、laravelフォルダ内の`.gitkeep`を削除してください。laravelフォルダが空でないと、前述の`composer create-project`コマンドが失敗する原因になります。
> 
> またMacの場合、Finderから削除すると、その際に`_.DS_Store`が作成されてしまう場合があるので、ターミナルから`rm`コマンドで削除するとよいでしょう。
>
> ```bash
> $ rm laravel/.gitkeep
> ```

# 4. Laravelの開発環境と一緒に使うと便利なDockerイメージ

ここでは、以下の4つのDockerイメージを紹介していきます。

* **MailCatcher**: （[公式サイト](https://nodejs.org/ja/) / [Docker Hub](https://hub.docker.com/r/schickling/mailcatcher/)）（Ruby製の簡易SMTPサーバー）
* **Node.js**: （[公式サイト](https://mailcatcher.me/) / [Docker Hub](https://hub.docker.com/_/node/)）（Laravel Mixでのビルド用）
* **MinIO**: （[公式サイト](https://min.io/) / [Docker Hub](https://hub.docker.com/r/minio/minio/) / [GitHub](https://github.com/minio/minio)）（S3互換のオブジェクトストレージサーバー）
* **ElasticMQ**: （[公式サイト](https://softwaremill.com/open-source/) / [Docker Hub](https://hub.docker.com/r/softwaremill/elasticmq/) / [GitHub](https://github.com/softwaremill/elasticmq)）（SQS互換のメッセージキュー）（※別の記事で紹介します。）

## 4.1. MailCatcher

MailCatcherイメージを使うと、Laravelから送信したメールをブラウザで確認することができます。

### `docker-compose.yml`に追加

```yaml:docker-compose.yml
# services:
  # MailCatcher
  mailcatcher:
    image: schickling/mailcatcher
    ports:
      - 1080:1080
```

```bash
$ docker-compose up -d
Creating laravel_mailcatcher_1 ... done
```

http://localhost:1080 にアクセスすると、MailCatcherのメール送信トレイが表示されます。

![mailcatcher](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/02f367f3-f3f5-02c6-23e9-f58e6a5c3003.png)

### Laravelからメールを送信する

Laravelで標準実装されているユーザー認証機能では、パスワードリセットのリクエスト時にメールが送信される仕様なので、MailCatcherを使ってシミュレーションしてみましょう。

まず、`laravel/.env`を編集します。

```bash:laravel/.env
# Mail
MAIL_DRIVER=smtp
MAIL_HOST=mailcatcher           # MailCatcherコンテナのサービス名を指定します。
MAIL_PORT=1025                  # MailCatcherへの送信ポートは1025番です。
MAIL_USERNAME=null              # ユーザー名や暗号化設定はnullのままでOKです。
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
```

次に、php-fpmコンテナから`php artisan make:auth`コマンドを実行し、Laravelのユーザー認証機能を追加します。

```bash
$ docker-compose exec php-fpm php artisan make:auth
Authentication scaffolding generated successfully.
```

> Laravel 6では、`artisan make:auth`コマンドがないので、代わりにcomposerコマンドで`laravel/ui`をインストールし、`artisan ui:auth`コマンドを実行します。
>
> ```
> $ docker-compose exec php-fpm composer require laravel/ui
> $ docker-compose exec php-fpm php artisan ui:auth
> Authentication scaffolding generated successfully.
> ```

http://localhost/register にアクセスして、ユーザー登録をします。

![laravel_user_register](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/ee9ec0c6-6df3-fa89-bf08-501b46412053.png)

ユーザー登録をしたら、一旦ログアウトし、http://localhost/login にアクセスします。

![laravel_user_logout](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/ef812eb2-ed5a-35db-2e24-4c6405853e47.png)

「Forgot Your Password」のリンクを押します。

![laravel_user_login](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/754efab4-51ed-cda6-ed7e-73c1022f8ac0.png)

ユーザー登録時のメールアドレスを入力して、パスワードリセットのリクエストを送信します。

![laravel_pass_reset_req](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/03f53fac-7436-9a68-5912-a1068da739f2.png)

パスワード再設定メールがLaravelからMailCatcherに送信されるので、http://localhost:1080 を開き、メール内のパスワード再設定ボタンを押します。

![pass_reset_mail](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/828d7969-420f-5f07-83f5-7cc5fdcca5fd.png)

新しいパスワードを入力します。

![laravel_pass_reset](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/3fd28073-00f4-9e50-a06f-5fc4d9608a07.png)

これで、パスワードリセットの完了です。めでたし、めでたし。。。

![laravel_pass_reset_end](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/be7935d4-fe9f-16e7-7cd0-307b8c68356f.png)


## 4.2. Node.js

Laravelには、フロントエンドのアセットファイル（CSSやJavaScriptなど）をビルドする**Laravel Mix**（webpackベース）が同梱されています。

ホスト環境にNode.jsがインストールされていれば、それを使ってビルドすることができますが、nodeイメージのコンテナを使うことで、ホスト環境側のバージョンに依存せずにビルドできたり、Laravelソースファイルが入った名前付きボリュームをマウントしてビルドしたりすることができます。

ここでは、Laravelのアセットファイルの変更を監視し、自動的に再ビルドできるnodeコンテナを立てます。

### `docker-compose.yml`に追加

```yaml:docker-compose.yml
# services:
  # Node.js
  node:
    image: node:lts-alpine
    working_dir: /opt/laravel                # ワーキングディレクトリをLaravelのルートディレクトリにします。
    command: ["npm", "run", "watch-poll"]    # `docker-compose up -d`実行時のコマンドを`npm run watch-poll`にします。
    volumes:
      # npmのキャッシュファイル
      - npm-cache:/root/.mpm        
      # Laravelのソースファイル
      - ../laravel:/opt/laravel

# volumes:
  npm-cache:
    driver: local
```

### `webpack.mix.js`の編集

`laravel/webpack.mix.js`に以下のオプションを追加します。

```js:laravel/webpack.mix.js
mix.webpackConfig({
    watchOptions: {
        poll: 1000,
        // node_modulesフォルダ内を監視の対象外にします。
        // （このオプションを忘れると、nodeコンテナがCPUを爆食いします。。。）
        ignored: /node_modules/
    }
});
```


### nodeパッケージのインストール

nodeコンテナから`npm install`コマンドを実行します。

```bash
$ docker-compose run --rm node npm install --no-optional
```

### アセットファイルの変更を監視

nodeコンテナを起動すると、アセットファイルがビルドされ、その後はアセットファイル変更を監視するようになります。

```bash
$ docker-compose up -d
Creating laravel_node_1 ... done
```

ビルドの結果を見るには、nodeコンテナのログを見ます。

```bash
$ docker-compose logs node
Attaching to laravel_node_1

# 中略

node_1    |        Asset      Size   Chunks             Chunk Names
node_1    | /css/app.css   173 KiB  /js/app  [emitted]  /js/app
node_1    |   /js/app.js  1.38 MiB  /js/app  [emitted]  /js/app
```

### アセットファイルのビルド（＋圧縮）

nodeコンテナから`npm run dev` or `npm run prod`コマンドを実行すると、アセットファイルがビルド（`prod`の場合、さらに圧縮）されます。

```bash
# アセットのビルド
$ docker-compose run --rm node npm run dev

# アセットのビルド
$ docker-compose run --rm node npm run prod
```

## 4.3. MinIO

MinIOイメージを使って、Laravelからオブジェクトストレージにファイルをアップロード・ダウンロードすることができます。

### `docker-compose.yml`に追加

```yaml:docker-compose.yml
# services:
  # MinIO
  minio:
    image: minio/minio
    environment: 
      MINIO_ACCESS_KEY: LARAVEL_MINIO_ACCESS_KEY    # アクセスキー
      MINIO_SECRET_KEY: laravel_mino_hogefuga       # シークレットキー
    ports: 
      - 9000:9000
    command: server /data
    volumes:
      - minio-data:/data

# volumes: 
  minio-data:
    driver: local
```

```bash
docker-compose up -d
Creating volume "laravel_minio-data" with local driver
Creating laravel_minio_1 ... done
```

http://localhost:9000 にアクセスすると、MinIOのログインページが開くので、環境変数に指定したアクセスキーとシークレットキーを入力してログインします。

![minio](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/b3b81757-171e-1323-4b3e-0f426953f337.png)

### バケットの作成

右下の「+」ボタンから「Create bucket」ボタンを押します。

![minio_create_buket](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/71f20add-a3b2-504a-ec73-b6b6c9d01daf.png)

バケット名を入力し、Enterキーを押して、バケットを作成します。

![minio_create_buket_2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/6c02f706-b4fb-0c01-f44a-49b6f4ac207f.png)

### LaravelからMinIOにアクセスする

php-fpmコンテナから`composer require`コマンドを実行し、**league/flysystem-aws-s3-v3**をインストールします。

```bash
$ docker-compose exec php-fpm composer require league/flysystem-aws-s3-v3
```

`laravel/config/filesystems.php`の'disk'内に以下を追加します。

```php:laravel/config/filesystems.php
'disk' = [

    // 中略

    'minio' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION'),
        'bucket' => env('AWS_BUCKET'),
        'url' => env('AWS_URL'),
        'endpoint' => env('MINIO_ENDPOINT'),
        'use_path_style_endpoint' => true,
    ],
]
```

`laravel/.env`を編集します。

```bash:laravel/.env
AWS_ACCESS_KEY_ID=LARAVEL_MINIO_ACCESS_KEY    # minioコンテナの環境変数「MINIO_ACCESS_KEY」の値を指定します。
AWS_SECRET_ACCESS_KEY=laravel_mino_hogefuga   # minioコンテナの環境変数「MINIO_SECRET_KEY」の値を指定します。
AWS_DEFAULT_REGION=us-east-1                  # 使用しません。
AWS_BUCKET=laravel                            # 先ほど作成した、minioコンテナ内のバケット名を指定します。
MINIO_ENDPOINT=http://minio:9000              # minioコンテナのエンドポイントをセットします。（http://{MinIOコンテナのサービス名}:9000）
FILESYSTEM_CLOUD=minio                        # ファイルシステムは'minio'を指定します。（ちなみに省略すると、's3'が指定されます。）
```

Laravelからの疎通確認には、php-fpmコンテナから`php artisan tinker`（Laravel版のインタラクティブシェル（REPL））を使って確認します。

```bash
$ docker-compose exec php-fpm php artisan tinker
```

```php
// '{ "hello": "world" }'を'hello.json'として、MinIOコンテナにアップロード
>>> Storage::cloud()->put( 'hello.json', '{ "hello": "world" }' );
=> true
// MinIOコンテナから'hello.json'をダウンロード
>>> Storage::cloud()->get( 'hello.json' );
=> "{ "hello": "world" }"

// exitで終了します。
>> exit
Exit:  Goodbye
```

MinIOコンテナのlaravelバケットを確認すると、先ほどアップロードしたファイルがリストに表示されています。

![minio_upload](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78881/16c7c92f-c841-ea77-25cd-084070aa180b.png)

## 4.4. ElasticMQ



[LaravelをElasticMQ（Amazon SQS互換）と連携してみる](https://qiita.com/nia_tn1012/items/1bd60b1a3900a2b52939)

# 参考にしたサイト

* [Laravelの開発環境をDockerを使って構築する](https://qiita.com/ucan-lab/items/17c806973e69792ada99)
* [【Docker Compose】container_name ではなくて COMPOSE_PROJECT_NAME 変数を使うと良さそう](https://qiita.com/okashoi/items/1ddf3724ad5c166e417b)
* [MailCatcherでメール送信をテスト](https://qiita.com/pocari/items/de0436c39ffc65647cf0)
* [アセットのコンパイル(Mix) 5.8 Laravel](https://readouble.com/laravel/5.8/ja/mix.html)
* [Minio: An Open-Source S3 Compliant Storage Service | Laravel News](https://laravel-news.com/minio-s3-compliant-storage?utm_medium=feed&utm_source=facebook.com&utm_campaign=Feed%3A+laravelnews)
* [Running make:auth in Laravel 6 | Laravel News](https://laravel-news.com/running-make-auth-in-laravel-6)