tohokuaikiのチラシの裏

技術的ネタとか。

DockerでLaravel+Inertia.jsでReactを動かすところまでやってみる。

もうね、なんか大変だったので記事にとっておく。

参考にしたのは、Laravelの公式の方じゃなくてInertiajsの方

バージョンとか環境周りとか

このあたり重要。鳥獣用。OSはWindows11です。

バージョン

PowerShellから

 wsl --version
WSL バージョン: 2.2.4.0
カーネル バージョン: 5.15.153.1-2
WSLg バージョン: 1.0.61
MSRDC バージョン: 1.2.5326
Direct3D バージョン: 1.611.1-81528511
DXCore バージョン: 10.0.26091.1-240325-1447.ge-release
Windows バージョン: 10.0.22631.3737

使っているLinuxイメージは Ubuntu-24.04で、Dockerで使っているライブラリ情報は以下の通り

Ubuntu-24.04 > Docker version 26.1.4, build 5650f9b

ライブラリ バージョン
PHP 8.3.7
Laravel 11.9
React 18.3.1
inertiajs 0.11.1
DB 11.4.2-MariaDB-ubu2404

Docker

$ tree
.
├── docker
│   ├── app
│   │   └── Dockerfile
│   ├── compose.yaml
│   └── mariadb
│       ├── Dockerfile
│       └── my.cnf
└── src
    └── laravel
name: inertia_demo
services: 
  laravel: 
    container_name: laravel
    build: 
      dockerfile: ./app/Dockerfile
      context: .
      tags: 
       - "mylaravel:inertia_demo"
    image: "mylaravel:inertia_demo"
    environment:
      DB_HOST: mariadb
      DB_USERNAME: docker
      DB_PASSWORD: passwd
      DB_DATABASE: inertia_demo
    volumes: 
      - type: bind
        source: ../src/laravel
        target: /app
    ports:
      - "18000:8000"
      - "5173:5173"
    command: 
  mariadb:
    container_name: mariadb
    build:
      dockerfile: ./mariadb/Dockerfile
      context: .
    environment:
      MARIADB_ROOT_PASSWORD: passwd
      MARIADB_USER: docker
      MARIADB_PASSWORD: passwd
      MARIADB_DATABASE: inertia_demo
    volumes: 
      - type: volume
        source: mariadb
        target: /var/lib/mysql
      - type: bind
        source: ./mariadb
        target:  /docker-entrypoint-initdb.d
volumes:
  mariadb: 
FROM bitnami/laravel:11.1.1
FROM --platform=linux/amd64 mariadb:latest
COPY ./mariadb/my.cnf /etc/my.cnf
RUN chmod 644 /etc/my.cnf

Inertiajsが動くまで

Docker起動の時にLaravelが遅くなって困ったので注意

当初、Windows11のデスクトップにフォルダを作成して、wslからは cd /mnt/c/Users/itoh/Desktop/inertia_demo/docker とか作ってdocker compose upしてたらLaravelがすんげーおもくなった。dockerの起動やbindマウントはwsl内の記述にして、laravelなどのコードを触るときはwsl内のファイルをWindowsから \wsl.localhost\Ubuntu-24.04\home\t-ito\inertia_demo とかで覗きに行くようにしよう。

compose.yamlの場所が
ダメなパターン:C:\Users\itoh\Desktop\inertia_demo\docker\compose.yaml 良いパターン: \wsl.localhost\Ubuntu-24.04\home\t-ito\inertia_demo\docker\compose.yaml

bitnami/laravel のクセ

以下は、今回使ったDockerのbitnami/laravelについて。

起動スクリプトでartisan serveが実行される。

このDockerイメージを使うと、Laravelのインストールとartisan serveはDockerを起動した時点で既に行われているので、ポートフォワードした http://localhost:18000 にアクセスすればLaravelのトップページが見られる。

しかも、laravelの.envを編集した時に自動でartisan serveを再読み込み実行してくれる。

しかし当然、次回にdockerを再起動させるとインストール作業はされずにartisan serveされるが、このartisan serveがコケるとコンテナが起動しない。コンテナの/appディレクトリが空の時のみにLaravelのインストールがされるので、artisan serveが上がって来なかったら./src/laravelは別のディレクトリ名にして別の場所にbindマウントさせた上で./src/laravelディレクトリを改めて作ったうえでdocker compose upする必要がある。

したがって、「LaravelがWindows11だとおせー」とか言って、DockerでLaravelが重すぎたのでvendorとstorageをマウントしないことで解決した気がする #Docker - Qiitaにあるようにvendor / storage をbindマウントではなくvolumeマウントして解決しようとすると、「./src/laravaelにstorageやvendorがあるから、Laravelはインストールしないよ」ってbitnamiが判断してしまってartisan serveが起動せずにコンテナが上がってこないというハメになる。*1

composer や artisanコマンドで作るファイルがWSL側ではrootオーナーになってしまう。

docker内でartisanコマンドでScaffoldを作っていくと、WSL側から見たときにrootになってしまう。

じゃあ、ユーザー情報とグループ情報をWSLと同一にしてしまえばと

      - type: bind
        source: /etc/passwd
        target: /etc/passwd
        read_only: true
      - type: bind
        source: /etc/group
        target: /etc/group
        read_only: true

とかすると、bitnamiユーザーがいないということで chown: invalid user: 'bitnami:bitnami' が失敗してコンテナが起動しない。さらに言うと、chown bitnamiしてるってことは、WSLのitohユーザーでは編集できない。しゃーないので、起動後にWSL側から sudo chown -R itoh. laravel している。このあたり、なんとかならんかな…

LaravelでInertiajs(React)を動かすまで

ここから本番、前振り長い。長いけど、この前提をしっかりやんないとどっかでコケる。というか、もう最近の開発って前提長ないすか?Oracle VirtualBoxLAMP + Sambaだとこのあたり手間だけど難しくないのになーって思います。まぁいいや。

inertia-laravelのインストール

Dockerコンテナに docker container exec -ti laravel bash で接続して

# composer require inertiajs/inertia-laravel

MiddleWareの生成と登録

# ./artisan inertia:middleware
   INFO  Middleware [app/Http/Middleware/HandleInertiaRequests.php] created successfully.

で、Laravel11ではMiddlewareは./bootstrap/app.php

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\HandleInertiaRequests;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [
            HandleInertiaRequests::class,
            ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Viewの作成

さきほどのMiddlewareの app/Http/Middleware/HandleInertiaRequests.php には

<?php
protected $rootView = 'app';

という記述があったので、resources/views/app.blade.phpにInertiaの起点となるViewを作成する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Vite DEMO Top</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    @viteReactRefresh
    @vite(['resources/css/app.css', 'resources/js/app.jsx'])
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>

resources/js/app.jsだったのをReactなのでjsxにして、@viteReactRefreshを付け加えた。

Routing

Routeを routes/web.php に一番簡単に作る。

<?php

use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
    return Inertia::render('hello-world');
});

Reactをインストールして、Inertiajsの起点スクリプトを作る

npm install -D react react-dom @inertiajs/react @vitejs/plugin-react

先ほどの app.blade.phpには、@vite(['resources/css/app.css', 'resources/js/app.jsx'])とあったので、resources/js/app.jsx を記述する。

import React from 'react'
import {createRoot} from 'react-dom/client'
import {createInertiaApp } from '@inertiajs/react'
import {resolvePageComponent} from 'laravel-vite-plugin/inertia-helpers'

createInertiaApp({
    // Below you can see that we are going to get all React components from resources/js/Pages folder
    resolve: (name) => resolvePageComponent(`./Pages/${name}.jsx`,import.meta.glob('./Pages/**/*.jsx')),
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />) 
    },
})

Reactのページを作る

さきほどの、app.blade.phpには、 return Inertia::render('hello-world'); とあったので、hello-worldのページを作る。ファイルは、resources/js/Pages/hello-world.jsx

export default function HelloWorld() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}

npm run devでトランスパイル…なんだけど

Laravel11では、webpack/mixではなくてViteでトランスパイルする。package.josnにそう書いてある。

{
    "private": true,
    "type": "module",
    "scripts": {
        "dev": "vite",
        "build": "vite build"
    },

ということで、vite.config.js を調整

vite.config.jsの沼

デフォルトのままのvite.config.jsだとLaravelトップの http://localhost:18000/ にアクセスしても何も表示されない。

HTMLをChrome Developer toolsで見てみると

<html><head>
    <meta charset="utf-8">
    <title>Vite DEMO Top</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <script type="module">
    import RefreshRuntime from 'http://[::1]:5173/@react-refresh'
    RefreshRuntime.injectIntoGlobalHook(window)
    window.$RefreshReg$ = () => {}
    window.$RefreshSig$ = () => (type) => type
    window.__vite_plugin_react_preamble_installed__ = true
</script>
    <script type="module" src="http://[::1]:5173/@vite/client"></script>
    <link rel="stylesheet" href="http://[::1]:5173/resources/css/app.css">
    <script type="module" src="http://[::1]:5173/resources/js/app.jsx"></script>
  </head>
  <body>
    <div id="app" data-page="{省略}"></div>  
<deepl-input-controller></deepl-input-controller>
</body></html>

とかなってる。

http://[::1]:5173/@vite/client

とかなんじゃい?とソースコードを眺めてると、public/hot から取得してる模様。しかし、これがどこからきてるかよくわからない…そもそも、http://localhost:5173にアクセスしてもERR_EMPTY_RESPONSEでエラー表示。どうも serverのhost指定でnpmの立てるHTTPサーバーが制限されている模様。ということでvite.config.jsを次のように変えてみると、

/// これはうまくいかないvite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        react(), // React plugin that we installed for vite.js
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
    server: {
        host: true,
    }
});

http://localhost:5173/@vite/client にアクセスするとレスポンスがあった!!!…が404 forbidden…Docker内部から curl http://localhost:5173/@vite/client するとJavaScriptが取れてる!!!そのあとだとホストのWindows11ブラウザからアクセスしてもJavaScript見えてるじゃーーん!!!やったーー!!!…って、全然こんなの解決になってないわー。そもそも、http://localhost:18000/ にアクセスして得られるスクリプトタグのURLは <script type="module" src="http://[::]:5173/@vite/client"></script> と相変わらずだし。

で、こちらのページを見ると、解決方法が書いてあった。 qiita.com server.hmr.host: localhostをつける

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        react(), // React plugin that we installed for vite.js
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.jsx'],
            refresh: true,
        }),
    ],
    server: {
        host: true,
        hmr: {
            host: 'localhost', 
        },
    },
});

ここまで。なんか公式のドキュメント通りやっても通らずにすごい大変だった…npm installのところはあのパッケージでいいのかは

*1:しかし、おかげでbindマウントした中のディレクトリもVolumeマウントできるということを知れてよかった。