Laravel 9 + VITEの開発環境をdockerで実現する方法

Laravel 9でLaravel Mixの代わりに新しく導入されたViteを使えるLaravel開発環境を、docker上に実現する方法を紹介します。

# ゴール

- WindowsやMac上に、dockerを使ったLaravel 9開発環境を作る。
- Laravel MixではなくViteを使う
- サーバーはnginx, php, mysqlを別にする。
- Viteの利点であるページリロード無しでのCSS/JS更新を実現する。

## ところでViteの発音は？

Viteは「ヴィート」と発音します。フランス語だそうです。アメリカ人なら「ヴァイト」と読みそうですが、アメリカ人も「ヴィート」と読んでます。

## ところでLaravel Mixではだめなのか？

Laravel 9の途中から、フロントエンド・ビルド・ツールとして長年標準だったLaravel Mixが非標準となりViteが標準となりました。
現時点では[Laravel 9公式でMixもサポート](https://laravel.com/docs/9.x/mix)しています。したがって、現時点でMixを使うことに問題はない一方で、今後はViteに完全に置き換わる可能性も考えておいたほうが良いでしょう。

## ところでなぜViteなのか？

Laravel Mixは[webpack](https://webpack.js.org/)を使っていますが、Viteは全く違う構造になっています。
詳しく見ていくと本記事の本題から外れるのでやめておきますが、webpackと比較したViteについてはざっと以下のようなイメージだけ持っていればよいかもしれません。
- 開発中の再コンパイルが圧倒的に高速になる
- ソースへの変更が瞬時にフロントページに反映される
- フロントページでAjax通信したあとのページのままデザインや動作を調整できる
- フロントページで動作しているReactのステートを変更せずソース変更が反映される
- ViteはIE11をサポートしてるけど開発中はIE11で見られないなど、これまでになかった制限がある
- でも本番リリース用のビルドはwebpackとあまり変わらない

# 前提

- PHP 8の知識がある
- Laravel 9の知識がある
- Laravel 9に必要な環境構築ができる
- Dockerで簡単な環境構築ができる

本投稿ではWindowsを使っているので、MacやLinuxで合わないところは読み替えてください。

# Dockerに各サーバーを準備する

目指すフォルダ構成はこんな感じにします。(Docker関連のみ)

```
l9vitedev (プロジェクトフォルダ)
├ docker
│ ├ mysql
│ │ └ (中身たくさん)
│ ├ nginx
│ │ ├ logs
│ │ ├ default.conf
│ │ └ Dockerfile
│ └ php
│ 　├ Dockerfile
│ 　└ php.ini
└ docker-compose.yml
```

## Dockerをインストールする

ホストOSに[Docker](https://www.docker.com/)をインストールした状態から開始。

## プロジェクトフォルダにDocker用ファイルを作る

今回はドキュメントフォルダの下に``Qiita``フォルダを作り、``l9vitedev``フォルダを作ります。
作り終わったら、そのフォルダの中にDocker用ファイルを作っていきましょう。

### docker-compose.yml

まずは``docker-compose.yml``から。

```docker-compose.yml
version: '3'
services:
  l9vitedev-nginx:
    container_name: "l9vitedev-nginx"
    build:
      context: ./docker/nginx
    depends_on:
      - l9vitedev-app
    ports:
      - 80:80
    volumes:
      - ./:/src

  l9vitedev-app:
    container_name: "l9vitedev-app"
    build:
      context: ./docker/php
    depends_on:
      - l9vitedev-mysql
    volumes:
      - ./:/src
      - /src/node_modules
      - /src/vendor
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini

  l9vitedev-mysql:
    image: mysql:8.0.28
    command: --max_allowed_packet=32505856
    container_name: "l9vitedev-mysql"
    volumes:
      - ./docker/mysql:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=l9vitedev
    ports:
      - 3306:3306

  l9vitedev-redis:
    image: redis:alpine
    container_name: "l9vitedev-redis"
    ports:
      - 16379:6379
```

いくつかポイントを抑えます。

Webサーバーはnginxなので、appサーバー（PHPサーバー）を別に立ち上げてます。
→今回、後で出てくる説明で対象サーバーをわかりやすくするため意図的にWebサーバーとappサーバーを分けてます。Apache+PHPで同一サーバーにしても全く問題ありません。

Webサーバーはnginxで、ポート80をDocker外に見せて起動します。

AppサーバーはPHPが動作するようにし、ポートはDocker外に見せず、nginxからdocker内のネットワークで呼び出されて動作します。PHPの設定を指定しやすいように、php.iniだけdocker/phpフォルダで見えるようにしています。
また、``/src/node_modules``フォルダと``/src/vendor``フォルダはボリュームマウントから外してます。これは依存ライブラリ群をマウント対象から外すことで動作の高速化を狙ってします。Viteを使った開発環境ではその特性上★超重要★で、これをしないとページのロードに平気で1分とかかかります。

DBサーバーはMySQLで、デフォルトポートの3306をDocker外に見せて起動します。
ポート3306をDocker外に見せなくても動作はしますが、開発中にHeidiSQLやSequel Aceを使ってDBの中身を見るためには必要です。MySQLユーザー情報は、ユーザーIDがroot、パスワードがenvironmentで指定したMYSQL_ROOT_PASSWORDになります。
MySQL（とredis）はここでイメージを指定し、個別の``Dockerfile``を持たない設定とします。
max_allowed_packetオプションは指定しなくてもいいです。(が、指定したくなることが多いのでここに書いてます)

### nginxのDockerfile等

まずは手動で``プロジェクトフォルダ/docker/nginx``フォルダを作ります。

``nginx``の``Dockerfile``を作ります。
同時に、最低限必要な設定である``default.conf``を記述します。

```Dockerfile:Dockerfile
FROM nginx:1.21
COPY ./default.conf /etc/nginx/conf.d/default.conf
```

さらに、``nginx``の動作を設定する``default.conf``を作ります。

```default.conf
server {
    
    listen 80;
    server_name _;

    client_max_body_size 1G;

    root /src/public;
    index index.php;

    access_log /src/docker/nginx/logs/access.log;
    error_log  /src/docker/nginx/logs/error.log;

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

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass l9vitedev-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;
    }
        
}
```

開発を進める上で``nginx``の設定を変更したい場合はここに記載することになります。

### phpのDockerfile等

まずは手動で``プロジェクトフォルダ/docker/php``フォルダを作ります。

``php``の``Dockerfile``を作ります。
同時に、最低限必要な設定である``php.ini``を記述します。

```Dockerfile:Dockerfile
FROM php:8.1-fpm
RUN apt-get update \
    && apt-get install -y \
    git \
    zip \
    unzip \
    vim \
    libfreetype6-dev \
    libjpeg62-turbo-dev \
    libmcrypt-dev \
    libpng-dev \
    libfontconfig1 \
    libxrender1

RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install gd
RUN docker-php-ext-install bcmath
RUN docker-php-ext-install pdo_mysql mysqli exif
RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer

ENV NODE_VERSION=16.14.0
RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
ENV NVM_DIR=/root/.nvm
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
RUN node --version
RUN npm --version

WORKDIR /src
ADD . /src/storage
RUN chown -R www-data:www-data /src/storage
```

大事なポイントは、``Composer``と``Node``を入れている部分です。Laravel 9では、Composerは2以上、Nodeは16以上が必要です。

開発中に``Node``のバージョンコントロールもしたい場合があるので、``NVM``も入れています。
``gd``などいくつかライブラリを入れていますが、このあたりは趣味（私の場合、自分がよく使うレンタルサーバーに寄せてる）なので好きなライブラリを入れましょう。

```php.ini
upload_max_filesize=256M
post_max_size=256M
```

上記では例として``upload_max_filesize``と``post_max_size``を指定していますが、特に設定がなければ空のファイルでもOKです。

## 実行テストする

ターミナル（WindowsならPowerShell）を起動し、プロジェクトフォルダに移動します。

たとえばWindowsのPowerShellを起動して、ドキュメントフォルダのQiitaフォルダに作った``l9vitedev``プロジェクトフォルダに移動した場合は：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/049f11a7-2340-60fa-3a51-208445b49da5.png)

ターミナルで、nginxとMySQLの起動に必要なフォルダを準備します。
※gitを使う場合、これらはignoreすべきで、gitからcloneした直後に実行すべきターミナルコマンドです。

```bash
# ログファイルを格納するディレクトリを作る（Gitを使う場合はこの``logs``フォルダをignoreすべき）
> mkdir ./docker/nginx/logs

# MySQLで使用するディレクトリを作る（Gitを使う場合はこの``mysql``フォルダをignoreすべき）
> mkdir ./docker/mysql
```

これでDocker起動に必要なすべてのファイルが揃いました。起動しましょう。

```bash
> docker compose up -d
```

うまく行けば以下のように４つのコンテナが正常起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/25e04b9e-844a-83e8-7d90-a33a1b67a1e7.png)

Webサーバーとして機能しているか確認するため、Chromeを開いて``http://localhost``にアクセスしてみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/81558109-6058-c4df-e5e8-1e288209f074.png)

まだドキュメントルートに何もアップロードしてないので「File not found.」で正解です。サーバーは正しく起動できました。

# Laravelをインストールする

Laravelのインストール先は、appサーバー＝phpサーバーです。
appサーバーに入って、composerを使ってLaravelをインストールします。

## appサーバーのコンソールに入る

```bash
> docker compose exec l9vitedev-app bash
```

以下のように入れたらOKです。

```bash
root@681925db0ad3:/src#
```

ここからは「`>`」がホストOS（ここではWindows）、「`$`」がDockerコンテナ内のOSとして表記します。
上記/src#の#部分が$だと思って読み替えてください。

## サブフォルダにLaravelをインストールする

Laravelをプロジェクトフォルダ（l9vitedevフォルダ）にインストールします。
ただ、プロジェクトフォルダにはすでにdocker関連ファイルがあるので、以下のようにサブフォルダ「l9vitedev_tmp」に一旦インストールし、その後中身をプロジェクトフォルダに移動することにします。

まずサブフォルダにLaravelをインストール。ここではバージョン９を指定しています。

```bash
$ composer create-project "laravel/laravel=9.*" l9vitedev_tmp --prefer-dist
```

次に中身をプロジェクトフォルダに移動し、一時サブフォルダを削除。

```bash
$ mv l9vitedev_tmp/* ./
$ mv l9vitedev_tmp/.* ./
$ rm l9vitedev_tmp -rf
```

このとき以下のように、/.や/..が移動できないというメッセージ以外に問題がなければ成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/7fd2651f-32ae-f03b-b32e-df29ce89c4f6.png)

最終的に、フォルダ構成が以下のようになるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/a7533eee-7221-8e31-820f-49bb45048995.png)

ここまで``root``ユーザーでLaravelをインストールしましたが、``storage``フォルダ以下のフォルダはWeb用のユーザー``www-data``が書き込みできなくてはいけないので、書き込み権限を付与しておきます。
これをやっておかないと、実行テストをした時点で「The stream or file "/src/storage/logs/laravel.log" could not be opened in append mode」というエラーになります。

```bash
$ chmod -R guo+w storage
```

[Laravel公式のstorage利用手順](https://laravel.com/docs/9.x/filesystem)にも指定のあるように、storageのシンボリックリンクを設置します。

```bash
$ php artisan storage:link
```

## 実行テストする

WebサーバーにアクセスするとLaravelが動作するはずなので、Chromeを開いて``http://localhost``にアクセスしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/a9822973-9dc6-3738-0e5c-02f7a5f7c385.png)

# Viteを使う

この記事ではViteを使ってCSSを変更し、その変更がフロントに反映されるまでを実装してみます。

## 依存のインストール

```bash
$ npm install
```

## ViewにViteを埋め込む

``\resources\views\welcome.blade.php``をエディタで開き、以下のようにViteを埋め込みます。

```php:welcome.blade.php
<!DOCTYPE html>
<html ...>
    <head>
        {{-- ... --}}
 
        @vite(['resources/css/app.css', 'resources/js/app.js'])
    </head>
```

埋め込もうとしている``app.css``や``app.js``は、Laravelインストール時に自動生成されているファイルです。

まずはこの状態で``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/7d28cfa5-c694-e630-ecc1-503069a7b9d3.png)

Viteを起動していないので、当然エラーになるわけです。

## Viteを起動する

次に、Viteを起動します。

```bash
$ npm run dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/7be1396b-9047-c604-d156-da20314c0ea9.png)

起動成功したので、``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/4fea6cbb-9139-d183-e7ca-127864db5361.png)

ブラウザのコンソールログにいくつかエラーが出ます。これは、Viteから提供されるはずのファイルが読めない状態です。
通信先は``127.0.0.1:5173``です。これが失敗しているのは、以下２つが原因です。
- そもそもポート5173がDocker外に開かれていない
- Viteは``Local:``は見ているが``Network:``は見ていない

先程Viteを起動したときに``Local: http://localhost:5173/``と表示されているのがローカル（appサーバー）に反応するという意味です。
``Network: use --host to expose``となっているのは、ネットワーク経由でもアクセスするなら``--host``オプションをつけろという意味です。

## appサーバーのポート5173を見せる

Docker外にいるブラウザから、appサーバー内にいるViteが見えるよう、ポート5173を見せるため、``docker-compose.yml``にポートを追記します。

```docker-compose.yml
...
  l9vitedev-app:
    container_name: "l9vitedev-app"
    build:
      context: ./docker/php
    depends_on:
      - l9vitedev-mysql
    ports:
      - 5173:5173
    volumes:
      - ./:/src
      - /src/node_modules
      - /src/vendor
      - ./docker/php/php.ini:/usr/local/etc/php/php.ini
...
```

``ports:``が追記されました。これでDocker外から``localhost:5173``にアクセスすると、appサーバーのポート5173につながります。

また、``/docker/php/Dockerfile``の先頭にもEXPOSEの設定が必要です。

```Dockerfile:Dockerfile
FROM php:8.1-fpm
EXPOSE 5173
RUN apt-get update \
...
```

Dockerfileへの変更を反映するには、Dockerコンテナを再起動する必要があります。
この時、``composer``と``npm``のinstallコマンドを再実行することに注意してください。理由はこの記事の下部に書きます。

```bash
> docker compose down
> docker compose up -d --build
> docker compose exec l9vitedev-app bash
$ composer install
$ npm install
```

## --host オプションを追加する

--host オプションは、Vite起動オプションなので``vite.config.js``または``npm run dev``の中身を変更することになります。どちらの方法でも大丈夫ですが、あとで``vite.config.js``は追記するので``vite.config.js``側に設定を集約するほうが後々楽でしょう。

``vite.config.js``で設定する場合は、以下のように``server`` ``host``を設定します。

```vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        ...
    ],
    server: {
        host: true,
    },
});
```

``package.json``で設定する場合は、以下のように--hostオプションを追記します。

```package.json
{
    "private": true,
    "scripts": {
        "dev": "vite --host",
        "build": "vite build"
    },
    ...
}
```

Viteを起動（すでに起動中の場合はCtrl+Cで停止してから再度起動）します。

```bash
$ npm run dev
```

今度は``Network:``も見ていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/9fd1c037-d8f0-dc7d-280c-f40e8983926f.png)

もう一度、``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/af411229-6e8f-40c9-4d83-c071331219cb.png)

またエラーが出ています。
今度は``http://[::]:5173/``以下のファイルが見えないというエラーです。
これはViteの真骨頂であるホットリロード機構``HMR``と通信できていないことを意味します。解決には、``HMR``の設定でホストを``localhost``にすればOKです。

```vite.config.js
...
    server: {
        host: true,
        hmr: {
            host: 'localhost',
        },
    },
});
```

ファイルを保存したら、Viteを再起動して、もう一度、``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/0e029031-323f-30d9-ee9f-8359c9c9511c.png)

コンソールのエラーが消えました。

## CSSを変更し、フロントに反映する

ではCSSを変更してみましょう。
``/resources/css/app.css``をエディタで開くと空ですので、以下のように入力し保存します。

```app.css
.underline{
  color: red;
}
```

ブラウザで``http://localhost``を見てみましょう。（Windowsでは）反映されていません。
※MacやLinuxの場合は問題が出ないと思われますので、読み飛ばしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/a761cc82-7a75-87e4-bf44-340f7cc08986.png)

Viteを再起動して、もう一度、``http://localhost``を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/d81800ec-2c06-08b9-f84e-e54c6464938c.png)

反映されました！
しかし、これではViteの最重要機能「CSS保存で瞬時に反映」が実現できていません。
問題は、appサーバー内でViteがファイル編集イベントに反応できていないことで起こっています。この問題はどうもWSL2に関連する問題のようで、[Vite公式でも言及されています](https://vitejs.dev/config/server-options.html)。

こういった場合、Viteをポーリングモードで動作させることができます。Viteの設定ファイルにポーリングモードで動作するよう``usePolling``を記述します。

```vite.config.js
...
    server: {
        host: true,
        hmr: {
            host: 'localhost',
        },
        watch: {
          usePolling: true,
        },
    },
});
```

Viteを再起動し、ブラウザで``http://localhost``を開き、リンク文字が赤いことを確認したら、以下のようにリンク文字のCSS設定を黄色にして保存してみましょう。

```app.css
.underline{
  color: yellow;
}
```

保存後すぐにViteが以下のように反応するはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/b33ccb22-b5e1-7642-134f-05e65f0b863f.png)

ブラウザ側でもすぐリンク文字が黄色くなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/e0576f03-ba19-83d6-0593-fc7837eb9472.png)

# Dockerコンテナの操作におけるノウハウ

## Dockerコンテナの起動と終了

起動

```bash
> docker compose up -d
```

リビルドと起動

```bash
> docker compose up -d --build
```

停止

```bash
> docker compose down
```

## Dockerコンテナ起動時に毎回実行すべきコマンド

今回は、動作を高速にするために``/src/node_modules``フォルダと``/src/vendor``フォルダはボリュームマウントから外してます。
これにより、``composer``や``npm``でインストールされた依存ライブラリはDockerコンテナを終了するごとに消えてしまいます。したがって、``docker compose up``するときは毎回直後に依存ライブラリをインストールする必要があります。

```bash
> docker compose up -d
> docker compose exec l9vitedev-app bash
$ composer install
$ npm install
```

面倒だという方は起動時自動実行シェルスクリプトを組み込むことをおすすめします。それも面倒なら、appサーバーのDockerfileに以下の行を入れれば幸せになれるかも。

```Dockerfile:Dockerfile
ENTRYPOINT [ "bash", "-c", "composer install; npm install; exec php-fpm" ]
```

以上です！お疲れ様でした！