Laravel + PHP7.3 + MySQL5.7の環境をDocker(with Alpine Linux)で構築した(コード/解説付き)

# 概要
こんにちは。Firebaseに訓練された高専卒Webエンジニアの @mejileben です。

Dockerをずっと毛嫌いしていたので、ガッツリ環境構築にチャレンジしてみました。
勉強も兼ねてなので、Laradockは使いません。
前提として、本番運用ではなく個人開発環境としての運用を想定します。
また、複数人でチーム開発することも考えて、設定ファイル等を共有/理解しやすい形にしておければ尚良と思っています。

# 設定内容
## ディレクトリ構成
```shell
.
├── Dockerfile_nginx
├── Dockerfile_php
├── docker-compose.yml
├── etc
│   ├── logs
│   │   └── nginx
│   │       ├── access.log
│   │       └── error.log
│   ├── mysql
│   │   ├── env
│   │   │   └── setting.env
│   │   └── mysql_conf
│   ├── nginx
│   │   └── conf.d
│   │       └── default.conf
│   └── php
│       ├── env
│       │   └── setting.env
│       └── php.ini
├── laravel
└── mnt
    └── mysql
```

以下、```docker-compose.yml```から順に、NginxやMySQLも含めた各設定ファイルの内容を提示、解説していきます。

## docker-compose.yml
```yml:docker-compose.yml
version: "3"

services:
  nginx:
    build:
      context: .
      dockerfile: ./Dockerfile_nginx
    volumes:
      - ./etc/logs/nginx:/etc/nginx/logs
      - ./etc/nginx/conf.d:/etc/nginx/conf.d
      - ./laravel:/var/www/laravel
    ports:
      - 8081:80
    links:
      - phpfpm
    depends_on:
      - phpfpm
  phpfpm:
    build:
      context: .
      dockerfile: ./Dockerfile_php
    volumes:
      - ./laravel:/var/www/laravel
    links:
      - mysql
    depends_on:
      - mysql
    env_file:
      ./etc/php/env/setting.env
  mysql:
    image: mysql:5.7
    volumes:
      - ./mnt/mysql:/var/lib/mysql
      - ./etc/mysql/mysql_conf:/etc/mysql/conf.d
    env_file:
      ./etc/mysql/env/setting.env
    ports:
      - 13306:3306
```

思想としては、NginxとかMySQLの設定ファイルは```etc```の中に詰め込んであげて、DockerのゲストOSにマウントしてあげます。
これがベストプラクティスかどうかは、運用してみて決めるかなとは思うのですが、実際問題Nginxの設定ファイルをGit管理したことってあまりなくて、もし```etc/nginx```配下をLaravelソースと一緒にGit管理できたら、運用面で知見が得られそう、とは思います。
あくまで個人開発環境としてのDockerを想定しているので、NginxのConfをGit管理すること自体に問題はないかなと感じています。

また、NginxのログファイルもホストOS```./etc/logs/nginx```にマウントするようにしています。ログの調査もこれで捗りますね。

localhostはポート番号80を取りたくないので8081番を使うように設定しました。

## PHP
### Dockerfile
WORKDIRをソースコードのrootにしておくと、あとでcomposerを使ってLaravelのセットアップをするときにわざわざDockerゲスト内に入らなくていいのでお得です。

phpのalpineを利用します。

```dockerfile:Dockerfile_php
FROM php:7.3.0RC6-fpm-alpine3.8

# setting working directory to source code root
WORKDIR /var/www/laravel

# copy php.ini
COPY ./etc/php/php.ini /usr/local/etc/php/

# install pdo, etc...
RUN apk update && apk add --no-cache \
  freetype-dev libjpeg-turbo-dev libpng-dev libmcrypt-dev \
  git vim unzip tzdata \
  libmcrypt-dev \
  libltdl \
  && docker-php-ext-install pdo_mysql mysqli mbstring gd iconv \
  && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
  && apk del tzdata \
  && rm -rf /var/cache/apk/*

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

この```Dockerfile```は、先の```docker-compose.yml```にて```phpfpm```の```dockerfile: ./Dockerfile```で指定しているファイルになります。

やっていることは、PHPのalpineのDockerイメージに対して、php.iniをホストOSからコピーして配置してあげて、あとは```alpine linux```のパッケージマネージャである```apk```を使ってもろもろパッケージを入れています。
最後にLaravelのセットアップのためにPHPのパッケージマネージャである```composer```も入れておきます。

```alpine linux```が```apk```というパッケージマネージャを使っているのを知らず、しばらく悪戦苦闘しましたが、[Alpine Linux入門 -内部構造とapkでパッケージインストール編-](https://blog.stormcat.io/post/entry/alpine-entry-apk/)の記事を参考に、コマンドの叩き方を調べたのと、いくつか記事を見ている中で、これくらいのパッケージを```apk add```すればいいやろ、というのを入れるようなDockerfileになっています。

#### 補足（Alpine Linuxにおけるタイムゾーンの設定について）
```Alpine Linux```を使った場合、タイムゾーンを日本（東京）に変更する設定が```docker-compose.yml```からできないらしいです。手元ではできませんでした。

そこで、[Alpine Linux でタイムゾーンを変更する](https://qiita.com/dtan4/items/8359e389b95cbc60952d)を見て、```Dockerfile```で```tzdata```を```apk add```した上で、用済みになった部分を全てdeleteする形で、```Asia/Tokyo```の時刻に合わせるようにしました。

あとで、Nginxのほうでもこの対策は実施していますが、どちらかというとアクセスログなどで時刻を参照するNginxのほうがタイムゾーンの設定は必要ですね。

これだけでもQiitaなりMediumに書けそうなくらい罠ですねｗ

#### 補足（mcryptについて）
mcryptっていうパッケージも調べた範囲で出てきたので入れようとしたのですがエラーで入らず・・・不要だと思っておきます。

### php.ini
php.iniには最低限必要なものを。

```ini:php.ini
[Date]
date.timezone = "Asia/Tokyo"
[mbstring]
mbstring.internal_encoding = "UTF-8"
mbstring.language = "Japanese"
```

```ini:setting.env
DB_HOST=mysql
DB_DATABASE=mejileben
DB_USERNAME=root
DB_PASSWORD=pass
```

## Nginx
先程も書きましたが、タイムゾーンの設定のために、```tzdata```を入れています。

### Dockerfile
```dockerfile:Dockerfile_nginx
FROM nginx:1.15.7-alpine

RUN apk --update add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata && \
    rm -rf /var/cache/apk/*
```

### default.conf
laravelのrootディレクトリは、```docker-compose.yml```の設定で、ゲストOSの```./laravel```にマウントしています。
後ほど、コンテナに入り込んで```/var/www/laravel```にLaravelをセットアップします。

開発環境のつもりなので、そこまで本気のセッティングはしないでおきます。

ここで設定するaccess_log/error_logはホストOSにもマウントしているので見ることができるようになるはずです。

```conf:default.conf
server {
    listen       0.0.0.0:80;
    server_name  localhost;
    charset      utf-8;

    root /var/www/laravel/public;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    index index.php;

    location / {
        access_log  /etc/nginx/logs/access.log main;
        error_log   /etc/nginx/logs/error.log;
        try_files $uri $uri/ /index.php$is_args$args;
    }

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

### 補足（Nginx✕Alpine Linuxについて）
[Alpine Linux で Docker イメージを劇的に小さくする](https://qiita.com/asakaguchi/items/484ba262965ef3823f61)あたりを参考にして、もうすこしゴリゴリにDockerfileを書いても良いかもしれません。

## MySQL
```ini:setting.env
MYSQL_ROOT_PASSWORD=pass
MYSQL_DATABASE=mejileben
```

config用のディレクトリもマウントしているけど、特に今の時点で置くものがないので置いてないです。

## docker-compose up
ここまでできたら、```docker-compose up```を叩きます。

```shell
[mejileben]$ docker-compose up
Starting laravel_docker_mysql_1_aad144ffc82c ... done
Creating laravel_docker_phpfpm_1_526cdb24bb99 ... done
Creating laravel_docker_nginx_1_2244d1f9f74f  ... done
Attaching to laravel_docker_mysql_1_aad144ffc82c, laravel_docker_phpfpm_1_1353a5b38614, laravel_docker_nginx_1_96e7d440124a
mysql_1_aad144ffc82c | Initializing database
phpfpm_1_1353a5b38614 | [21-Dec-2018 03:04:11] NOTICE: fpm is running, pid 1
phpfpm_1_1353a5b38614 | [21-Dec-2018 03:04:11] NOTICE: ready to handle connections
mysql_1_aad144ffc82c | 2018-12-21T03:04:10.371603Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
...
```

エラーパターンとして、このようなログが出たことがありました。

```shell
[mejileben]$ docker-compose up  
Starting laravel_docker_mysql_1_aad144ffc82c ... error

ERROR: for laravel_docker_mysql_1_aad144ffc82c  Cannot start service mysql: b'OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:402: container init caused \\"rootfs_linux.go:58: mounting \\\\\\"/Users/saitoyus/Documents/dev/laravel_docker/etc/mysql/mysql_conf\\\\\\" to rootfs \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged\\\\\\" at \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged/etc/mysql/conf.d\\\\\\" caused \\\\\\"not a directory\\\\\\"\\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type'

ERROR: for mysql  Cannot start service mysql: b'OCI runtime create failed: container_linux.go:348: starting container process caused "process_linux.go:402: container init caused \\"rootfs_linux.go:58: mounting \\\\\\"/Users/saitoyus/Documents/dev/laravel_docker/etc/mysql/mysql_conf\\\\\\" to rootfs \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged\\\\\\" at \\\\\\"/var/lib/docker/overlay2/0f80e4463441b42748eaae98b154f59e4fb7a27ddba9a56c79db5a0f33c2d2e8/merged/etc/mysql/conf.d\\\\\\" caused \\\\\\"not a directory\\\\\\"\\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type'
ERROR: Encountered errors while bringing up the project.
```

この場合、```Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type```というログの通り、```docker-compose.yml```で指定しているマウントするディレクトリが、存在しないというのが原因です。
僕の場合、

```docker-compose.yml
      - ./etc/mysql/mysql_conf:/etc/mysql/conf.d
```
のmysql_confが、フォルダではなくファイルになってしまっていたことが原因でした。
vimのNERDTreeから作ったのでコマンドミスしてファイルになってしまってましたｗ（言い訳）。

mysqlを起動したことで、ホストOSの```./mnt/mysql```に中身ができています。

```shell
[mejileben]$ tree mnt -L 2                              (git)-[master]
mnt
└── mysql
    ├── auto.cnf
    ├── ca-key.pem
    ├── ca.pem
    ├── client-cert.pem
    ├── client-key.pem
    ├── ib_buffer_pool
    ├── ib_logfile0
    ├── ib_logfile1
    ├── ibdata1
    ├── ibtmp1
    ├── mysql
    ├── performance_schema
    ├── private_key.pem
    ├── public_key.pem
    ├── server-cert.pem
    ├── server-key.pem
    ├── sys
    └── mejileben

5 directories, 14 files
```

## Laravel用のソースコードセットアップ
起動が完了したら、自身の```localhost```上でLaravelが起動しているはずですが・・・
http://localhost:8081
を開くと、```Not Found```になってしまいます。ページが開けません。

```docker-compose up```しているシェルの画面を開くと、

```shell
nginx_1_96e7d440124a | 172.18.0.1 - - [21/Dec/2018:03:05:07 +0000] "GET / HTTP/1.1" 403 555 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" "-"
nginx_1_96e7d440124a | 2018/12/21 03:05:07 [error] 6#6: *1 directory index of "/var/www/laravel/public/" is forbidden, client: 172.18.0.1, server: localhost, request: "GET / HTTP/1.1", host: "localhost"
phpfpm_1_1353a5b38614 | 172.18.0.4 -  21/Dec/2018:03:05:08 +0000 "GET /index.php" 404
```
などと出ていることが確認できます。
※ここでは```-d```をつけずに起動していますが、普段は```docker-compose up -d```することのほうが多いです。

これはLaravelの肝心のソースコードが入っていないことが原因なので、Laravelのソースコードを入れましょう。

```docker-compose up```していることで、もうコンテナ上でゲストOSが起動しているので、そこの中に入ってあげて、Laravelのソースコードを```composer```使って入れてあげる流れになります。

当然ですが、ここでホストOSに```composer```をinstallしてしまうとDockerの意味がなくなってしまいます。

ですので、立ち上げた```phpfpm```コンテナに入り込んで、その内部でLaravelをセットアップしましょう。
```Dockerfile```にて、WORKDIRを```/var/www/laravel```にしているので、ホストOSから直接```docker-compose exec```で実行できます。
このコマンドはWORKDIRで実行されるため、コマンド末尾の```.```は```/var/www/laravel```を指します。

```shell
[mejileben]$ docker-compose exec phpfpm composer create-project laravel/laravel .
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Installing laravel/laravel (v5.7.19)
  - Installing laravel/laravel (v5.7.19): Loading from cache
Created project in .
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 86 installs, 0 updates, 0 removals
  - Installing vlucas/phpdotenv (v2.5.1): Loading from cache
  - Installing symfony/css-selector (v4.2.1): Loading from cache
  - Installing tijsverkoyen/css-to-inline-styles (2.2.1): Loading from cache
  - Installing symfony/polyfill-php72 (v1.10.0): Loading from cache
  - Installing symfony/polyfill-mbstring (v1.10.0): Loading from cache
...
```

ここでうっかりプロジェクト名を指定すると、その名前のディレクトリにソースコードがセットアップされて、NginxからするとNot Foundになってしまうので注意です。
あと、冒頭でrootユーザーでInstallするな！って怒られていますが、僕は気にしませんでしたｗ

さて、このインストールが終わった頃に、ホストOSのほうで```tree```コマンドしてみると

```shell
[mejileben]$ tree laravel -L 1                         (git)-[master]
laravel
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── package.json
├── phpunit.xml
├── public
├── readme.md
├── resources
├── routes
├── server.php
├── storage
├── tests
├── vendor
└── webpack.mix.js

10 directories, 8 files
```

見事にLaravel関連ファイルが入っていることが確認できます。
こうやって環境構築自体はDocker内でやって、実態のあるファイルは自分のOSにマウントするというのをやると、**Docker使っている感**が出ますね〜

この状況で
http://localhost:8081
を開くと、

<img width="1280" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/156567/8cc661d8-2451-366b-3787-1f7f66c5902e.png">

無事にLaravelのページが表示されました！

## 次は
``npm``のコンテナも入れて、``Nuxt.js``の環境構築もしていきたいと思います！

## 参考記事
[Laravel+MySQL+NginxでさくっとDocker開発立ち上げる](https://qiita.com/bboobbaa/items/67bd9e74d7805516e68d)
[Alpine Linux で Docker イメージを劇的に小さくする](https://qiita.com/asakaguchi/items/484ba262965ef3823f61)
[Alpine Linux でタイムゾーンを変更する](https://qiita.com/dtan4/items/8359e389b95cbc60952d)
[Alpine Linux入門 -内部構造とapkでパッケージインストール編-](https://blog.stormcat.io/post/entry/alpine-entry-apk/)