LEMP環境 Laravel Nginx Mysql PHPをDockerで構築してみた

## はじめに
最近LaravelのDocker環境を考える機会がありました。  
他の環境は作ったことが何度かありましたが、Laravelは立ち上げのコマンドすら知らない状態でDocker環境を構築していったので良い学びになりました。
`healthcheck`を使用することにより、DBの準備完了前にLaravelがDBを読み取ろうとする事故をなくすことに成功しています。
コンテナの実行順序を正しく制御することで、terminarl上でDocker構築のログが止まる頃にはアプリが完全に応答する状態を保証しています。
適宜コメントを入れているので、参考にしてください。

私は学生エンジニアです~~あと2ヶ月くらい...~~。  
至らない点もあるかもしれません。  
その点ご了承ください。

## 前提条件
Docker, Docker Composeの知識

## 各ファイルの説明

ファイル構成
```shell
data/ # Dockerで立てたdatabaseの中身
docker/
  db/
    conf.d/
      - my.cnf
    init/
      - init.sql # 初回に実行したいsql処理
    .env
  php/
    - .env # Laravelはrootの.envを読み込むので、特段phpコンテナにのみ指定したenvがなければ不要
    - Dockerfile
    - php.ini
  nginx/
    - default.conf # enginxの設定ファイル
src/ # Laravel本体 (localのsrcの中でLaravelをセットアップしてください)
.dockerignore
.gitignore
docker-compose.yml
```

docker/db/conf.d/my.cnf
```
[mysql]
default-character-set=utf8mb4
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_bin
[client]
default-character-set=utf8mb4
```
docker/db/init/init.sql
```sql
/* 実行したいsqlを記述 */
/* volumeのマウント先の*.sqlはentrypointにimportされて全て実行される仕組み */
```
docker/db/.env
```
MYSQL_ROOT_PASSWORD=root
MYSQL_DATABASE=test
MYSQL_USER=test
MYSQL_PASSWORD=test
```

docker/nginx/default.conf
```shell
server {
  listen 80;
  root /var/www/html/public; # index.phpが配置されているpublicディレクトリをrootにする
  index index.php;

  # 特定のURIやパターンに対するサーバーの動作を指定する
  location / {
    # 左から順に評価
    # 存在しなければ最後の指定に従ってindex.phpを返す
    root /var/www/html/public;
    index index.php;
    try_files $uri $uri/ /index.php$query_string;
  }

  # phpファイルはphp-fpmコンテナにリクエストして処理してもらう
  location ~ \.php$ {
    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000; # (Dockerのservice名を使用していることに注意) (php-pfmはwebサーバを9000で受け付けている)
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
  }

  # statusページの設定
  location /status {
    access_log off;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass php:9000; # (Dockerのservice名を使用していることに注意) (php-pfmはwebサーバを9000で受け付けている)
    fastcgi_index status.html;
  }
}
```

docker/php/Dockerfile
```Dockerfile
# ExpressやRailsと違って、ソースと依存packageが紐づいている
# composer関連ファイルだけ先にコンテナにCOPYしてinstallしようとするとartisanを呼び出してエラーになる
FROM php:8.3.2-fpm-bullseye

WORKDIR /var/www/html

COPY ./src /var/www/html/

# composer use git zip
# apt may change the interface. apt-get would be better now.
# docker-phpext-install pdo_mysql provide driver of mysql
RUN apt-get update && apt-get install -y git zip zlib1g-dev lsof \
    && docker-php-ext-install pdo_mysql

# enable status code page
RUN echo pm.status_path = /status >> /usr/local/etc/php-fpm.d/www.conf

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

# package install
RUN composer install

COPY ./docker/php/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

CMD ["php-fpm"]
```
docker/php/entrypoint.sh
```shell
#!/bin/bash
set -e

# chown -R www-data:www-data ./
# chmod -R 777 storage bootstrap/cache
php artisan migrate
  
exec "$@"
```
docker/php/.env
```shell
# 指定したい環境変数(外部に晒したくない)があればここで指定する
# Laravelはプロジェクトのroot(今回だとsrc/)に存在する.envを読み込むので、それ以外でコンテナに付与したければここで記載する
```
docker/php/php.ini
```shell
# phpが利用しているphp.iniは php --iniで検索できる
# 複数出てくるが、Loaded Configuration Fileが利用しているphp.ini
# デフォルトはnoneで、php.iniを指定の場所に置くことで設定できる
[Date]
date.timezone = "Asia/Tokyo"
[opcache]
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=1
```

docker-compose.yml
```yml
version: "3.8"
services:
  php: # phpコンテナはnginxが実行できないphpの処理をするために存在する # webサーバーからのリクエストは9000で受け付ける
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
    env_file: ./docker/php/.env
    tty: true
    healthcheck: # healthcheck
      test: ["CMD", "lsof", "-i:9000"]
      start_period: 5s
      timeout: 20s
      retries: 10
    depends_on:
      db:
        condition: service_healthy # dbコンテナの構築が完了(healthcheckに合格する)まで待つ
    volumes:
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini
      - ./src/:/var/www/html
    environment:
      - TZ=Asia/
  nginx:
    image: nginx:latest
    depends_on:
      db:
        condition: service_healthy
      php:
        condition: service_healthy
    ports:
      - 8080:80
    volumes:
      - ./src:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf # 設定ファイルをnginxの指定位置にマウント
  db:
    image: mysql:8.0.31
    platform: linux/amd64
    env_file: ./docker/db/.env
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    volumes:
      - ./data:/var/lib/mysql
      - ./docker/db/conf.d/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./docker/db/init:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306
    healthcheck: # healthcheck
      test:
        ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-proot"]
      start_period: 5s
      timeout: 20s
      retries: 10
    environment:
      TZ: Asia/Tokyoaa
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    platform: linux/amd64
    container_name: phpMyAdmin
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=db
    depends_on:
      db: # phpmyadminはdbの準備が完了したら構築する
        condition: service_healthy
    ports:
      - 8000:80

```
.dockerignore
```
**/vendor
```
.gitignore
```
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.env.example
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode

.DS_Store
db
```
## Laravel側の設定
src/.env(Laravelのroot)
```shell
DB_CONNECTION=mysql
DB_HOST=db # Dockerのservice名を利用
DB_PORT=3306
DB_DATABASE=test # Mysqlのenvに合わせる
DB_USERNAME=test # Mysqlのenvに合わせる
DB_PASSWORD=test # Mysqlのenvに合わせる
```

## おわりに
Dockerの環境づくりは何度かやったことがあるのですがhealthcheckを利用したの初めてでした(存在は知ってた)  
今回腰を据えて環境構築をすることになったので、healthcheckは絶対に導入したいと考えていました。
慣れないうちはDockerで環境構築をするに当たり様々な試行錯誤と調査が必要で大変ですが、完成した時の達成感は大きなものがありました。  
この記事を作成するにあたり多くの有益な情報源を頼らせていただきました。  
ありがとうございます。  
参考URLを貼っておきますので確認してください。

## 参考
https://qiita.com/moaikids/items/f7c0db2c98425094ef10

https://zenn.dev/redheadchloe/articles/778606a0b34d6a

https://qiita.com/takegons/items/644dd262801244af769f

https://qiita.com/te-k/items/1f2deddb6808afca2f2b

https://zukucode.com/2019/06/docker-laravel-mysql.html

https://zenn.dev/sun_asterisk/articles/b4b17681d08018

https://tech.actindi.net/2022/02/21/083000

https://papakoletter.com/docker_healthcheck/

https://serverfault.com/questions/928983/php-fpm-status-page-not-found-on-docker