Dockerでlaravelの本番環境構築をしてみた (php-pfm,nginx, mysql, redis)

こんにちは

タイトルの通りに今日はDockerでlaravelの本番環境構築経験を述べたいと思います。

私はまだ日本語を勉強中なので。わかりにくいことがあれば申し訳ありません。

よろしくお願いします。

前回は**開発環境**について話しました。下記です。
http://qiita.com/mytv1/items/f26cd77f2801357dee8f

今回の本番構築は前回に述べた開発環境構築と違うところがいくらかあります。主にはサービス管理仕方とサービスをビルドイメージについてです。

詳細は下記にお書きします。

#本番環境
下記は自分の本番環境情報です：

* ソフトウェア：
OS : Alpine Linux 3.5.2
Docker-Client : 17.06.0-ce
Docker-Server: 17.06.0-ce
docker-compose : 1.15.0

* ハードウェア：
awsのサービスを使います。t2.mircoの3つのインスタントです。

今度は`docker swarm`を使うということで、上の3つインスタントを一つの管理ノードと2つのワーカーノードに割り当てる予定です。

### 予定の環境構築
1. docker swarm モードで走ります。
2. 下記のサービスを上げます：
 * app : laravelの処理を行うサービス
 * web : nginxを持つサービス
 * database : データベース管理サービス
 * redis : redisサービス

ブラウザーの確認でデフォルトの「Laravel」が表示できると ok だという認識ですすめました。

まずはプロジェクトのディレクトリの構造について話したいと思います。

## 1. ディレクトリ構造
```
projectname/
    www/
        /* laravel source code */
    conf/
        web/
            prod.vhost.conf
            dev.vhost.conf
    prod.web.dockerfile
    prod.app.dockerfile
    prod.docker-compose.yml
    dev.web.dockerfile
    dev.app.dockerfile
    dev.docker-compose.yml
```

インターネット上で参考が見つからないので、構造は自分で考えて上記でしてみました。

* `www` : laravelのマインのソースコードを置きる場所。
* `conf`: サービスのコンテナーに入れたいファイルを置きる場所。今はnginxの `vhost.conf`だけあります。
* `prod.<service-name>.dockerfile` : 本番で利用するイメージをビルドする指定の`dockerfile`。
* `prod.docker-compose.yml` : 本番環境の`docker-compose`設定。
* `dev.<service-name>.dockerfile` : 開発環境でサービスのイメージをビルドする`dockerfile`。
* `dev.docker-compose.yml` : 開発環境の`docker-compose`設定。

理由：

* 本番と開発環境の設定が違うので、`dockerfile`と`docker-compose.yml`も別にします。
* `www`ディレクトリはLaravelに関係するファイルだけを設置したいです。dockerに関するのは上にします。

この構造の問題：

* `dockerfile`の数はサービスの数と一緒に上がります。サービスが増えると、ファイルが多くて見難くなります。
* 他のモージュルの`xdebug`、`phpadmin`などについてはまだ考えません。追加すると構築が難しくなるかわかりません。

ひとまずこれで次に勧めます。

## 2.`docker-compose.yml`
各設定ファイル内容を説明するという形式ですすめたいと思います。まずは`docker-compose.yml`です。

### 2.1. appサービス

```
version: '3'
services:

  # The Application
  app:
    image: myrepo/laravel-app
    working_dir: /var/www
    volumes:
      - /var/www/storage
    environment:
      - "DB_HOST=database"
      - "REDIS_HOST=cache"
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
    tty: true
    ports:
      - "9000:9000"

```
* `version: '3'`: docker-composeのバージョン。このバージョンは2よりswarmモードをよくサポートすると言われますので、このバージョンをすすめます。
* `image: myrepo/laravel-app` サービスが利用するイメージ。このイメージをビルドやり方は前述にお話します。
* `volume`記載されるボーリュームはcontainerがストップされても残るように宣言。laravelとしては、このディレクトリはログとキャッシュviewを保存しますね。
* `enviroment` 環境変数を宣言します。Laravel利用する変数は`.env`だけにいれてもいいと思います。
* `logging` ログ形を宣言します。私は「json-file」を選びます。
* `tty: true` この項目はあまりわかりませんが、宣言しないとサービスが始まる途端すぐに止まることがあります。デプロイする時にこのサービスは自分自身のターミナルがなくて、`pseudo terminal `指定しない問題になるかなと思います。
* `ports:` ホストの9000ポートをサービスの9000ポートに紐付けます。

### 2.2. webサービス

```
# The Web Server
  web:
    image: myrepo/laravel-web
    ports:
      - 80:80
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: "10"
    depends_on:
      - app

```

`depends_on`: **web**は**app**サービスに依存するという関係を宣言します。記載しなくて、webサービスがappより先に
起動すると、nginxで**app**が見つからないエラーになります。

### 2.3. databaseサービス

```
   # The Database
  database:
    image: mysql:latest
    volumes:
      - dbdata:/var/lib/mysql
    environment:
      - "MYSQL_DATABASE=homestead"
      - "MYSQL_USER=homestead"
      - "MYSQL_PASSWORD=secret"
      - "MYSQL_ROOT_PASSWORD=secret"
    ports:
        - "33061:3306"
    logging:
      driver: "json-file"
      options:
        max-size: "200k"
        max-file: 10
```

### 2.4. redisサービス

```
  # redis
  cache:
    image: redis:3.0-alpine
```

`database` と `redis`サービスは公開のmysqlとredisイメージを利用します。

設定はこれで終わります。本番環境にこのファイルを投げればokだと思います！
次は`app`と`web`サービスが利用するイメージをビルドする仕方を話したいと思います。

## 3.docker イメージをビルド
#### 前提：
docker hub アカウント所持しています。

例として、こらからはそのアカウント名は「myrepo」とします。

### 3.1. `myrepo/laravel-app` イメージをビルド
#### 3.1.1. `dockerfile`を用意
**prod.app.dockerfile**

```
FROM php:7.0.4-fpm

COPY ./www/.env.prod ./www/.env
COPY ./www /var/www

WORKDIR /var/www

RUN apt-get update -y && apt-get install -y zip unzip

RUN docker-php-ext-install pdo mbstring

RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
    && php composer-setup.php \
    && php -r "unlink('composer-setup.php');" \
    && php composer.phar install --no-dev --no-scripts \
    && rm composer.phar

RUN chmod -R 777 /var/www/storage \
        /var/www/bootstrap/cache

RUN php artisan key:generate
RUN php artisan config:clear
RUN php artisan config:cache
RUN php artisan optimize
```

* `FROM php:7.0.4-fpm`: ベースイメージの宣言
* 

```
COPY ./www/.env.prod ./www/.env
COPY ./www /var/www
``` 

ソースコードをコピーします。`.env.prod`を事前に用意しました。

* `RUN apt-get update -y && apt-get install -y zip unzip
..
    && rm composer.phar
` : zip, unzip, pdo, mbstring, composerをインストールします
* 最後のところのコマンドはLaravelセットアップコマンドですね。

#### 3.1.2. `dockerfile`でイメージをビルド
`docker build -f prod.app.dockerfile -t myrepo/laravel-app .`

成功であれば、`Successfully tagged myrepo/laravel-app:latest`が表示されますね。

#### 3.1.3. docker hubにイメージをプッシュ
```
docker login
docker push myrepo/laravel-app
```

appのイメージが制作できました！次の webのイメージビルドも同じです。

### 3.2. `myrepo/laravel-web` イメージをビルド
#### 3.2.1. dockerfileを用意
**prod.web.dockerfile**

```
FROM nginx:1.10-alpine

ADD conf/web/prod.vhost.conf /etc/nginx/conf.d/default.conf

COPY ./www/public /var/www/public
```
`public`ディレクトリのコピーで、staticファイルを参照しはやくなるようにと言われるので、してみました。

**vhost.conf**

```
server {
    listen 80;
    index index.php index.html;
    root /var/www/public;

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

    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;
        sendfile off;
    }
}
```

#### 3.2.2. `dockerfile`でイメージをビルド
`docker build -f prod.web.dockerfile -t myrepo/laravel-web .`

#### 3.2.3. docker hubにイメージをプッシュ
`docker push myrepo/laravel-web`

これで必要なイメージが出来上がりました！
本番で構築を実行する前にチェックのため開発の環境で一度実行しましょうか。ということで、次は開発環境で、本番環境を仮として実行します。

## 4. 開発環境で本番環境構築を実行してみます
`docker swarm`モードを利用するということで、swarmモードを初期化します。
`docker swarm init  #init swarm mode`

stackにデプロイ:
`docker stack deploy --compose-file prod.docker-compose.yml mysrv `

`mysrv`は全体サビースの名前です。任意に好きな名前でしました。

`docker stack deploy` を実行した後、走っているサビース(docker service ls)やプロセス(docker ps)をチェックすることができます。

自分の場合は`docker ps`結果は以下です。

```
my@mymy:~/Projects/mysrv$ docker ps
CONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS              PORTS                       NAMES
811e818e83b9        myrepo/laravel-web:latest       "nginx -g 'daemon ..."   3 minutes ago       Up 3 minutes        80/tcp, 443/tcp             mysrv_web.1.hogvz22nxo19b1w92ev3pu5xz
426b99609e97        myrepo/laravel-app:latest       "php-fpm"                3 minutes ago       Up 3 minutes        9000/tcp                    mysrv_app.1.n763hd569tu5hxd3u6m3ee02e
6a02ce819570        mysql:latest                    "docker-entrypoint..."   3 minutes ago       Up 3 minutes        3306/tcp                    mysrv_database.1.sc8j9xvqmkaotyjpotol9nucf
5238474eba93        redis:3.0-alpine                "docker-entrypoint..."   3 minutes ago       Up 3 minutes        6379/tcp                    mysrv_cache.1.qbtraxvu6kcchiwm6w5sjkgsj
```

deploy成功で問題がなければ、ブラウザーで結果を確認できます：http://0.0.0.0/
デフォルトのLaravel画面が表示すると想定します。

**Note :**
開発の時に私は使っていたコマンドは`docker stack`ではなく、`docker-compose up`でした。
`docker-compose up`を本番で実行することができます。でも swarm モードではないので、いろいろな制限があります。

違うところはdockerの公開ページと[ここ](https://blog.nimbleci.com/2016/09/14/docker-stacks-and-why-we-need-them/)などに参考することができます。

開発で本番構築を実行しても問題なくということで、本番で実行しましょう！

## 5. 本番で構築を実行
本番で実行するのは前述で述べたのと全く同じです！初回時の基本なステップを話します。

1. `prod.docker-compose.yml`を本番にコピー
コマンドの`scp`などで、ファイルを本番に上げます。

2. swarmを初期化
`docker swarm init`

3. `docker stack deploy --compose-file prod.docker-compose.yml mysrv`
4. servicesとprocessが正常に動くかをチェック

```
~/myproject $ docker service ls
ID                  NAME                        MODE                REPLICAS            IMAGE                           PORTS
fvoyz57upgxr        mysrv_app                   replicated          1/1                 myrepo/laravel-app:latest   *:9000->9000/tcp
jq491ivl1cid        mysrv_database              replicated          1/1                 redis:3.0-alpine                *:33061->3306/tcp
ltr1yqrny80b        mysrv_web                   replicated          1/1                 myrepo/laravel-web:latest   *:80->80/tcp
s4laek150xv7        dockercloud-server-proxy    global              1/1                 dockercloud/server-proxy        *:2376->2376/tcp
```
`docker service ls`で、`mysrv`の３つの子供サービスが上がったと確認しました。

サーバーのipは `x.x.x.x`であれば、http://x.x.x.x で`Laravel`のデフォルト画面が表示すると思います。
![laravel-aws.png](https://qiita-image-store.s3.amazonaws.com/0/147307/9877562f-ba40-26a0-7d97-526cd6017e41.png)
上は自分のawsサーバーに上がった結果です。

ダウドしたい時は`docker stack rm mysrv`を実行します。

これで終わります！

ソースコードは下記です。
https://github.com/mytv1/sample-docker-laravel-prod

# 終わりに
サーバー構築についてあまり経験がない私は簡単なLaravelプロジェクトを上げました。
nginx, mysqlなどのイメージはもうdocker hubで用意されていてよかったと思います。swarmのおかげで、将来はいろいろサーバー構築コストも削減できればよかったと思います。

# 参考
https://medium.com/@shakyShane/laravel-docker-part-2-preparing-for-production-9c6a024e9797