【Laravel 13】ReverbとEchoでリアルタイムチャットを作る手順|Lightsail + Ubuntu 24.04 + PHP 8.4
Laravel 13でリアルタイムチャットを作ってみました。
最初は「メッセージを投稿して、他のブラウザにも即時反映されるだけなら簡単では?」と思っていましたが、実際にやってみると、Laravel本体だけでなく、Reverb、Broadcasting、Echo、Vite、WebSocket用ポート、Supervisorなど、意外と多くの部品が関係していました。
完成形
今回作るのは、最小構成のリアルタイムチャットです。
できることは以下です。
/messagesにアクセスするとメッセージ一覧が表示される- フォームからメッセージを投稿できる
- 投稿内容はMySQLの
messagesテーブルに保存される - 別ブラウザで開いている画面にも、投稿がリアルタイム反映される
- Laravel ReverbをSupervisorで常駐化する
逆に、まだ作らないものは以下です。
- ログイン機能
- ユーザー名表示
- 複数チャットルーム
- 既読・未読
- HTTPS化
- 投稿削除・編集
まずは「Laravelでリアルタイムチャットの中核部分を理解する」ことを目的にします。
使用環境
今回の環境です。
- AWS Lightsail:AWSが提供する、VPS感覚で使えるシンプルなクラウドサーバーサービス。
- Ubuntu 24.04 LTS:長期サポート版のLinux OSで、今回のLaravelアプリを動かす土台。
- PHP 8.4:Laravel 13を動かすためのサーバーサイド言語実行環境。
- MySQL:投稿されたチャットメッセージを保存するデータベース。
- nginx:ブラウザからのHTTPアクセスを受けて、Laravelへ処理を渡すWebサーバー。
- Composer:Laravel本体やPHPライブラリをインストール・管理するPHP用パッケージ管理ツール。
- Node.js 22:ViteやLaravel Echoなど、フロントエンド関連のビルドに使うJavaScript実行環境。
- Laravel 13:今回のチャットアプリ本体を作るPHPフレームワーク。
- Laravel Reverb:Laravel公式のWebSocketサーバーで、リアルタイム通信を担当する。
- Laravel Echo:ブラウザ側でReverbからのリアルタイム通知を受け取るJavaScriptライブラリ。
- Vite:CSSやJavaScriptを本番用にまとめるフロントエンドビルドツール。
- Supervisor:Reverbなどの常駐プロセスを起動・監視・自動再起動するためのプロセス管理ツール。
Laravel 13はPHP 8.3以上が必要です。今回はPHP 8.4を使いました。
全体の仕組み
リアルタイムチャットの流れはこうです。
ユーザーAがメッセージ投稿
↓
Laravelがバリデーション
↓
messagesテーブルに保存
↓
MessageCreatedイベントをbroadcast
↓
Laravel ReverbがWebSocketで配信
↓
ユーザーBのブラウザでLaravel Echoが受信
↓
JavaScriptで画面にメッセージを追加
普通の投稿フォームなら、DBに保存してリダイレクトすれば終わりです。
リアルタイム化する場合は、DB保存後に「新しいメッセージができた」というイベントをWebSocketで他のブラウザへ通知します。
1. サーバーを用意する
Lightsailでは、LAMPブループリントではなく、OS OnlyのUbuntuを選びました。
Laravel 13を使うなら、最初からUbuntuを選んでPHP 8.4を入れる方がきれいです。
Lightsailの作成画面では、以下のようにしました。
Platform: Linux/Unix
Blueprint: OS Only
OS: Ubuntu 24.04 LTS
Plan: 2GB RAM以上
Static IP: 割り当てる
SSHユーザーはUbuntuの場合、通常は ubuntu です。
ssh -i "C:\Users\ユーザー名\.ssh\lightsail-key.pem" ubuntu@サーバーの固定IP
2. Ubuntuを更新する
まずはOSを更新します。
sudo apt update
sudo apt upgrade -y
sudo reboot
再接続後、タイムゾーンを日本にします。
sudo timedatectl set-timezone Asia/Tokyo
timedatectl
基本ツールも入れます。
sudo apt install -y curl git unzip ca-certificates lsb-release software-properties-common
3. swapを作る
2GB RAMのサーバーだと、Composerやnpmでメモリが苦しくなることがあります。
念のため2GBのswapを作りました。
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h
4. nginxとMySQLを入れる
sudo apt install -y nginx mysql-server
sudo systemctl enable nginx
sudo systemctl enable mysql
sudo systemctl status nginx --no-pager
sudo systemctl status mysql --no-pager
active (running) になっていればOKです。
5. PHP 8.4を入れる
Ubuntu標準のPHPではなく、PHP 8.4を使うためにPPAを追加します。
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
Laravelでよく使う拡張もまとめて入れます。
sudo apt install -y php8.4 php8.4-fpm php8.4-cli php8.4-mysql php8.4-mbstring php8.4-xml php8.4-curl php8.4-zip php8.4-bcmath php8.4-intl php8.4-gd php8.4-sqlite3
確認します。
php -v
php-fpm8.4 -v
php -m | grep -i sqlite
PHP 8.4.x、pdo_sqlite、sqlite3 が表示されればOKです。
6. Composerを入れる
cd ~
curl -sS https://getcomposer.org/installer -o composer-setup.php
php composer-setup.php
sudo mv composer.phar /usr/local/bin/composer
composer -V
7. Node.js 22を入れる
Ubuntu標準のNode.jsだと古く、Viteでエラーになることがあります。
今回もNode.js 18では以下のようなエラーになりました。
Vite requires Node.js version 20.19+ or 22.12+
Node.js 22を入れます。
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -v
8. Laravel用のDBを作る
MySQLに入ります。
sudo mysql
DBとユーザーを作ります。
CREATE DATABASE laravel13_chat CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'ここに強いパスワードを入れる';
GRANT ALL PRIVILEGES ON laravel13_chat.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
今回は日本語検索での意図しない一致を避けるため、照合順序は utf8mb4_general_ci にしました。
utf8mb4_unicode_ci は便利な面もありますが、日本語ではひらがな・カタカナ・濁音・半濁音などをかなり広く同一視することがあり、通常のチャット検索では違和感が出る可能性があります。
9. Laravel 13プロジェクトを作る
cd /var/www
sudo chown -R ubuntu:ubuntu /var/www
composer create-project laravel/laravel:^13.0 laravel13-chat
cd /var/www/laravel13-chat
php artisan --version
Laravel Framework 13.x と出ればOKです。
10. .envを設定する
.env を編集します。
nano .env
DB設定を変更します。
APP_URL=http://サーバーの固定IP
APP_LOCALE=ja
APP_FALLBACK_LOCALE=ja
APP_FAKER_LOCALE=ja_JP
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel13_chat
DB_USERNAME=laravel_user
DB_PASSWORD=ここにDBパスワードを入れる
マイグレーションを実行します。
php artisan migrate
11. Viteをビルドする
npm install
npm run build
LaravelではBladeを使う場合でも、CSSやJavaScriptはViteでビルドされます。
resources/js/app.js や resources/css/app.css を変更した場合、本番表示では npm run build が必要です。
12. nginxでLaravelを公開する
nginx設定を作ります。
sudo nano /etc/nginx/sites-available/laravel13-chat
以下を貼ります。
server {
listen 80;
listen [::]:80;
server_name サーバーの固定IP;
root /var/www/laravel13-chat/public;
index index.php index.html;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
error_page 404 /index.php;
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
有効化します。
sudo ln -s /etc/nginx/sites-available/laravel13-chat /etc/nginx/sites-enabled/laravel13-chat
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
権限も整えます。
cd /var/www/laravel13-chat
sudo chown -R ubuntu:www-data /var/www/laravel13-chat
sudo chmod -R 775 storage bootstrap/cache
ブラウザで確認します。
http://サーバーの固定IP
Laravelの初期画面が出れば成功です。
13. まず普通のメッセージ投稿機能を作る
ここからアプリ側の実装です。
まず、メッセージ用のModel、Migration、Controller、Request、Factory、Testを作ります。
php artisan make:model Message -mf
php artisan make:controller MessageController
php artisan make:request StoreMessageRequest
php artisan make:test MessagesTest
14. messagesテーブルを作る
database/migrations/xxxx_xx_xx_create_messages_table.php を編集します。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* マイグレーションを実行します。
*/
public function up(): void
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->timestamps();
$table->index('created_at');
});
}
/**
* マイグレーションを元に戻します。
*/
public function down(): void
{
Schema::dropIfExists('messages');
}
};
マイグレーションを実行します。
php artisan migrate
15. Messageモデルを作る
app/Models/Message.php です。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
/** @use HasFactory<\Database\Factories\MessageFactory> */
use HasFactory;
/**
* 一括代入できる属性です。
*
* @var list<string>
*/
protected $fillable = [
'body',
];
}
16. バリデーションを作る
app/Http/Requests/StoreMessageRequest.php です。
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreMessageRequest extends FormRequest
{
/**
* このリクエストを許可するか判定します。
*/
public function authorize(): bool
{
return true;
}
/**
* バリデーションルールを返します。
*
* @return array<string, list<string>|string>
*/
public function rules(): array
{
return [
'body' => ['required', 'string', 'max:1000'],
];
}
}
17. Controllerを作る
app/Http/Controllers/MessageController.php です。
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreMessageRequest;
use App\Models\Message;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
class MessageController extends Controller
{
/**
* メッセージ一覧を表示します。
*/
public function index(): View
{
return view('messages.index', [
'messages' => Message::query()
->latest()
->get(),
]);
}
/**
* メッセージを保存します。
*/
public function store(StoreMessageRequest $request): RedirectResponse
{
Message::create($request->validated());
return redirect()
->route('messages.index')
->with('status', 'メッセージを投稿しました。');
}
}
18. ルートを追加する
routes/web.php に追加します。
<?php
use App\Http\Controllers\MessageController;
use Illuminate\Support\Facades\Route;
Route::get('/', fn () => redirect()->route('messages.index'));
Route::get('/messages', [MessageController::class, 'index'])
->name('messages.index');
Route::post('/messages', [MessageController::class, 'store'])
->name('messages.store');
19. Blade画面を作る
resources/views/messages/index.blade.php を作ります。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Messages</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
<main class="mx-auto max-w-3xl p-6">
<header class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">Messages</h1>
<p class="text-sm text-gray-500">{{ $messages->count() }} total</p>
</header>
@if (session('status'))
<div class="mb-4 rounded border border-green-300 p-3 text-green-700">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('messages.store') }}" class="mb-6 rounded border p-4">
@csrf
<label for="body" class="mb-2 block font-semibold">Body</label>
<textarea
id="body"
name="body"
rows="4"
class="w-full rounded border p-2"
required
>{{ old('body') }}</textarea>
@error('body')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
<div class="mt-3 text-right">
<button type="submit" class="rounded bg-green-600 px-4 py-2 text-white">
Post
</button>
</div>
</form>
<section id="messages" class="space-y-3">
@foreach ($messages as $message)
<article class="rounded border p-4" data-message-id="{{ $message->id }}">
<p>{{ $message->body }}</p>
<time class="mt-2 block text-xs text-gray-500">
{{ $message->created_at->format('Y-m-d H:i') }}
</time>
</article>
@endforeach
</section>
</main>
</body>
</html>
この段階ではまだリアルタイムではありません。
ただし、ここまでで「普通の投稿型チャット」は動きます。
20. テストを書く
tests/Feature/MessagesTest.php で以下のような内容を確認します。
/messagesが表示できる- メッセージを投稿できる
bodyは必須bodyは1000文字まで- 1000文字を超えるとエラー
テストを実行します。
php artisan test
ここまで通れば、投稿型チャットとしては完成です。
21. Reverbをインストールする
次にリアルタイム化します。
php artisan install:broadcasting --reverb
途中でNode関連がうまく入らない場合は、手動で入れます。
npm install --save-dev laravel-echo pusher-js
22. .envにReverb設定を追加する
.env のReverb周辺を設定します。
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=任意のID
REVERB_APP_KEY=任意のキー
REVERB_APP_SECRET=任意のシークレット
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_HOST=サーバーの固定IP
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
ここで重要なのは、REVERB_HOST を localhost にしないことです。
ブラウザ上の localhost は、Lightsailサーバーではなく、自分のPCを指します。
そのため、外部ブラウザから接続するなら、REVERB_HOST はサーバーの固定IPやドメインにします。
23. イベントを作る
新しいメッセージを配信するイベントを作ります。
php artisan make:event MessageCreated
app/Events/MessageCreated.php を編集します。
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MessageCreated implements ShouldBroadcastNow
{
use Dispatchable;
use SerializesModels;
public function __construct(
public Message $message,
) {
}
/**
* イベントを配信するチャンネルを返します。
*/
public function broadcastOn(): Channel
{
return new Channel('messages');
}
/**
* ブラウザ側で受け取るイベント名を返します。
*/
public function broadcastAs(): string
{
return 'message.created';
}
/**
* ブラウザへ送るデータを返します。
*
* @return array<string, mixed>
*/
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'body' => $this->message->body,
'created_at' => $this->message->created_at?->format('Y-m-d H:i'),
];
}
}
今回は ShouldBroadcastNow を使っています。
そのため、キューを使わず即時配信されます。
ShouldBroadcast にする場合は、別途 queue:work が必要です。
24. 投稿時にイベントをbroadcastする
MessageController の store を変更します。
use App\Events\MessageCreated;
public function store(StoreMessageRequest $request): RedirectResponse
{
$message = Message::create($request->validated());
broadcast(new MessageCreated($message))->toOthers();
return redirect()
->route('messages.index')
->with('status', 'メッセージを投稿しました。');
}
toOthers() を使うことで、投稿者自身にはWebSocket通知を返さないようにしています。
これにより、投稿者側で同じメッセージが二重表示されるのを防ぎます。
25. Echoを初期化する
resources/js/echo.js を作ります。
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
26. app.jsでメッセージを受信する
resources/js/app.js でEchoを読み込みます。
import './bootstrap';
import './echo';
const messages = document.querySelector('#messages');
const template = document.querySelector('#message-template');
if (messages && template && window.Echo) {
window.Echo.channel('messages')
.listen('.message.created', (event) => {
if (document.querySelector([data-message-id="${event.id}"])) {
return;
}
const clone = template.content.cloneNode(true);
const article = clone.querySelector('[data-message-id]');
const body = clone.querySelector('[data-message-body]');
const createdAt = clone.querySelector('[data-message-created-at]');
article.dataset.messageId = event.id;
body.textContent = event.body;
createdAt.textContent = event.created_at;
messages.prepend(clone);
});
}
27. Bladeにtemplateを追加する
resources/views/messages/index.blade.php にリアルタイム追加用のtemplateを追加します。
<template id="message-template">
<article class="rounded border p-4" data-message-id="">
<p data-message-body></p>
<time class="mt-2 block text-xs text-gray-500" data-message-created-at></time>
</article>
</template>
既存のメッセージ一覧には data-message-id を付けておきます。
<article class="rounded border p-4" data-message-id="{{ $message->id }}">
<p>{{ $message->body }}</p>
<time class="mt-2 block text-xs text-gray-500">
{{ $message->created_at->format('Y-m-d H:i') }}
</time>
</article>
28. Viteを再ビルドする
.env の VITE_REVERB_* や resources/js/* を変更したら、必ずビルドします。
php artisan config:clear
npm run build
これを忘れると、ブラウザ側JavaScriptが古い設定のままになり、ws://localhost:8080 に接続しようとして失敗します。
29. Lightsailで8080番ポートを開ける
Reverbは8080番ポートで動かします。
LightsailのNetworkingで、IPv4 Firewallに以下を追加します。
Application: Custom
Protocol: TCP
Port: 8080
Source: Any IPv4 address
検証段階では Any IPv4 address で構いません。
本番では、ドメインやHTTPS構成、リバースプロキシを含めて考えた方が良いです。
30. Reverbを起動する
まず手動で起動します。
cd /var/www/laravel13-chat
php artisan reverb:start --host=0.0.0.0 --port=8080 --hostname=サーバーの固定IP
この状態でブラウザを2つ開きます。
http://サーバーの固定IP/messages
片方で投稿して、もう片方に即時反映されれば成功です。
31. SupervisorでReverbを常駐化する
手動起動のままだと、SSHを切るとReverbが止まります。
Supervisorで常駐化します。
sudo apt install -y supervisor
sudo systemctl enable supervisor
sudo systemctl start supervisor
設定ファイルを作ります。
sudo nano /etc/supervisor/conf.d/laravel-reverb.conf
中身は以下です。
[program:laravel-reverb]
process_name=%(program_name)s
command=/usr/bin/php /var/www/laravel13-chat/artisan reverb:start --host=0.0.0.0 --port=8080 --hostname=サーバーの固定IP
directory=/var/www/laravel13-chat
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-reverb.log
stopwaitsecs=10
Supervisorに読み込ませます。
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status
laravel-reverb RUNNING と出ればOKです。
ログ確認は以下です。
sudo tail -f /var/log/supervisor/laravel-reverb.log
32. Reverbを再起動したい場合
コードや設定を変えた場合は、Reverbを再起動します。
cd /var/www/laravel13-chat
php artisan reverb:restart
またはSupervisorから再起動します。
sudo supervisorctl restart laravel-reverb
33. よくハマったポイント
PHP 8.2ではLaravel 13が厳しい
Laravel 13を使うならPHP 8.3以上が必要です。
今回はPHP 8.4を入れました。
Node.js 18ではViteが動かなかった
Ubuntu標準の nodejs は古いことがあります。
今回はViteがNode.js 20.19以上または22.12以上を要求したため、Node.js 22に入れ替えました。
localhostはサーバーではなくブラウザ側のPC
今回一番分かりづらかったのがここです。
.env に以下のように書くと、ブラウザ側では自分のPCの localhost を見に行ってしまいます。
REVERB_HOST=localhost
外部ブラウザからLightsailのReverbへ接続するなら、固定IPまたはドメインを指定します。
REVERB_HOST=サーバーの固定IP
.envを変えたらnpm run buildが必要
VITE_ で始まる環境変数は、フロントエンドのビルドに埋め込まれます。
そのため、.env を変えたら以下が必要です。
php artisan config:clear
npm run build
8080番ポートを開けないとWebSocket接続できない
HTTPの80番だけでなく、Reverb用の8080番もLightsailのFirewallで許可する必要があります。
34. いまの構成で足りないもの
今回作ったのは、最小構成のリアルタイムチャットです。
実用化するなら、次は以下が必要です。
- ログイン機能
- ユーザー名表示
- 複数ルーム
- HTTPS化
APP_DEBUG=false- 投稿削除・編集
- スパム対策
- CSRFや認可の確認
- Reverbを443番経由で使う構成
.env秘密情報のローテーション
特に公開するなら、最低でも以下は必要です。
APP_ENV=production
APP_DEBUG=false
また、学習中に .env の中身をどこかに貼った場合は、以下を作り直した方が安全です。
APP_KEY- DBパスワード
REVERB_APP_KEYREVERB_APP_SECRET
まとめ
Laravel 13でリアルタイムチャットを作るには、単にフォームを作るだけではなく、以下の部品が関係します。
- LaravelのController / Request / Model / Migration
- Blade
- MySQL
- Broadcasting
- Laravel Reverb
- Laravel Echo
- Vite
- WebSocket用ポート
- Supervisor
最初は部品が多くて大変ですが、一度動くところまで作ると、流れはかなりシンプルです。
DBに保存
↓
イベントをbroadcast
↓
Reverbで配信
↓
Echoで受信
↓
画面に追加
この中核が理解できれば、ログイン付きチャットや複数ルーム、既読機能などにも発展させやすくなります。