目次
はじめに
ソーイ株式会社の臼井です。
業務では、CloudWatch Logsを利用して不具合の原因調査や、ユーザーから報告された事象の調査を行うことがあります。
CloudWatch Logsで調査することで、不具合そのものだけでなく、「この情報もログに残っていれば、さらに調査しやすかった」と気づくこともあります。
今後ログを増やしていくにあたり、CloudWatch Logsについて改めて理解しておきたいと考えました。
そこで、Dockerを利用したLaravelの環境構築から始め、認証機能とWebSocketを利用した簡単なリアルタイムチャットを作成し、その処理で出力されるログをCloudWatch Logsへ送信してみます。
前編となるこの記事では、
DockerでLaravel + Vue環境を構築
↓
Laravel Reverbを導入
↓
ユーザー登録・ログイン
↓
メッセージをLaravelへ送信
↓
MySQLへ保存
↓
Laravelでログを出力
↓
WebSocketで別ユーザーへリアルタイム配信
というところまで進めます。
後編では、前編でLog::info()などを利用して出力したログをCloudWatch Logsへ送信し、AWS上でログを確認・検索していきます。
想定読者
- Dockerを使ってLaravel + Vueの環境を構築してみたい方
- LaravelでWebSocketを使ってみたい方
- Laravel Reverbを触ってみたい方
- 簡単なリアルタイム通信を実装してみたい方
- CloudWatch Logsを利用したログ調査に興味がある方
この記事で伝えたいこと
前編では、Laravel + Vue + Laravel Reverbを利用して、WebSocketによるリアルタイム通信を実際に動かします。
メッセージを送信すると、
Vue
↓ HTTP
Laravel
↓
MySQLへ保存
↓
MessageSentイベント
↓
Laravel Reverb
↓ WebSocket
別のブラウザ
という流れで、別のログインユーザーへリアルタイムにメッセージが届くところまで確認します。
また、Laravel側ではメッセージ送信時にログを出力します。
後編では、このログをCloudWatch Logsへ送信して、不具合調査に利用できる形にしていきます。
開発環境
今回の環境は以下です。
| 項目 | 環境 |
|---|---|
| OS | Windows 11 Home |
| Docker | Docker Desktop |
| PHP | 8.5 |
| Laravel | 13 |
| Vue | Vue 3 |
| Database | MySQL 8.4 |
| WebSocket | Laravel Reverb |
| WebSocket Client | Laravel Echo |
| JavaScript Build | Vite |
| Node.js | Node.js 22 |
今回作成するアプリの構成
今回の構成は以下です。
メッセージを登録する処理自体はHTTP通信です。
Vue
↓ POST /messages
Laravel
↓
MySQL
メッセージを保存したあと、LaravelからMessageSentイベントをBroadcastします。
Laravel
↓
MessageSent
↓
Laravel Reverb
↓ WebSocket
接続中のブラウザ
ここで重要なのは、すべてをWebSocketで通信するわけではないという点です。
今回は、
メッセージを登録する
→ HTTP
登録されたメッセージを他ユーザーへ通知する
→ WebSocket
という役割分担にします。
Dockerの導入
Docker Desktopをインストールします。
今回はWindowsのコマンドプロンプトから作業します。
インストール後、以下を実行します。
docker --version
docker compose version
私の環境では以下のバージョンを使用しました。
Docker version 29.8.0, build 88096ef
Docker Compose version v5.5.1
バージョンが表示されれば準備完了です。
既にDocker Desktopをインストールしていた場合は、必要に応じて右上の通知からDocker Desktopを最新の状態へ更新します。
Laravel + Vueの環境構築
Laravelをインストールする
今回はWindows側のC:\php配下にLaravelプロジェクトを作成します。
まず、以下へ移動します。
cd C:\php
ComposerのDockerイメージを利用してLaravelプロジェクトを作成します。
docker run --rm -v "%cd%:/app" -w /app composer create-project laravel/laravel cloudwatch-chat
作成したプロジェクトへ移動します。
cd cloudwatch-chat
以降のコマンドは、C:\php\cloudwatch-chatで実行します。
Dockerfileを作成する
プロジェクト直下にDockerfileを作成します。
FROM php:8.5-cli
RUN apt-get update \
&& apt-get install -y git unzip libzip-dev \
&& docker-php-ext-install pdo_mysql pcntl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
pdo_mysqlはLaravelからMySQLへ接続するために使用します。
また、Laravel Reverbではシグナルを扱うため、pcntlも有効にしています。
docker-compose.ymlを作成する
プロジェクト直下にdocker-compose.ymlを作成します。
今回は、
Laravel
Laravel Reverb
Node.js
MySQL
をそれぞれDockerで動かします。
services:
app:
build:
context: .
ports:
- "8000:8000"
volumes:
- ./:/var/www/html
depends_on:
- db
reverb:
build:
context: .
command: php artisan reverb:start --host=0.0.0.0 --port=8080
ports:
- "8080:8080"
volumes:
- ./:/var/www/html
depends_on:
- app
node:
image: node:22
working_dir: /var/www/html
command: npm run dev -- --host 0.0.0.0
ports:
- "5173:5173"
volumes:
- ./:/var/www/html
- node_modules:/var/www/html/node_modules
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: chat
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: laravel
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
volumes:
mysql-data:
node_modules:
Laravel Reverbは通常のLaravelとは別の常駐プロセスとして動作するため、reverbサービスを分けています。
また、node_modulesについては名前付きVolumeを使用します。
- node_modules:/var/www/html/node_modules
Windows側のnode_modulesをLinuxコンテナから直接操作すると、環境によっては権限エラーが発生するためです。
実際にWindows側のnode_modulesを直接利用した際、
EACCES: permission denied
というエラーが発生しました。
MySQLを設定する
.envのDB設定を以下に変更します。
APP_URL=http://localhost:8000
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=chat
DB_USERNAME=laravel
DB_PASSWORD=password
MySQLはDockerコンテナ内で動かすため、Windows側に別途MySQLをインストールする必要はありません。
Laravelコンテナから見たMySQLのホスト名は、Composeのサービス名であるdbになります。
npmパッケージをインストールする
Laravelプロジェクトに最初から定義されているnpmパッケージをインストールします。
docker compose run --rm node npm install
node_modulesは先ほど作成したDocker Volumeへ保存されます。
Laravel・MySQL・Node.jsを起動する
この時点ではまだLaravel Reverbをインストールしていないため、reverbサービスは起動しません。
まず、
docker compose up -d --build app db node
を実行します。
起動状態を確認します。
docker compose ps
この時点では、
app
db
node
が起動していれば問題ありません。
migrationを実行する
DB設定を反映するため、念のためLaravelの設定キャッシュを削除します。
docker compose exec app php artisan config:clear
続いてmigrationを実行します。
docker compose exec app php artisan migrate
MySQLの起動直後に接続エラーになる場合は、数秒待ってから再度実行してください。
WebSocketについて
WebSocketは、ブラウザとサーバーの間で接続を維持し、双方向にデータを送受信できる通信方式です。
通常のHTTP通信では、
ブラウザ
↓ Request
サーバー
↓ Response
ブラウザ
という流れが基本です。
一方、WebSocketでは一度接続を確立すると、その接続を維持できます。
ブラウザ
↕
WebSocket
↕
サーバー
そのため、サーバー側でイベントが発生したタイミングでブラウザへデータを送信できます。
リアルタイムチャットでは、別のユーザーがメッセージを送信した際に、その内容を他のユーザーの画面へすぐ反映する必要があります。
今回のアプリでは、
メッセージ送信
↓
HTTPでLaravelへ送信
↓
MySQLへ保存
↓
イベントをBroadcast
↓
WebSocketで別ユーザーへ通知
↓
画面へリアルタイムに反映
という構成にします。
WebSocketサーバーにはLaravel Reverbを利用します。
ブラウザ側ではLaravel Echoを利用して、Laravel Reverbから送られてくるイベントを受信します。
Laravel Reverbの導入
Laravelではinstall:broadcastingコマンドを利用してBroadcastingを有効化できます。
ただし、今回の構成ではPHPとNode.jsを別々のDockerコンテナにしています。
通常のinstall:broadcasting --reverbではNode.jsのパッケージもインストールしようとしますが、appコンテナにはnpmを入れていません。
そのため、サーバー側とNode.js側を分けてインストールします。
Reverbをインストールする
まずLaravel ReverbをComposerでインストールします。
docker compose exec app composer require laravel/reverb -W
-Wを付けることで、必要に応じて関連する依存パッケージも更新できるようにしています。
続いてBroadcastingを有効化します。
docker compose exec app php artisan install:broadcasting --reverb --without-node
--without-nodeを付けることで、npmパッケージのインストールをスキップします。
このコマンドによって、Broadcasting用の設定や、
routes/channels.php
などが作成されます。
続いてReverbの設定を生成します。
docker compose exec app php artisan reverb:install
確認を求められた場合は、ReverbをBroadcasting Driverとして有効にします。
Vue・Laravel Echoをインストールする
Node.js側で必要なパッケージをインストールします。
まずVueをインストールします。
docker compose run --rm node npm install vue
続いて、VueをViteで扱うためのプラグインと、Laravel Echo・pusher-jsをインストールします。
docker compose run --rm node npm install --save-dev @vitejs/plugin-vue laravel-echo pusher-js
Laravel ReverbはPusherプロトコルを利用するため、ブラウザ側ではpusher-jsを使用します。
Viteを設定する
vite.config.jsを以下のように変更します。
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
fonts: [
bunny('Instrument Sans', {
weights: [400, 500, 600],
}),
],
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
tailwindcss(),
],
server: {
host: '0.0.0.0',
port: 5173,
origin: 'http://localhost:5173',
hmr: {
host: 'localhost',
},
watch: {
ignored: [
'**/storage/framework/views/**',
],
},
},
});
Dockerコンテナ内ではViteを0.0.0.0で待ち受けます。
一方、Windowsのブラウザからアクセスする場合は、
http://localhost:5173
を利用します。
Reverbの環境変数を設定する
reverb:installによって.envへReverb用の設定が追加されます。
REVERB_APP_ID、REVERB_APP_KEY、REVERB_APP_SECRETは生成された値をそのまま利用します。
ホスト関連を以下のように設定します。
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=生成された値
REVERB_APP_KEY=生成された値
REVERB_APP_SECRET=生成された値
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_HOST=reverb
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST=localhost
VITE_REVERB_PORT=8080
VITE_REVERB_SCHEME=http
REVERB_HOST=reverbは、LaravelコンテナからReverbコンテナへ接続するための設定です。
VITE_REVERB_HOST=localhostは、Windows上のブラウザからDockerで公開している8080番ポートへ接続するための設定です。
設定を変更したためキャッシュを削除します。
docker compose exec app php artisan config:clear
Laravel 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',
],
})
これでブラウザからLaravel ReverbへWebSocket接続できるようになります。
Node.jsとReverbを起動する
パッケージやViteの設定を変更したため、Node.jsコンテナを再起動します。
docker compose restart node
Laravel Reverbも起動します。
docker compose up -d reverb
確認します。
docker compose ps
最終的に、
app
db
node
reverb
の4つが起動していれば準備完了です。
認証機能を作成する
今回のチャットではPrivate Channelを利用します。
Private Channelは認証済みユーザーのみ参加できるチャンネルなので、最低限のユーザー登録・ログイン機能を作成します。
AuthControllerを作成する
以下を実行します。
docker compose exec app php artisan make:controller AuthController
app/Http/Controllers/AuthController.phpを以下のようにします。
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function register(Request $request)
{
$validated = $request->validate([
'name' => [
'required',
'string',
'max:255',
],
'email' => [
'required',
'email',
'unique:users,email',
],
'password' => [
'required',
'min:8',
],
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make(
$validated['password']
),
]);
Auth::login($user);
$request->session()->regenerate();
return redirect('/chat');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => [
'required',
'email',
],
'password' => [
'required',
],
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect('/chat');
}
return back()->withErrors([
'email' => 'メールアドレスまたはパスワードが違います。',
]);
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/login');
}
}
ユーザー登録時は、パスワードをそのまま保存せず、
Hash::make()
を利用してハッシュ化します。
ログインには、
Auth::attempt()
を利用します。
ログイン成功後は、
$request->session()->regenerate();
でセッションIDを再生成します。
ユーザー登録画面を作成する
resources/views/register.blade.phpを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ユーザー登録</title>
</head>
<body>
<h1>ユーザー登録</h1>
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>
{{ $error }}
</li>
@endforeach
</ul>
@endif
<form method="POST" action="/register">
@csrf
<div>
<label>
名前
<input type="text" name="name" value="{{ old('name') }}" required>
</label>
</div>
<div>
<label>
メールアドレス
<input type="email" name="email" value="{{ old('email') }}" required>
</label>
</div>
<div>
<label>
パスワード
<input type="password" name="password" required>
</label>
</div>
<button type="submit">登録</button>
</form>
<p>
<a href="/login">ログインはこちら</a>
</p>
</body>
</html>
ログイン画面を作成する
resources/views/login.blade.phpを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン</title>
</head>
<body>
<h1>ログイン</h1>
@if ($errors->any())
<p>{{ $errors->first() }}</p>
@endif
<form method="POST" action="/login">
@csrf
<div>
<label>メールアドレス
<input type="email" name="email" value="{{ old('email') }}" required>
</label>
</div>
<div>
<label>
パスワード
<input type="password" name="password" required>
</label>
</div>
<button type="submit">ログイン</button>
</form>
<p>
<a href="/register">ユーザー登録はこちら</a>
</p>
</body>
</html>
今回は動作確認が目的のため、認証画面のデザインは行いません。
チャット機能を作成する
messagesテーブルを作成する
Messageモデルとmigrationファイルを作成します。
docker compose exec app php artisan make:model Message -m
-mを付けることで、
app/Models/Message.php
database/migrations/
xxxx_xx_xx_xxxxxx_create_messages_table.php
が作成されます。
migrationファイルのup()を以下のようにします。
public function up(): void
{
Schema::create(
'messages',
function (Blueprint $table) {
$table->id();
$table
->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->text('message');
$table->timestamps();
}
);
}
今回のmessagesテーブルは、
id
user_id
message
created_at
updated_at
を持ちます。
user_idはusers.idへの外部キーです。
ユーザーが削除された場合は、
cascadeOnDelete()
によって、そのユーザーのメッセージも削除されます。
migrationを実行します。
docker compose exec app php artisan migrate
Messageモデルを設定する
app/Models/Message.phpを以下のようにします。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
protected $fillable = [
'user_id',
'message',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
ここではDB構造を変更していないため、Messageモデル変更後のmigrationは不要です。
MessageSentイベントを作成する
メッセージが保存されたことをWebSocketで通知するため、イベントを作成します。
docker compose exec app php artisan make:event MessageSent
app/Events/MessageSent.phpを以下のようにします。
<?php
namespace App\Events;
use App\Models\Message;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\InteractsWithSockets;
class MessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(
public Message $message
) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('chat'),
];
}
public function broadcastAs(): string
{
return 'message.sent';
}
public function broadcastWith(): array
{
return [
'id' => $this->message->id,
'message' => $this->message->message,
'user' => [
'id' => $this->message->user->id,
'name' => $this->message->user->name,
],
'created_at' => $this->message->created_at,
];
}
}
今回は記事を簡単にするため、キューを利用するShouldBroadcastではなく、
ShouldBroadcastNow
を使用して即時Broadcastします。
Private Channelを設定する
今回は認証済みユーザーだけがチャットを受信できるようにします。
Broadcastingのインストール時に作成された、
routes/channels.php
へPrivate Channelの認証ルールを設定します。
<?php
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('chat', function (User $user) {
return true;
});
Private Channelへ参加しようとすると、Laravel側でこの認証処理が実行されます。
今回は1つのチャットルームだけなので、
return true;
として、ログイン済みユーザーであれば参加可能とします。
MessageControllerを作成する
以下を実行します。
docker compose exec app php artisan make:controller MessageController
app/Http/Controllers/MessageController.phpを以下のようにします。
<?php
namespace App\Http\Controllers;
use App\Events\MessageSent;
use App\Models\Message;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class MessageController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'message' => [
'required',
'string',
'max:1000',
],
]);
$message = Message::create([
'user_id' => auth()->id(),
'message' => $validated['message'],
]);
$message->load('user');
Log::info('message_created', [
'user_id' => auth()->id(),
'message_id' => $message->id,
'message' => $message->message,
]);
broadcast(new MessageSent($message))->toOthers();
return response()->json($message, 201);
}
}
ここでは、
バリデーション
↓
MySQLへメッセージ保存
↓
投稿者情報を取得
↓
Laravelでログを出力
↓
MessageSentをBroadcast
↓
HTTPレスポンスを返す
という処理を行っています。
今回は動作確認を分かりやすくするためメッセージ本文もログに出力していますが、実際の運用では個人情報や機密情報が含まれる可能性があるため、本文は記録せず、IDなど調査に必要な情報のみをログに残す設計が一般的です。
フロントエンドを作成する
今回はWebSocketの動作確認が目的のため、フロントエンドは最低限にします。
メッセージ一覧やデザインは作らず、入力、送信。受信した最新メッセージを表示だけ実装します。
また、コードを簡略化するため、送信者ごとに表示を分ける処理や、メッセージ履歴の表示は今回は実装しません。
Chat.vueを作成する
resources/js/componentsディレクトリを作成し、
resources/js/components/Chat.vue
を作成します。
<template>
<main>
<h1> WebSocket Chat</h1>
<input
v-model="message"
type="text"
placeholder="メッセージを入力"
@keyup.enter="sendMessage"
>
<button
type="button"
:disabled="!message.trim()"
@click="sendMessage"
>
送信
</button>
<p>受信メッセージ: {{ receivedMessage }}</p>
</main>
</template>
<script>
export default {
data() {
return {
message: '',
receivedMessage:
'メッセージ待機中...',
}
},
mounted() {
window.Echo
.private('chat')
.listen(
'.message.sent',
(event) => {
this.receivedMessage =
event.message
}
)
},
beforeUnmount() {
window.Echo.leave('chat')
},
methods: {
async sendMessage() {
const message = this.message.trim()
if (!message) {
return
}
const csrfToken =
document
.querySelector(
'meta[name="csrf-token"]'
)
.content
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': csrfToken,
}
const socketId = window.Echo.socketId()
if (socketId) {
headers['X-Socket-ID'] = socketId
}
const response =
await fetch(
'/messages',
{
method: 'POST',
headers,
body: JSON.stringify({
message,
}),
}
)
if (!response.ok) {
console.error(
await response.text()
)
return
}
this.message = ''
},
},
}
</script>
画面が表示されると、
window.Echo
.private('chat')
によってPrivate Channelのchatへ参加します。
そして、
.listen('.message.sent', ...)
でLaravelからBroadcastされるmessage.sentイベントを待ちます。
イベントを受信すると、
this.receivedMessage = event.message
によって画面へ表示します。
今回は最新の受信メッセージ1件だけを表示する簡単な画面です。
X-Socket-IDについて
メッセージ送信にはfetch()を利用しています。
MessageControllerでは、
->toOthers()
を利用しているため、Laravelへ現在のWebSocket接続IDを伝える必要があります。
そこで、
const socketId = window.Echo.socketId()
headers['X-Socket-ID'] = socketId
としてX-Socket-IDを送信します。
これによって、
送信したブラウザ
→ Broadcast対象から除外
別のブラウザ
→ WebSocketで受信
という動作になります。
app.jsを設定する
resources/js/app.jsを以下のようにします。
import './echo'
import { createApp } from 'vue'
import Chat from './components/Chat.vue'
createApp(Chat).mount('#app')
最初にecho.jsを読み込み、Laravel Echoを初期化します。
その後、Chat.vueをVueアプリとしてマウントします。
Bladeを作成する
resources/views/chat.blade.phpを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>WebSocket Chat</title>
@vite(['resources/js/app.js'])
</head>
<body>
<div id="app"></div>
</body>
</html>
Chat.vueは、
<div id="app"></div>
へマウントされます。
また、POST時のCSRF対策のため、
<meta name="csrf-token" content="{{ csrf_token() }}">
を設定しています。
ルートを設定する
routes/web.phpを以下のようにします。
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\MessageController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/register', function () {
return view('register');
});
Route::post('/register', [
AuthController::class,
'register',
]);
Route::get('/login', function () {
return view('login');
})->name('login');
Route::post('/login', [
AuthController::class,
'login',
]);
Route::post('/logout', [
AuthController::class,
'logout',
]);
Route::middleware('auth')->group(function () {
Route::get('/chat', function () {
return view('chat');
});
Route::post('/messages', [
MessageController::class,
'store',
]);
});
/chatとPOST /messagesには、
auth
ミドルウェアを設定しています。
そのため、ログインしていないユーザーは利用できません。
また、
->name('login')
によって/loginをloginという名前のルートとして登録しています。
authミドルウェアで未ログインユーザーが弾かれた場合、このログイン画面へリダイレクトされます。
リアルタイム通信を確認する
最後にコンテナの状態を確認します。
docker compose up -d
docker compose ps
以下の4サービスが起動していることを確認します。
app
db
node
reverb
ユーザーを2人作成する
以下へアクセスします。
http://localhost:8000/register
今回は、りんご、みかんという2人のユーザーを作成しました。
2人のユーザーを同時にログインさせるため、今回は、通常のChromeとは別に、シークレットウィンドウも利用します。
通常ウィンドウとシークレットウィンドウではセッションを分けられるため、それぞれ別ユーザーでログインできます。
3ユーザー以上を同時に確認したい場合は、別のChromeプロフィールやEdge、Firefoxなど別のブラウザを使用します。
ユーザー登録が完了すると、自動的にchatへ移動します。
メッセージを送信する
みかん側から、
こんにちは、りんご
と送信します。
通信の流れは以下です。
みかん
↓ HTTP POST
Laravel
↓
MySQL
↓
MessageSent
↓
Laravel Reverb
↓ WebSocket
りんご
りんご側で、
受信メッセージ:
こんにちは、りんご
と、画面をリロードせずに表示されれば成功です。
Laravelのログを確認する
MessageControllerでは、メッセージを保存したタイミングで、
Log::info('message_created', [
'user_id' => auth()->id(),
'message_id' => $message->id,
'message' => $message->message,
]);
を実行しています。
現時点ではCloudWatch Logsへ送信していないため、Laravelのローカルログを確認します。
storage/logs/laravel.log
以下のように出力されました。user_id1がみかんで、2がりんごです。
[2026-09-26 13:33:43] local.INFO: message_created {"user_id":1,"message_id":1,"message":"こんにちは、りんご"}
[2026-09-26 13:33:51] local.INFO: message_created {"user_id":2,"message_id":2,"message":"こんにちは、みかん"}
これで、
メッセージを送信
↓
MySQLへ保存
↓
Laravelでログを出力
↓
MessageSentを発火
↓
Laravel Reverb
↓
WebSocketで別ユーザーへ配信
という一連の流れを確認できました。
後編では、このLog::info()などで出力したログをCloudWatch Logsへ送信し、AWS上から確認・検索していきます。
躓いた箇所
Reverb起動時にSIGINTエラーが発生した
Reverbを起動した際、
Undefined constant
"Laravel\Reverb\Servers\Reverb\Console\Commands\SIGINT"
というエラーが発生しました。
原因はPHPコンテナにpcntl拡張が入っていなかったことでした。
pcntl(Process Control)は、PHPからプロセスやSIGINTなどのシグナルを扱うための拡張です。
Laravel Reverbでは、常駐しているWebSocketサーバーの停止などのシグナルを処理するために利用されます。
Dockerfileを、
docker-php-ext-install pdo_mysql pcntl
としてpcntlを追加することで解決しました。
LaravelがSQLiteへ接続していた
メッセージを送信すると500エラーが発生し、storage/logs/laravel.logを確認すると、
Connection: sqlite
no such table: messages
と出ていました。
LaravelがMySQLではなくSQLiteへ接続していたことが原因でした。
Laravel 11以降では、新規プロジェクトのデフォルトのデータベースとしてSQLiteを使用しており、今回は初期設定のSQLiteが残ったままになっていました。
.envを以下のようにMySQLへ変更します。
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=chat
DB_USERNAME=laravel
DB_PASSWORD=password
その後、設定キャッシュを削除してmigrationを実行します。
docker compose exec app php artisan config:clear
docker compose exec app php artisan migrate
これによりMySQLへ接続できるようになりました。
.envを変更しても接続先が変わらない場合は、設定キャッシュが残っている可能性があるため、php artisan config:clearを実行して確認します。
今回のようにエラーが発生した場合は、ブラウザのConsoleだけでなく、storage/logs/laravel.logも確認することが重要だと感じました。
Laravelのレスポンスが遅かった
リアルタイム通信を確認していると、メッセージを送信してから相手のブラウザへ表示されるまで数秒かかる状態になりました。
最初はWebSocketやLaravel Reverbが遅いのではないかと考えました。
しかし、Chrome DevToolsのNetworkを確認すると、POST /messagesだけでなく、GET /upにも数秒かかっていました。
/upはLaravelのヘルスチェック用エンドポイントなので、WebSocketだけではなくLaravel側のレスポンス自体が遅くなっていることが分かりました。
まず、LaravelとComposer側でできる最適化として以下を実行しました。
docker compose exec app php artisan optimize
docker compose exec app composer dump-autoload -o
php artisan optimizeでは、Laravelの設定やルートなどをキャッシュします。
composer dump-autoload -oでは、Composerのオートローダーを最適化します。
実行前は/upへのアクセスに約5秒かかっていましたが、実行後は約1.5秒まで改善しました。
POST /messagesについても、数秒程度かかっていた状態から約500ms程度まで短縮されました。
ただし、/upは改善後も約1.5秒かかっていました。
今回のDocker構成では、Windows側のプロジェクトを、
volumes:
- ./:/var/www/html
としてLinuxコンテナへbind mountしています。
Windows側のファイルシステムをLinuxコンテナへbind mountする構成では、ファイルアクセスがレスポンス性能に影響する場合があります。
そのため、さらに改善する方法として、プロジェクトをWSL2側のLinuxファイルシステムへ配置する方法があります。
今回はWSL2への移行による比較までは行っていないため、bind mountが原因とは断定できませんが、改善後もレスポンスに時間がかかっていた要因の1つとして考えられます。
今回のように「リアルタイム通信が遅い」と感じても、必ずしもWebSocket自体が原因とは限りません。
Chrome DevToolsのNetworkを確認し、
HTTPリクエストが遅いのか
WebSocketの受信が遅いのか
を切り分けて確認することが重要だと感じました。
まとめ
今回はDockerを利用してLaravel + Vueの環境を構築し、Laravel Reverbを利用したWebSocket通信を実装しました。
メッセージの登録にはHTTP通信を使用し、登録後の通知にはWebSocketを利用しました。
Vue
↓ HTTP
Laravel
↓
MySQL
Laravel
↓ MessageSent
Laravel Reverb
↓ WebSocket
Vue
また、Private Channelを利用するために簡単なユーザー登録・ログイン機能も実装しました。
実際に2つのブラウザセッションを用意し、片方から送信したメッセージがもう片方へ画面更新なしで届くことを確認できました。
さらに、メッセージ投稿時には、
Log::info()
を利用して、
storage/logs/laravel.log
へログを出力しました。
前編では、
DockerによるLaravel環境構築
Vueの導入
Laravel Reverbの導入
Laravel Broadcasting
Laravel Echo
Private Channel
ユーザー認証
WebSocket通信
Laravelのログ出力
までを確認しました。
後編では、今回Laravelで出力したログをCloudWatch Logsへ送信します。
また、単にログをAWSへ送るだけではなく、
どのユーザーが操作したのか
どの処理まで成功したのか
どこでエラーになったのか
どの情報を検索条件として利用したいのか
といった、不具合調査を行う際に必要となる情報についても考えていきます。
参考文献
- Laravel公式ドキュメント「Installation」
- Laravel公式ドキュメント「Broadcasting」
- Laravel公式ドキュメント「Laravel Reverb」
- Laravel公式ドキュメント「Authentication」
- Laravel公式ドキュメント「Vite」
- Vue.js公式ドキュメント
- Docker Desktopダウンロード
お知らせ
技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii



