5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel Reverb+Vueのリアルタイムチャットを作る — CloudWatch Logs連携の前編

5
Last updated at Posted at 2026-10-01

目次

はじめに

ソーイ株式会社の臼井です。

業務では、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を最新の状態へ更新します。

image.png

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人のユーザーを作成しました。

image.png

2人のユーザーを同時にログインさせるため、今回は、通常のChromeとは別に、シークレットウィンドウも利用します。

通常ウィンドウとシークレットウィンドウではセッションを分けられるため、それぞれ別ユーザーでログインできます。

3ユーザー以上を同時に確認したい場合は、別のChromeプロフィールやEdge、Firefoxなど別のブラウザを使用します。

ユーザー登録が完了すると、自動的にchatへ移動します。

image.png

メッセージを送信する

みかん側から、

こんにちは、りんご

と送信します。

通信の流れは以下です。

みかん
↓ HTTP POST
Laravel
↓
MySQL
↓
MessageSent
↓
Laravel Reverb
↓ WebSocket
りんご

りんご側で、

受信メッセージ:
こんにちは、りんご

と、画面をリロードせずに表示されれば成功です。

image.png

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へ送るだけではなく、

どのユーザーが操作したのか
どの処理まで成功したのか
どこでエラーになったのか
どの情報を検索条件として利用したいのか

といった、不具合調査を行う際に必要となる情報についても考えていきます。

参考文献

お知らせ

技術ブログを週1〜2本更新中、ソーイをフォローして最新記事をチェック!
https://qiita.com/organizations/sewii

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?