LaravelのインフラをDockerで作ってみた

Laravelのインフラ構築をDockerでやってみました。


この記事ではLaravelのアプリケーションをコンテナ化するのではなく、
インフラだけをコンテナにして、アプリケーションはDockerの外でデプロイするようにしています。

ここに至る経緯として、最初はECSでの運用なども視野に入れていたのですが、
最終的に現在のチームのスキルなどを考慮して、コンテナの知識がそれほど高くないエンジニアでも気軽にデプロイできることを目指しました。

今回やりたいことはAnsibleなどを使えばいいんでしょうが、あんまり知識がないのでとりあえず多少知識のあるDockerを採用しました。

構築対象はまっさらのEC2インスタンスを想定しており、phpなどが入ってない想定です。
なので、Dockerでそのあたりも構築していきます。

というわけで、まずは構成を紹介して、後半で環境構築手順を説明していきます。

## コンテナ構成

- web
    - nginxを実行するコンテナ
- fpm
    - php-fpmを実行するコンテナ
- php
    - phpコマンドを実行するためのコンテナ
- worker
    - キューワーカとして動作させるコンテナ
- scheduler
    - スケジューラとして動作させるコンテナ
- db
    - develop環境でのdbコンテナ
- phpmyadmin
    - db管理用のPHPMyAdminコンテナ
- redis
    - develop環境でのredisコンテナ
- composer
    - composerを実行するためのコンテナ
- npm
    - npmを実行するためのコンテナ

### ファイル構成

laravel-project以下に必要なファイルを追加します。

```:ファイル構成
+ laravel-project
  + docker
    + composer
      - Dockerfile
    + fpm
      - Dockerfile
    + npm
      - Dockerfile
    + web
      - Dockerfile
      - default.conf
  - .env.development
  - docker-compose.yml
```

### webコンテナ

webコンテナはnginxのイメージからdefault.confを書き換えて、phpの実行をfpmコンテナに渡すようにします。
ここではdefault.confはそれ以外何もしないシンプルなものとします。

```:docker/web/default.conf
server {
    listen  80;
    server_name  localhost;
    index  index.php
    charset  utf-8;
 
    location / {
        root  /usr/share/nginx/html;
        try_files  $uri $uri/ /index.php$is_args$args;
    }
 
    location ~ \.php$ {
        root  /var/www/html/public;
        fastcgi_pass   fpm:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  /var/www/html/public$fastcgi_script_name;
        include        fastcgi_params;
    }
}
```

```:docker/web/Dockerfile
FROM nginx:alpine

COPY default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
```

### fpmコンテナ

fpmコンテナはphp-fpmイメージをベースに諸々インストールします。

```:docker/fpm/Dockerfile
FROM php:7.2.17-fpm-alpine

RUN apk add --update --no-cache git openssh

RUN apk add --no-cache \
    freetype-dev \
    libjpeg-turbo-dev \
    libpng-dev \
    openssl-dev \
 && docker-php-ext-configure gd \
    --with-freetype-dir=/usr/include/ \
    --with-jpeg-dir=/usr/include/ \
 && docker-php-ext-install \
    gd \
    mbstring \
    pdo_mysql

WORKDIR /var/www/html
```

### phpコンテナ、workerコンテナ、schedulerコンテナ

これらのコンテナはfpmのイメージを使います。

### dbコンテナ、phpmyadminコンテナ、redisコンテナ

これらのコンテナはOfficialやPublicのイメージのまま使います。

### composerコンテナ

Laravelではcomposerコマンドが必要になるのでcomposerのためのイメージを作成します。

```:docker/composer/Dockerfile
FROM php:7.3.3-alpine
 
RUN apk add --update --no-cache git unzip && \
    curl -sL https://getcomposer.org/installer | php && \
    mkdir /usr/local/src && \
    mv composer.phar /usr/local/src && \
    ln -s /usr/local/src/composer.phar /usr/local/bin/composer
 
RUN mkdir -p /laravel-project
VOLUME ["/laravel-project"]
WORKDIR /laravel-project
 
ENTRYPOINT ["composer"]
CMD ["--help"]
```

### npmコンテナ

npmコマンドを使うためのイメージを作成します。

```:docker/npm/Dockerfile
FROM node:12.7-alpine
 
RUN apk add --update --no-cache pkgconfig autoconf automake libtool nasm build-base zlib-dev libpng-dev
 
RUN mkdir -p /laravel-project
VOLUME ["/laravel-project"]
WORKDIR /laravel-project
 
ENTRYPOINT ["npm"]
CMD ["help"]
```

### docker-compose.yml

docker-compose.ymlを定義します。
ここでは1ノードで完結できる環境として構築します。

各コンテナに必要なディレクトリをマウントできるようにVolumeを設定します。

また、Laravelで必要な環境変数は.env.developmentファイルに書いておいて、コンテナに渡します。

fpmのイメージにlaravel_project_phpと名前を付けてphp、worker、schedulerで使いまわします。
php、worker、schedulerコンテナはentrypointを指定してartisanコマンドを実行することで、目的の動作をさせます。

```yml:docker-compose.yml
version: "3"
services:
  web:
    build: ./docker/web
    restart: always
    ports:
      - "80:80"
    links:
      - "fpm:fpm"
    volumes:
      - ./public:/usr/share/nginx/html:ro
      - ./storage/app/public:/usr/share/nginx/html/storage:ro
  fpm:
    build: ./docker/fpm
    image: laravel_project_php
    restart: always
    expose:
      - "9000"
    links:
      - "db:db"
      - "redis:redis"
    volumes:
      - .:/var/www/html
    env_file: ./.env.development
  php:
    image: laravel_project_php
    volumes:
      - .:/var/www/html
    env_file: ./.env.development
    working_dir: /var/www/html
    entrypoint:
      - php
  worker:
    image: laravel_project_php
    restart: always
    links:
      - "db:db"
      - "redis:redis"
    volumes:
      - .:/var/www/html
    env_file: ./.env.develop
    working_dir: /var/www/html
    entrypoint:
      - php
      - artisan
      - queue:work
      - redis
      - --tries=3
  scheduler:
    image: laravel_project_php
    links:
      - "db:db"
    volumes:
      - .:/var/www/html
    env_file: ./.env.develop
    working_dir: /var/www/html
    entrypoint:
      - php
      - artisan
      - schedule:run
  db:
    image: mysql:5.7
    restart: always
    ports:
      - "3306:3306"
    volumes:
      - ./docker/mysql/data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_USER=db_user
      - MYSQL_PASSWORD=db_password
      - MYSQL_DATABASE=db_database
    command: --innodb-use-native-aio=0
  redis:
    image: redis:alpine
    restart: always
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    restart: always
    ports:
      - "8080:80"
    links:
      - "db:db"
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=db
      - PMA_USER=root
      - PMA_PASSWORD=root
  composer:
    build: ./docker/composer
    volumes:
      - .:/laravel-project
  npm:
    build: ./docker/npm
    volumes:
      - .:/laravel-project

```

.env.developmentでLaravelの設定をしていきます。
今回セッションやキューはredisを使います。

```:.env.development
APP_ENV=staging
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost/
 
LOG_CHANNEL=stack
LOG_LEVEL=debug
 
SMS_DISABLE=false
 
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=db_database
DB_USERNAME=db_user
DB_PASSWORD=db_password
 
BROADCAST_DRIVER=log
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_DRIVER=redis
 
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379
 
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
```

## 環境構築

まずはこのあたりを参考にDockerとDocker Composeをインストールします。

> AWS EC2 Amazon LinuxでDocker, Docker Composeをインストールする
> https://qiita.com/shinespark/items/a8019b7ca99e4a30d286

インストールが済んだら、本記事の前半でご紹介したファイル構成を含んだlaravel-projectを配置します。

```
git clone laravel-project
cd laravel-project
```

Dockerイメージをビルドします。

```
docker-compose bulid
```

phpコマンドやcomposerコマンドはコンテナ上で実行するので、
コマンドのエイリアスを作っておくと便利です。

```:~/.bashrc
alias php='docker-compose run --rm php'
alias composer='docker-compose run --rm composer'
alias npm='docker-compose run --rm npm'
```

ここから先は上のエイリアスを定義している前提でお読みください。

### Laravel環境構築

Laravelの環境構築をしていきます。

Composerを実行します。

```
composer install
```

ディレクトリのパーミッションを設定します。

```
chmod -R 777 storage bootstrap/cache
```

アプリケーションキーを生成します。

```
php artisan key:generate
```

.envファイルにキーが生成されるため、.env.developmentにコピーします。

public/storageのシンボリックリンクも作っておきます。

```
php artisan storage:link
```

Dockerコンテナを起動します。

```
docker-compose up -d
```

DBのマイグレーションをします。

```
php artisan migrate
```

npmビルドします。

```
npm install
npm run dev
```

スケジューラをcronに登録します。

```
sudo crontab -e
```
```:crontab
* * * * * /usr/local/bin/docker-compose -f /path-to-laravel-project/docker-compose.yml run --rm scheduler >> /dev/null 2>&1
```

一応これで起動はするはずですが、問題が少しあるので対策します。

### 問題:ログのパーミッション

この構成だと、ログがWebから書き込まれた場合と、スケジューラやワーカから書き込まれた場合でパーミッションが違ってしまいます。
本来ならパーミッションをそろえるなどすればいいのですが、妥協してcronで毎分ログファイルのパーミッションを変更してます。

```
sudo crontab -e
```
```:crontab
* * * * * chmod -R 777 /path-to-laravel-project/storage/logs >> /dev/null 2>&1
```

### アプリケーションのデプロイ

laravel-projectディレクトリのファイルを更新するだけです。コンテナの再起動などは必要ありません。

今回は以上となります。