【Laravel】Sailを使用せずにDockerで開発、TinyMCEを使用したWeb回覧システム設計

XserverにそのままアップできるLaravelプロジェクトをローカルで開発する

Xserver上では原則「composer は使えない」

※Xserverは共用サーバー。つまり、root権限はないし、OSレベルで自由にツールをインストールすることもできない。

→ローカルで composer install を済ませて vendor/ をUPする、npm run buildも同様

そもそも開発環境について整理すると

composer(PHPのパッケージマネージャー)、npm(Node.jsのpm)を2つ使用します!

Laravel + Vite構成で開発してる場合、開発サーバーは実質2つになりますね

項目内容
ComposerはXserverで使える?❌ ローカルで完了させて vendor/ をアップ
npm run build は必要?public/build/ に出力してアップ必須
artisanコマンドはXserverで?❌ 基本ローカルで済ませておく
public_html/ の中身は?index.php, .htaccess, build/, css/, etc
Laravel本体はどこに?public_html/ の外に laravel_project/ として設置

Dockerコンテナ作成手順(Sailでない)

Laravel Sailは、LaravelをDockerで簡単に構築できるツールで便利ですが、今回はローカルでのみcomposerを使用するので、本番でcomposerコマンドを使用しない想定なので使用しません、、

docker-compose.yml

laravel-xserver-dev/
├── docker/
│   ├── php/
│   │   └── Dockerfile
│   └── mysql/
├── docker-compose.yml
├── .env
└── laravel/        ← Laravel本体(後で Composer で生成)
services:
  app:
    build:
      context: .
      dockerfile: docker/php/Dockerfile
    container_name: laravel-app
    volumes:
      - ./laravel:/var/www
    ports:
      - "8000:8000"
    depends_on:
      - db
    working_dir: /var/www

  db:
    image: mysql:8.0
    container_name: laravel-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: laravel
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_USER: laravel
      MYSQL_PASSWORD: secret
    ports:
      - "3306:3306"
    volumes:
      - dbdata:/var/lib/mysql

volumes:
  dbdata:

Dockerfile

FROM php:8.2-fpm

RUN apt-get update && apt-get install -y \
    git curl zip unzip libonig-dev libxml2-dev \
    && docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath

# Composer install
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

WORKDIR /var/www

Laravelをインストール(コンテナ内で)

docker compose up -d
docker compose exec app bash
# コンテナ内で以下を実行
composer create-project laravel/laravel .
exit

結構時間かかります

タイムアウトしてしまったらcomposer config –global process-timeout 900

コンテナ内でLaravelがHTTPサーバーを立てる
→ホスト側(ブラウザ)から http://localhost:8000 でアクセス可能になる。

docker compose exec app php artisan serve --host=0.0.0.0 --port=8000
サーバを 0.0.0.0 にバインド(全てのインターフェースでListen)にしておく

Laravelの開発サーバはデフォルトで 127.0.0.1:8000(localhost)(ループバック)で起動ます

そしてコンテナ内部のネットワークのため

php artisan serveのみだとホストからアクセスできず、「–host=0.0.0.0 –port=8000」が必要です

(全てのインターフェースでListen→ポートをマッピング)

※Laravelは、起動時に環境変数を読み込んで設定クラスにバインドします

どの.env?

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel
DB_PASSWORD=secret
.env のキー反映される場所
APP_ENVconfig/app.php'env' => env('APP_ENV')
APP_DEBUGconfig/app.php'debug' => env('APP_DEBUG')
DB_*config/database.php で使用される
MAIL_*config/mail.php で使用される

マウントの設定を修正し、スピードパフォーマンスを改善

原因はvendorとstorageがDocker環境でファイルI/O(Input/Output)が極端に遅くなるから

Laravelは、リクエストごとに以下を行う:

  • vendor/autoload.php からオートロード
  • .env を読んで構成
  • storage/logs/ にログ書き込み
  • cache/, sessions/, views/ を操作

つまり、毎回100回以上ディスクアクセスしてる
そのたびに「ホストマシンとコンテナ間のファイルシステム同期」が発生してたら、そりゃ爆遅に

DockerでLaravelが重すぎたのでvendorとstorageをマウントしないことで解決した気がする
https://qiita.com/ryo_one/items/cced7c7b6e21527ad81e

名前付きボリューム初期化手順

毎回同期をしない名前付きボリュームを導入すればOK

「バインドマウント」と「ボリュームマウント」については下記の記事参考に

Dockerのマウントについて
https://zenn.dev/randd_inc/articles/84ac7de7f22800

docker-compose.ymlに名前付きボリュームの設定を追記

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: laravel-app-fast
    volumes:
      - ./laravel:/var/www
      
      - storage-volume:/var/www/html/storage/framework
      - vendor-volume:/var/www/html/vendor
    ports:
      - "8000:8000"

    ...


volumes:
  dbdata:
  storage-volume:
  vendor-volume:

コンテナとVolumeを含めて完全再起動(vendor-volがマウントされる)

docker-compose down -v
docker-compose up -d

vendorが空になるので、Volume上に再構築

docker-compose exec app composer install

   INFO  Discovering packages.

  laravel/pail ................................................................................................................... DONE
  laravel/sail ................................................................................................................... DONE
  laravel/tinker ................................................................................................................. DONE
  nesbot/carbon .................................................................................................................. DONE
  nunomaduro/collision ........................................................................................................... DONE
  nunomaduro/termwind ............................................................................................................ DONE

79 packages you are using are looking for funding.
Use the composer fund command to find out more!


# Laravelのキャッシュ・権限系を整える
docker-compose exec app php artisan config:clear
docker-compose exec app chmod -R 777 storage bootstrap/cache

# アクセス確認(localhost:8000)
docker-compose exec app php artisan serve --host=0.0.0.0 --port=8000

名前付きボリュームが正しく設定されているか確認

docker-compose exec app ls /var/www/html/
storage  vendor

docker-compose exec app mount | grep /var/www/html/vendor
/dev/sde on /var/www/html/vendor type ext4 (rw,relatime)

/dev/sde はDockerの内部(ext4)のボリューム領域です、つまりホストにマウント(同期)していないことが分かります

Named Volumeは、Dockerが管理する永続的なデータ保存領域

項目Bind MountNamed Volume
書き方./laravel:/var/wwwvendor-volume:/var/www/vendor
保存場所ホストのフォルダDocker内部領域
速度遅い(特にWindows/Mac)高速
ホストからアクセス可能不可
永続性ホストファイルに依存コンテナ削除後も残る
リアルタイム同期ありなし

FTPでxservernのサブディレクトリにデプロイする場合、本番サーバーでもローカルと同じ構成にするのが、ベストプラクティス

vendor/ は 1万ファイル以上あるからFTPクライアント(WinSCPやFileZilla)でアップするのに時間かかります。

Xserver/public_html/
└── laravel-xserver/
    ├── app/
    ├── public/(中身あり)
    ├── .env
    ├── index.php      ← public/index.phpを書換コピペ
    ├── .htaccess      ← public/.htaccessをコピー
    └── ... その他Laravel一式
  • public/index.php → laravel-xserver/index.php にコピーして、以下のように修正:
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
  • public/.htaccess → laravel-xserver/.htaccess にそのままコピー

環境変数を編集してデプロイ

ローカルと本番のDBは「違って当然」、だからマイグレーションで定義する。

XserverはMySQLをサービスとして提供している

┌─────────────┐           ┌────────────┐
│ ローカルPC  │           │   Xserver  │
│ Docker(MySQL)│          │  MySQL提供  │
│ Laravel dev │           │ Laravel prod │
└────┬────────┘           └────┬───────┘
     │ DB接続 → localhost       │ DB接続 → mysql1234.xserver.jp
     │                         │
     │ migrate                 │ 手動インポート(phpMyAdmin)
     ▼                         ▼
  OK                          OK
内容ローカル本番
DBサーバーDockerで構築したMySQLXserverのMySQL(管理画面で作成)
接続情報.envDB_HOST=127.0.0.1.envDB_HOST=mysql1234.xserver.jp
DB中身(データ)開発用のサンプル/テストデータ本番の実データ
DB構造(テーブル定義)Laravelのマイグレーションで定義する← 同じマイグレーションで生成される

構造(スキーマ)をマイグレーションで共通化し、
中身(データ)は環境に応じて分ける

https://github.com/idw-coder/laravel-xserver

XserverでMySQLデータベースのデータ構造を反映する手段

  • SSH接続でphp artisan migrate
  • phpMyAdminでインポート

phpMyAdminでインポート方法

レンタルサーバーではDB接続はSSHでなくphpMyAdminからインポートはあり

むしろXserverのような共有ホスティングでは、SSHやCLIよりphpMyAdminの方が標準手段。

ローカルMySQLコンテナから .sql ファイルをエクスポートする

ローカルDBをダンプ

docker exec [DBコンテナ名] mysqldump -u root -p laravel > /tmp/laravel.sql

コンテナからホストにコピー

docker cp laravel-db:/tmp/backup.sql ./backup.sql

xserverレンタルサーバーコントロールパネル画面から「phpMyAdmin」を開き上記で生成したファイルをインポート

レンタルサーバーにssh接続してphp artisan migrate

client_loop: send disconnect: Connection reset
PS C:\Users\user\.ssh> ssh server
Last login: Wed Jun 25 06:38:32 2025 from 192.168.1.100
[user@server ~]$ cd mysite.com/public_html/laravel-app/
[user@server laravel-app]$ php8.3 artisan migrate
APPLICATION IN PRODUCTION.
Are you sure you want to run this command?
Yes
INFO
Running migrations.
2025_06_24_093122_add_qualification_and_role_to_users_table
.....................................................................
100.27ms
DONE

laravel Breeze

前提条件

  • Dockerでローカル開発し、xserver(レンタルサーバー)にデプロイしてます
  • composer が使える(Xserver本番環境は使えない、ローカルでやる)
  • npm が使える(ローカルでフロントビルド用)
  • .env に DB 接続ができている(マイグレーションで users テーブルが作られる)

Breeze インストール

Laravel Breezeパッケージがプロジェクトの開発依存関係(dev-dependencies)としてインストールされます

composer require laravel/breeze --dev

インストール後は以下のコマンドを実行して認証システムをセットアップします

php artisan breeze:install
root@dc586cadb708:/var/www# php artisan breeze:install Which Breeze stack would you like to install? ● Blade with Alpine ○ Livewire (Volt Class API) with Alpine ○ Livewire (Volt Functional API) with Alpine ○ React with Inertia ○ Vue with Inertia ○ API only

● Blade with Alpineを選択

選択肢内容
Blade with Alpine最もシンプルな構成。Laravel標準のBladeテンプレート + Alpine.js(軽量JS)
Livewire(Volt API)フロントのリアクティブ処理をPHPだけで書ける魔改造スタック。SPA風味
React/Vue with InertiaガチのSPA構成。React/VueとLaravelの融合
API only認証付きAPI構成(SPAやモバイル用)
Which testing framework do you prefer? ● Pest ○ PHPUnit

今度はテストフレームワークの選択

● Pestを選択

項目PestPHPUnit
記述の簡潔さ✅ 圧倒的に短くて読みやすい❌ 冗長なクラス宣言が必須
学習コスト✅ 初心者でも感覚的に書ける⛔ 「setUp()」だの「TestCase」だの面倒
Laravel公式推し✅ Breezeのデフォルト選択✅ 同じくサポートされてるが古い

npmビルドはホスト側で(Dockerコンテナ内ではなく)

Node.js入りDockerは構成が複雑になりすぎる

Breezeは「npmビルドさえ済めば」それでOK

cd \laravel-xserver-dev\laravel
npm install --save-dev vite
npm run build

xserver デプロイ用にパスを修正する

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
    base: '/laravel-xserver/build/', // xserverデプロイ時の構成に合わせて設定
});

今度はDockerコンテナ内で

php artisan migrate

http://localhost:8000/registerにアクセスで認証画面の確認OK

Name Email Password Confirm Password Already registered? REGISTER

現状の整理

laravel/
├── app/
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Auth/         ← Breezeの認証系コントローラ
│   │   │   └── Controller.php
├── database/
│   ├── migrations/           ← users, sessionsなどが定義されてる
├── resources/
│   ├── views/
│   │   ├── auth/             ← login, registerなど
│   │   ├── layouts/          ← app.blade.php 等のレイアウト
├── routes/
│   ├── web.php               ← Breezeが登録ルート追加済
├── public/
│   ├── build/                ← npm run build で生成されたフロントJS

laravel\app\Http\Requests\Auth\LoginRequest.php

メールアドレスではなくIDで認証できるように変更(Breeze)

ログイン機能をadmin_idを使用するように変更し、関連するバリデーションとエラーメッセージを更新。ユーザーモデルにadmin_idでユーザーを検索するメソッドを追加。ユーザー一覧ビューを改善し、テーブルにadmin_idを表示。ログインフォームのラベルと入力フィールドを更新。
https://github.com/idw-coder/laravel-xserver/commit/e00b58546117998e2cf6096fc89fb55817d54866

ユーザー新規作成をログインしている特定の権限のユーザーのみに公開

npm run buildでフロントのビルド後xserverのサブディレクトリにデプロイしたら表示崩れ、、

原因①「Laravelアプリを本番でサブディレクトリ配下にデプロイする」とローカルとディレクトリ構成がズレる→パスがおかしくなる

結局、臨時処置ですが、public配下のbuildディレクトリをコピーし、publicディレクトリと同配下に複製しました。

具体的には私はxserverのサブディレクトリにデプロイしたのですが、以下の通りです

local

/project-dir/
 ┣ docker
 ┣ laravel
 ┃  ┣ public
   ┃    ┣ build
   ┃    ┃  ┣ assets
   ┃    ┃  ┃  ┣ app-xx.js     ※デプロイしたら移動
   ┃    ┃  ┃  ┣ app-xx.css    ※デプロイしたら移動
   ┃    ┃  ┗ manifest.json
   ┃    ┃
   ┃    ┣ js

xserver

/your-domain.com/public_html/laravel-dir/
 ┣ public 
 ┃  ┣ js
 ┃  ┣ build
 ┃  ┃  ┗ manifest.json
 ┃
 ┣ build            ※public配下とは別で作成
 ┃  ┗ assets        ※public配下から移動
 ┃    ┣ app-xx.js
 ┃    ┗ app-xx.css
 ┃

表示はくずれなくなりました。


原因②Laravel はまだ「開発モード」と判断した状態でviteでbuildしてしまった場合

<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.js">

上記のように検証画面でソースコードみると、Vite Dev Server(http://[::1]:5173)を使おうとしているパスのままであればVite の開発モードの挙動です

.env(ローカルでビルドしてるからローカルの.env)を修正したらどうだろう、、

# APP_DEBUG=true
APP_DEBUG=false
APP_URL=http://localhost:8000 # @vite() とは無関係
# APP_ENV=local
APP_ENV=production

でもかわらず、、

npm run build で埋め込まれる URL(例:http://[::1]:5173)は、Laravel の .env じゃなくて Vite 側の環境変数定義から来てる。

つまり

APP_ENV=production とか Laravel 側にいくら書いても、
❌ Vite 側には 伝わらない

そもそも@vite()の仕様を確認すると(下記resources\views\welcome.blade.php)

<!-- Styles / Scripts -->
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
@vite(['resources/css/app.css', 'resources/js/app.js'])
@else

アセットバンドル(Vite)
https://laravel.com/docs/12.x/vite

Laravel Viteでフロントと管理画面のビルドを分ける方法
https://www.webopixel.net/php/1796.html

そもそもローカルでnpm run build → FTPでデプロイがまずいのか、、本番でbuildしないといけないのか、、

あきらめかけていたとこに原因判明「hot ファイル」

🔥 hot ファイルとは?
npm run dev(開発モード)を実行したときに public/hot が作成されます。

Laravel は public/hot が存在すると manifest.json を無視して localhost:5173 を参照します。

$ ls -l public
合計 12
drwx---r-x 3 jingtian members  53  6月 13 06:27 build
-rw----r-- 1 jingtian members   0  5月 27 02:17 favicon.ico
-rw----r-- 1 jingtian members  17  6月 21 08:42 hot
-rw----r-- 1 jingtian members 543  5月 27 02:17 index.php
drwx---r-x 3 jingtian members  29  6月 18 21:21 js
-rw----r-- 1 jingtian members  24  5月 27 02:17 robots.txt
drwx---r-x 2 jingtian members 290  6月 25 06:27 uploads

rm public/hot

ユーザー一覧画面を作成(練習)

https://github.com/idw-coder/laravel-xserver/blob/dev_breeze/laravel/resources/views/users/index.blade.php

Undefined variable $slot

上記エラー発生

{{ $slot }} を使おうとしているにも関わらず、その変数が定義されていないことが原因

こちらおそらくlaravel Breezeをインストールした際、自動生成された下記コンポーネント

<!-- resources/views/components/app-layout.blade.php -->
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ config('app.name', 'Laravel') }}</title>
    <!-- CSS読み込み -->
</head>
<body>
    <div class="min-h-screen bg-gray-100">
        <!-- ナビゲーション -->
        @include('layouts.navigation')
        
        <!-- ページヘッダー -->
        @if (isset($header))
            <header class="bg-white shadow">
                <div class="max-w-7xl mx-auto py-6 px-4">
                    {{ $header }} <!-- 名前付きスロット -->
                </div>
            </header>
        @endif
        
        <!-- メインコンテンツ -->
        <main>
            {{ $slot }} <!-- メインのコンテンツエリア -->
        </main>
    </div>
</body>
</html>

上記に合わせて形でlaravel/resources/views/users/index.blade.phpを修正でエラー解消

そもそも$slotってなに?

$slot:コンポーネントタグの開始タグと終了タグの間に書かれた内容を受け取る変数

項目従来の方法Bladeコンポーネント
主な機能@yield / @section$slot
ファイル名layouts/app.blade.phpcomponents/app-layout.blade.php
使用方法@extends(‘layouts.app’)<x-app-layout>
内容の渡し方@section(‘content’)タグの中に直接記述
導入時期Laravel初期からLaravel 7以降

Breezeをインストールすると、新しい方式のBladeコンポーネントが使われるため、$slotを見かけるようになります。これは正常な動作で、現在のLaravelで推奨されている方法です。

posts テーブルの作成

新しいマイグレーションファイルが生成されます

php artisan make:migration create_posts_table --create=posts

Enum = Enumeration(列挙型)で管理

Enum = 選択肢を限定する仕組み

  • 従来: 文字列なので何でも入る → バグの原因
  • Enum: 決められた値だけ → 安全

つまり、「決められたルールの中でしか選べない」ようにして、プログラムをより安全にする技術です!

Laravel Enum完全ガイド

https://github.com/idw-coder/laravel-xserver/blob/add_post/laravel_enum_guide.md

マイグレーション実行

php artisan migrate:status で現在の状況を確認
php artisan migrate

Tinkerを使ってテストデータを作成

php artisan tinker
  1. 必要なクラスを読み込み
>>> use App\Models\{User, Post};
>>> use App\Enums\PostStatus;
  1. ユーザーのIDを確認
>>> User::count()
>>> $user = User::first();
>>> $user
  1. 投稿データを作成
>>> Post::create([
...   'user_id' => $user->id,
...   'title' => 'はじめての投稿',
...   'slug' => 'first-post',
...   'body' => 'これは最初のテスト投稿です。',
...   'status' => PostStatus::DRAFT
... ]);
  1. 作成されたか確認
>>> Post::count()
>>> Post::first()

本番環境にPOSTのデータ構造を反映する

xserverにssh接続してmigrateする場合

もしphp コマンドのバージョンエラーが起きたら下記記事を参考に

[username@sv12345 laravel-xserver]$ php8.3 artisan migrate

   INFO  Running migrations.  

  2025_06_14_074533_create_posts_table .............. 87.84ms DONE

php my adminでも確認できました。

表示 構造 SQL 検索 挿入 エクスポート インポート 操作 トリガ テーブルの構造 リレーションビュー # 名前 タイプ 照合順序 属性 Null デフォルト値 コメント その他 操作 1 id 🔑 bigint(20) UNSIGNED いいえ なし AUTO_INCREMENT ✏️ 変更 ❌ 削除 2 user_id 🔑 bigint(20) UNSIGNED いいえ なし ✏️ 変更 ❌ 削除 3 title varchar(255) utf8mb4_unicode_ci いいえ なし ✏️ 変更 ❌ 削除 4 slug 🔑 varchar(255) utf8mb4_unicode_ci いいえ なし ✏️ 変更 ❌ 削除 5 excerpt text utf8mb4_unicode_ci はい NULL ✏️ 変更 ❌ 削除 6 body longtext utf8mb4_unicode_ci いいえ なし ✏️ 変更 ❌ 削除 7 status 🔑 enum(‘draft’,’published’,’archived’) utf8mb4_unicode_ci いいえ draft ✏️ 変更 ❌ 削除 8 published_at 🔑 timestamp はい NULL ✏️ 変更 ❌ 削除 9 created_at timestamp はい NULL ✏️ 変更 ❌ 削除 10 updated_at timestamp はい NULL ✏️ 変更 ❌ 削除 11 deleted_at timestamp はい NULL ✏️ 変更 ❌ 削除 すべてチェックする 表示 変更 削除 ユニーク インデックス 空間 全文

User → Post (HasMany)で既存のUserテーブルと関連づけ、詳細は下記のPDFリンクから👇

https://github.com/idw-coder/laravel-xserver/blob/add_post/blog_erd.md

PostControllerを作成

php artisan make:controller PostController --resource

LaravelにWYSIWYGを実装

WYSIWYGのメリデメ

  • ユーザーにとって使いやすい(リンク、見出し、画像アップ)
  • 実装が難しい(JSライブラリ + Upload + XSS対策)
  • 要サニタイズ処理必須(XSSの温床)
  • 簡単に実装できるのはMarkdown

TinyMCE

TinyMCEは「WYSIWYGエディタの具体的な製品・ライブラリの名前」

https://www.tiny.cloud

TinyMCE 無料 (Cloud) プランの制限

無料枠では月間 1,000 エディタ読み込み(editor load)まで。それを超えるとエディタが読み込み専用モードになり、閲覧はできても編集不可になる

100ユーザーが各10ページでTinyMCEを起動すれば、100 × 10 = 1000

方法概要
Self-hostedCDN 経由ではなく、自前サーバや public/static に TinyMCE スクリプトを置けば、読み込み回数に制限なし(tiny.cloud)。
有料プランへ移行5,000 loads/月の Essential ($79/月)、20,000 loads/月の Professional などから選べる(超過分も課金)。

動作確認手順

  1. npm install fs-extra
  2. npm run dev(またはnpm run build
  3. public/js/tinymce/にコピーされているか確認