[Docker,Laravel9(Vite,Vue3,Inertia),nginx,MySQL（非Laradoc,非LravelSail）]Laravel9で既存プロジェクト（モノリシック・アーキテクチャ）をDocker化する手順まとめ

## なぜ書いた？
最近筆者はAWSのECS利用を見据えて、LaradocやLaravelSailを使わずにLaravel9のアプリをDocker化する作業を行いました。

その際にLaravel9のモノリシック・アーキテクチャのDocker化の記事がまだまだ少なく、新しいフロントエンドビルドツールのViteに対応した構築部分で苦労したため、備忘録の意味も含めて誰かの役に立てればと思い書きました。

ちなみにLaravel9+Vue(Vite使用)を使ったマイクロサービス（フロントを別フレームワークに切り出したアーキテクチャ）の作り方は下記の英語の記事が落ちていたので、せっかくなのでこちらも共有しておきます。

[stackorverflowの記事](https://stackoverflow.com/questions/72548750/docker-nginx-reverse-proxy-to-vue-vite-and-backend-containers-unexpected-result)
[上記解決の元となった記事](https://dev.to/programmingdecoded/docker-configuration-with-nginx-routing-for-vue-and-laravel-49e9)

※Laravel8については結構記事が落ちているので通しの手順などは他の方のものを見ていただければと思いつつ、局地的なお困りごとにはお力になれるかもしれない記事は以前書いたので、よかったら参考にしてみてください。（下記の記事になります）

[[docker,laravel8,nginx(非Laradoc)] docker環境構築で躓いたところと解決した方法をまとめてみた](https://qiita.com/KONTA2019/items/f323ec606e70227dc542)


## 取り扱いのバージョン
Laravel9（vite,vue3,Inertia利用のモノリシック・アーキテクチャ）
PHP8.1
MySQL8.0
nginx1.23


## 完成形のアーキテクチャ図
![laravel9（モノリス）のアーキテクチャ図.drawio (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555157/f884f0b5-b03e-e207-1997-a232003682cf.png)


## 手順

※注意：手元で動いたソースコードから説明に必要ない部分を適宜削除しているため、そのままコピーするだけだと動かない可能性があります。あくまで参考資料としてご利用ください。

#### 1.すでに動いているLaravel9プロジェクトのディレクトリに移動

#### 2.アーキテクチャ図を参考にdocker-compose.ymlを作成
```:docker-compose.yml

version: "3"

volumes:
  nginx-public: {} # fpmコンテナからnginxコンテナにpublicディレクトリのコピーする必要があるため、ローカルPCを経由してpublicディレクトリを保存させられるようにするためvolumesを用意します。

services:
  nginx:
    container_name: nginx
    build:
      context: .
      dockerfile: nginx.Dockerfile
      args:
        fpm_host: fpm
    depends_on:
      - fpm
    ports:
      - 3334:80
    volumes:
      - ./logs:/var/log/nginx
      - nginx-public:/var/www/public # publicディレクトリを一時保存場所のvolumesから呼び出しで/var/www/publicにコピー

  fpm:
    container_name: fpm
    build:
      context: .
      dockerfile: fpm.Dockerfile # コンテナの設定が入り組むので後ほど手作りする
    volumes:
      - .:/var/www # Laravelプロジェクトをfpmコンテナにコピー
      - nginx-public:/var/www/public # /var/www/publicディレクトリを一時保存場所のvolumesにコピー
    depends_on:
      - db
    networks:
      - default

  db:
    container_name: db
    image: mysql/mysql-server:8.0
    volumes:
      - ./logs/mysql:/var/log/mysql
      - ./local-docker/mysql/mysqld_charset.cnf:/etc/mysql/conf.d/mysqld_charset.cnf
      - ./local-docker/mysql/initdb.d:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_ROOT_HOST: '%'
      TZ: Asia/Tokyo
    ports:
      - 3306:3306
    command: mysqld --innodb_use_native_aio=0

networks:
  default:
    name: network-name

```

#### 3.同じくプロジェクトトップのディレクトリで各Dockerfileを作成

```:fpm.Dockerfile
# ＝＝＝マルチステージビルドを2回行う（phpとnode）＝＝＝
FROM node:16 as node-build # nginx:1.23に対応しているのがnode16系だったので16で設定
COPY . /application
WORKDIR /application
RUN apt-get update\
    && npm install \
    && npm run build --force

FROM php:8.1-alpine as builder
# dockerパッケージインストール
RUN set -eux && \
    apk update && \
    apk add --update --no-cache \
    autoconf gcc g++ make icu-dev libzip-dev libpng libpng-dev oniguruma-dev
RUN docker-php-ext-install pdo_mysql mbstring opcache zip fileinfo gd
RUN curl -SL http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.15.tar.gz | tar -xz -C ~/ && \
    rm /usr/bin/iconv && \
    mv ~/libiconv-1.15 ~/libiconv && \
    ~/libiconv/configure --prefix=/usr/bin && \
    make && make install && \
    rm -rf ~/libiconv

# composerインストール
WORKDIR /app
COPY --from=composer:2.4 /usr/bin/composer /usr/bin/composer

RUN mkdir -p database/seeds && \
    mkdir -p database/factories  && \
    mkdir -p tests

# ＝＝＝マルチステージビルドここまで＝＝＝

# 実際に稼働するコンテナ
FROM php:8.1-fpm-alpine

COPY --from=builder /usr/bin/composer /usr/bin/composer
COPY --from=builder /usr/bin/lib /usr/bin/lib
COPY --from=builder /usr/lib /usr/lib
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20210902 /usr/local/lib/php/extensions/no-debug-non-zts-20210902
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=node-build /application/public /var/www/public

ENV IS_FPM=1

COPY ["docker/fpm/start.sh", "/"]
RUN chmod +x /start.sh

COPY docker/fpm/php.ini /usr/local/etc/php/php.ini

COPY . /var/www

ARG APP_ENV=local
ENV APP_ENV=$APP_ENV

WORKDIR /var/www

EXPOSE 80

CMD ["/start.sh"]

```

```:nginx.Dockerfile
FROM nginx:1.23-alpine

ARG fpm_host=localhost # 本番環境立ち上げではfpm_hostを準備しないことで、この値がlocalhostになる
ENV FPM_HOST=$fpm_host # ローカルではfpm（コンテナ名）として使う

COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf.tmp
RUN envsubst '$FPM_HOST'< /etc/nginx/nginx.conf.tmp > /etc/nginx/nginx.conf
```

#### 4.Dockerfile立ち上げに必要なファイルを作成

```:/laravelプロジェクト/docker/fpm/php.ini
error_reporting = E_ERROR | E_WARNING | E_PARSE | E_NOTICE
display_errors = stdout
display_startup_errors = on
log_errors = on
error_log = /var/log/php/php-error.log
upload_max_filesize = 100M
memory_limit = -1
post_max_size = 100M
max_execution_time = 900
max_input_vars = 100000
extension_dir = /usr/local/lib/php/extensions/no-debug-non-zts-20210902
# セキュリティを向上させるため、PHPのバージョンをレスポンスヘッダに含めないようにする
expose_php = Off

[Date]
date.timezone = "Asia/Tokyo"

[mbstring]
mbstring.language = "Japanese"

[opcache]
opcache.validate_timestamps = ${OPCACHE_VALIDATE_TIMESTAMPS}
opcache.revalidate_freq = 1

```

```:/laravelプロジェクト/docker/fpm/start.sh
#!/bin/sh
set -e

# composerインストール
composer clear-cache
composer install
echo "Complete composer install !"

# Laravelのキャッシュをクリア
composer dump-autoload
php artisan clear-compiled
php artisan optimize
php artisan config:cache
php artisan cache:clear
php artisan route:clear
php artisan view:clear

# マイグレーション実行
php artisan migrate

# バージョン確認
php -v

echo "PHP Setup Complete."

# fpm起動
if [ $IS_FPM -eq 1 ] ; then
  php-fpm
else
  echo "Skip run fpm"
fi

```

```:/laravelプロジェクト/docker/nginx/nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    server {
        listen 80;
        listen [::]:80;
        root /var/www/public;

        # Laravel公式推奨の（クリックジャッキングなどの）セキュリティ
        add_header X-Frame-Options "SAMEORIGIN";
        add_header X-Content-Type-Options "nosniff";

        index index.php;

        charset utf-8;

        location / {
            root /var/www/public;
            try_files $uri $uri/ /index.php$is_args$args;
        }

        location = /favicon.ico { access_log off; log_not_found off; }
        location = /robots.txt  { access_log offc; log_not_found off; }

        error_page 404 /index.php;

        location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            fastcgi_pass ${FPM_HOST}:9000;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
        }

        location ~ /\.(?!well-known).* {
            deny all;
        }
    }

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

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

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    # アップロード上限36MB制限(35*1024^2 = 36.7MB)
    client_max_body_size 35m;

    # セキュリティを向上させるため、nginxのバージョンをレスポンスヘッダに含めないようにする
    server_tokens off;
}

```

```:/laravelプロジェクト/local-docker/mysql/initdb.d/01_create_database.sql
-- DBとユーザーを作成
CREATE DATABASE IF NOT EXISTS `local_db`;
CREATE USER 'local_db'@'%' IDENTIFIED BY 'password';
GRANT ALL ON local_db.* TO 'local_db'@'%';
```

```:/laravelプロジェクト/local-docker/mysql/mysqld_charset.cnf
[mysqld]
character_set_server=utf8
character_set_filesystem=utf8
collation-server=utf8_general_ci
init-connect='SET NAMES utf8'
init_connect='SET collation_connection = utf8_general_ci'
skip-character-set-client-handshake

```

#### 5..envファイルを用意
```
APP_NAME=laravel
APP_ENV=local
APP_KEY=base64:xxxxxxxxxxxxxxxxx
APP_URL=http://0.0.0.0:3334
APP_TIMEZONE=Asia/Tokyo

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug # 本番でこのまま行くと危ないので注意

DB_HOST=db
DB_PORT=3306
DB_DATABASE=local_db
DB_USERNAME=local_db
DB_PASSWORD=xxxx

MIGRATION_ENV=local

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

STORAGE_NAME=s3
```

#### 6.docker-compose buid を実行

#### 7.docker-compose up を実行

大まかな流れとしては上記の通りとなります。何かの参考にしていただければと思います。

## Laravel9のDocker構築で知っておく必要があるLaravel8との相違点

Laravel9のweb上での公開を意識した構築（nginxとの接続の構築）で一番注意することは、一言で言うとwebpack.mixとViteのコンパイルの仕方の違いになります。

Laravel8ではassetsのバージョン管理でwebpack.mix.jsonを使っており、そのwebpack.mix.jsonはpublicの外側で独立して動作できるようになっています。具体的にはpublic配下のファイル(ccsファイルなど)はresources配下のファイル名と一致するようになっています。（表示のタイミングでファイル名にidを結合する仕組みになっています。）
そのおかげで、fpmコンテナと独立してnginxコンテナでコンパイル（`npm run prod`）をしてpublicディレクトリを用意しても動作させることができました。

しかし、Laravel9ではバージョン管理で使っているmanifest.jsonはpublic配下に入っており、public配下のファイル名がコンパイル（`npm run build`）のタイミングでid込みのものに変更されてしまいます。（おそらく処理速度向上のために事前にファイル名変換処理を済ませておいていると思われます。）
そのためLaravel8と同じように"fpmコンテナもnginxコンテナもpublicにコンパイルする大元のresorucesは一緒だから、nginxコンテナを作るタイミングで別途publicディレクトリを準備する形で進めよう"と試みると失敗します。
詳しく説明すると、**fpmコンテナ内にあるmanifest.json**を読み取ったnginxが、nginxコンテナ内の**fpmコンテナとは中身のファイル名が異なっているしまっているpublicディレクトリ**からファイルを探してしまいます。その結果最終的に読み取ったファイル名と一致するものはないと判断し真っ白な画面を表示させてしまいます。

上記の内容から、**Viteを使ったモノリシック・アーキテクチャをnginxを使って表示する場合は、nginxコンテナのpublicディレクトリはfpmコンテナ（laravelが入っているコンテナ）と全く同じものを準備しなければならない**ということを知っておく必要があります。

またそのほかに、Laravel9（Vite）では`npm run build`だとキャッシュが消せないので、`npm run build --force`としなければならない点も注意が必要です。
（Laravel8（webpack.mix）はプロジェクトにキャッシュを持たないため、`npm run prod`だけでよかったため。）

## 2022/12/15追記

qiitaを眺めていたら、参考になる下記の記事に出会いました。
[Docker環境のLaravel 9 + VITEでハマったこと](https://qiita.com/hello_myzn/items/b7bf5c209437ed70af74#node%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%AE5173%E3%83%9D%E3%83%BC%E3%83%88%E3%82%92%E5%85%AC%E9%96%8B)

まだ手元で試せていないですが、この記事から察するに、Viteは`server: {host: true}`を指定すると、5173portのエクスポートとリッスンをViteが独立して接続して、同じく`server: {host: true}`になっている場合にpublicディレクトリを同期していると思われます。

なので今回の記事のように無理やりコピーで同期しているような振る舞いをさせるというより、nginxコンテナにVite関連のフォルダを格納して5173portを使えるようにして、fpmコンテナとnginxコンテナ内のVite同士に勝手に同期し合ってもらう形の方がVite開発者の想定した挙動になっていると思われます。

本番環境のみの想定なら自分記事のものでも十分動かせるとは思うのですが、デプロイが毎度全て上げ直しになるか、fpmコンテナとnginxコンテナのpublicディレクトリのバージョン管理を手動でしなければならないところが辛みだったのですが、上記がうまくいけばおそらく毎回fpmコンテナを上げ直すだけで良くなりそうな予感がするので、皆さんはこちらで構築された方が良い気がします。