
はじめに
普段みなさんが使っているSNS等のアプリには、フォローしたユーザーがメッセージの送信や投稿をすると、リアルタイムで通知がされるようになっていると思います。
今では、SNSにおいてリアルタイム通信というのは、必須といえる機能となりUX(ユーザー体験)をよりよくしてくれます。
Webアプリにおけるリアルタイム通信といえば、 WebSocket や WebRTC が思い浮かびますよね。
ただ、私自身は WebSocket や WebRTC を使った開発経験はなく、ちょうどブログのネタも探していたので、 Laravel × Redis × Docker という構成で WebSocket による双方向通信を実装していこうと思います。
目標
ブラウザAからメッセージを送信すると、サーバーサイドでイベントが発火し、 WebSocket で接続されている別のブラウザBがデータを受信し、コンソールにメッセージが表示されるというところをゴールとして実装していきます。

完成形のコードはこちらのリポジトリの feature/websocket
ブランチにあります。
GitHub - TakanoriIchikawa/docker-laravel-websocket
Contribute to TakanoriIchikawa/docker-laravel-websocket development by creating an account on GitHub.
https://github.com/TakanoriIchikawa/docker-laravel-websocket
完成形のコードを確認したい方は、以下のコマンドでGitクローンしてください。
git clone -b feature/websocket https://github.com/TakanoriIchikawa/docker-laravel-websocket.git docker-laravel-websocket
環境構築
まずは環境構築をしていきます。すでに Laravel が動作する環境があれば不要ですが、docker-compose.ymlやDockerfileの設定、コンテナの再ビルドは忘れないよう注意していください。
Laravel 環境の準備
1. Gitクローン
こちらのリポジトリから環境構築のためのコードを取得します。上述したリポジトリと同じです。
GitHub - TakanoriIchikawa/docker-laravel-websocket
Contribute to TakanoriIchikawa/docker-laravel-websocket development by creating an account on GitHub.
https://github.com/TakanoriIchikawa/docker-laravel-websocket
master
ブランチ → Dockerfileの設定とLaravelの準備まで完了しています
feature/websocket
ブランチ → WebSocketを使ったリアルタイム通信が実装完了しています
git clone https://github.com/TakanoriIchikawa/docker-laravel-websocket.git docker-laravel-websocket
2. ディレクトリの移動
cd docker-laravel-websocket
3. dockerコンテナのビルド
docker compose build --no-cache
4. dockerコンテナの起動
docker compose up -d
5. laravelの初期設定
docker compose exec app bash
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
上記のコマンドを実行したら、 http://localhost にアクセスしてください。
以下の画面が表示されれば環境構築完了です。

Dokcer周りの設定
WebSocket通信を実現する上でポイントとなるDockerの設定ファイルを紹介します。
すでにLaravelが動作する環境があり、「Laravel 環境の準備」をやっていない方は、Dockerfileの追加やdocker-compose.ymlの調整をお願いします。
laravel-echo-serverのDockerfile
socket.ioで作られたLaravelでWebSocket通信(ブロードキャスト)するためのサーバー(Nodejs)です。
laravel-echo-server start
のコマンドで起動して、6001ポートでWebSocket接続を受けます。
FROM node:16.15.1-alpine
RUN npm install -g laravel-echo-server
WORKDIR /work
CMD ["laravel-echo-server", "start"]
php-fpmのDockerfile
Redisブロードキャスタを使用するので、 pecl
を使い phpredis
をインストールします。
Laravelで発火したイベントを受け取り、Redisのpub/sub機能を使い、laravel-echo-serverを介することでクライアント(ブラウザ)にデータを送ります。
FROM php:8.1-fpm

# php設定コピー
COPY ./docker/app/php.ini /usr/local/etc/php/php.ini
COPY ./docker/app/php-fpm.d/zzz-docker.conf /usr/local/etc/php-fpm.d/zzz-docker.conf

# パッケージインストール
RUN apt update && \
 apt -y install \
 git \
 zip \
 unzip \
 vim \
 && docker-php-ext-install opcache \
 && docker-php-ext-install pdo_mysql bcmath

# Composer
COPY --from=composer:2.1 /usr/bin/composer /usr/bin/composer

# phpredis // インストールを忘れすに!!
RUN pecl install redis \
 && docker-php-ext-enable redis

# Node.js
COPY --from=node:16.15.1 /usr/local/bin /usr/local/bin
COPY --from=node:16.15.1 /usr/local/lib /usr/local/lib

WORKDIR /var/www/html
d ocker-compose.yml
redisやecho-serverコンテナの設定を確認してください。
version: "3.8"

services:
 app:
 container_name: laravel-websocket-app
 build:
 context: .
 dockerfile: ./docker/app/Dockerfile
 restart: always
 volumes:
 - type: bind
 source: ./src
 target: /var/www/html
 - type: volume
 source: php_fpm_socket
 target: /var/run/php-fpm

 web:
 container_name: laravel-websocket-web
 build:
 context: .
 dockerfile: ./docker/web/Dockerfile
 restart: always
 volumes:
 - type: bind
 source: ./src
 target: /var/www/html
 - type: volume
 source: php_fpm_socket
 target: /var/run/php-fpm
 ports:
 - "${WEB_PORT}:80"
 depends_on:
 - app

 db:
 container_name: laravel-websocket-db
 build:
 context: .
 dockerfile: ./docker/mysql/Dockerfile
 restart: always
 volumes:
 - type: volume
 source: mysql_volume
 target: /var/lib/mysql
 ports:
 - ${DB_PORT}:3306
 environment:
 MYSQL_DATABASE: ${DB_DATABASE}
 MYSQL_USER: ${DB_USERNAME}
 MYSQL_PASSWORD: ${DB_PASSWORD}
 MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}

 redis:
 container_name: laravel-websocket-redis
 image: "redis:latest"
 restart: always
 volumes:
 - type: volume
 source: redis_data 
 target: /data
 ports:
 - "6379:6379"

 echo-server:
 container_name: laravel-websocket-echo-server
 build: ./docker/echo-server
 restart: always
 volumes:
 - type: bind
 source: ./src
 target: /work
 ports:
 - "6001:6001"

volumes:
 php_fpm_socket:
 mysql_volume:
 redis_data:

networks:
 default:
 name: laravel-websocket-network
laravel-echo-serverの設定
初回コンテナ起動時には、laravel-echo-serverの設定が済んでいないので、エラーが発生してecho-serverのコンテナが起動できません。
以下のコマンドを実行して、初期設定をします。
docker compose run echo-server laravel-echo-server init
設定は以下の通りです。
? Do you want to run this server in development mode? Yes
? Which port would you like to serve from? 6001
? Which database would you like to use to store presence channel members? redis
? Enter the host of your Laravel authentication server. http://localhost
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? No
? Do you want to setup cross domain access to the API? No
? What do you want this config to be saved as? laravel-echo-server.json
Configuration file saved. Run laravel-echo-server start to run server.
入力が終わると、Laravelプロジェクトの直下に laravel-echo-server.json
というファイルが生成されます。
生成されたファイルを編集します。 redis
の設定に以下を追記してください。
"databaseConfig": {
		"redis": {
			"host": "redis",
			"port": 6379
		},
 ...
},
最終的には、以下のようになります。
{
	"authHost": "http://localhost",
	"authEndpoint": "/broadcasting/auth",
	"clients": [],
	"database": "redis",
	"databaseConfig": {
		"redis": {
			"host": "redis",
			"port": 6379
		},
		"sqlite": {
			"databasePath": "/database/laravel-echo-server.sqlite"
		}
	},
	"devMode": true,
	"host": null,
	"port": "6001",
	"protocol": "http",
	"socketio": {},
	"secureOptions": 67108864,
	"sslCertPath": "",
	"sslKeyPath": "",
	"sslCertChainPath": "",
	"sslPassphrase": "",
	"subscribers": {
		"http": true,
		"redis": true
	},
	"apiOriginAllow": {
		"allowCors": false,
		"allowOrigin": "",
		"allowMethods": "",
		"allowHeaders": ""
	}
}
以上で、laravel-echo-serverの設定は完了です。
dockerコンテナを確認すると、echo-serverのコンテナが問題なく起動されていると思います。
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
... 
laravel-websocket-echo-server "docker-entrypoint.s…" echo-server running 0.0.0.0:6001->6001/tcp
また、 laravel-echo-server
が正しく起動されると、 laravel-echo-server.lock
ファイルも生成されるので確認してください。
redisの設定
.env
を以下のように編集します。
BROADCAST_DRIVER=redis // logからredisに変更
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis // syncからredisに変更、dockerコンテナ名(redis)
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_HOST=redis // dockerコンテナ名(redis)
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CLIENT=phpredis // appコンテナにインストールされたphpredisを使用
REDIS_PREFIX=""
./src/config/app.php
を開き、 App\Providers\BroadcastServiceProvider::class
のコメントアウトを外します。
App\Providers\BroadcastServiceProvider::class, // コメントアウトを外す
特に設定を変更する必要はありませんが、 ./src/config/database.php
の Redis 周りの設定は以下のようになっています。
'redis' => [

 'client' => env('REDIS_CLIENT', 'phpredis'),

 'options' => [
 'cluster' => env('REDIS_CLUSTER', 'redis'),
 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
 ],

 'default' => [
 'url' => env('REDIS_URL'),
 'host' => env('REDIS_HOST', '127.0.0.1'),
 'username' => env('REDIS_USERNAME'),
 'password' => env('REDIS_PASSWORD'),
 'port' => env('REDIS_PORT', '6379'),
 'database' => env('REDIS_DB', '0'),
 ],

 'cache' => [
 'url' => env('REDIS_URL'),
 'host' => env('REDIS_HOST', '127.0.0.1'),
 'username' => env('REDIS_USERNAME'),
 'password' => env('REDIS_PASSWORD'),
 'port' => env('REDIS_PORT', '6379'),
 'database' => env('REDIS_CACHE_DB', '1'),
 ],

],
ライブラリのインストール
appコンテナ内でライブラリをそれぞれインストールをしてください。
appコンテナには、ルートディレクトリから以下のコマンドを実行することで接続できます。
docker compose exec app bash
laravel-echo
https://www.npmjs.com/package/laravel-echo
Laravelが提供しているJavaScriptのライブラリです。
laravel-echo-server がブロードキャストしたイベントを受け取ってくれます。
npm install laravel-echo
socket.io-client
https://www.npmjs.com/package/socket.io-client
クライアントからWebSocket通信でサーバーへ接続するために必要になります。
socket.io-client のバージョンが2系でないと、laravel-echo-server に接続できないようなので、バージョンを ^2.4.0
で指定します。
GitHubのイシューにも上がっています。2022年12月時点ではまだ解決していないようです。
Unable to connect with `laravel-echo-server` with latest `socket.io-client` · Issue #576 · tlaverdure/laravel-echo-server
Describe the bug Whenever I try to connect with laravel-echo-server with the latest socket.io-client and laravel-echo, it does not connect to the laravel-echo-server, however If I downgrade socket....
https://github.com/tlaverdure/laravel-echo-server/issues/576
npm install socket.io-client@^2.4.0
これで、環境構築とWebSocket通信を実現するための準備ができました。
機能実装
クライアント(ブラウザ)からメッセージを送信すると、WebSocketで常時接続されているクライアント(ブラウザ)に対してイベントを介し、データ(メッセージ)送信する機能を実装します。
イベント
MessageEvent
を作成します。appコンテナで以下のコマンドを実行してください。
php artisan make:event MessageEvent
./src/app/Events/MessageEvent.php
が生成されるので、以下のように編集します。
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageEvent implements ShouldBroadcast // インターフェースの記述を忘れずに
{
 use Dispatchable, InteractsWithSockets, SerializesModels;

 public $message;

 /**
 * Create a new event instance.
 *
 * @return void
 */
 public function __construct($message)
 {
 $this->message = $message;
 }

 /**
 * Get the channels the event should broadcast on.
 *
 * @return \Illuminate\Broadcasting\Channel|array
 */
 public function broadcastOn()
 {
 return new Channel('message-event');
 }

 public function broadcastWith()
 {
 return [
 'message' => $this->message,
 ];
 }
}
Redisによる処理をキューで実行するには、ワーカーを起動する必要があります。
appコンテナで以下のコマンドを実行すると、ワーカーが起動します。
php artisan queue:work
ルーティング
./src/routes/web.php
に以下のルーティングを追記してください。
それぞれ以下の処理をします
- メッセージ送信フォームを表示する
- ブラウザから送信されたメッセージを処理する
Route::get('messages', [App\Http\Controllers\MessagesController::class, 'index'])->name('messages.index');
Route::post('messages', [App\Http\Controllers\MessagesController::class, 'store'])->name('messages.store');
コントローラー
MessagesControllerを作成します。appコンテナで以下のコマンドを実行してください。
php artisan make:controller MessagesController
コントローラーの記述はとてもシンプルです。単純にビューを返す index()
とメッセージが送信された際にイベントを呼ぶ store()
になります。
実際にはメッセージをDBに保存する等の処理が入ると思いますが、大筋のところには影響ないので、割愛します。
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MessagesController extends Controller
{
 public function index()
 {
 return view('messages.index');
 }

 public function store(Request $request)
 {
 event(new \App\Events\MessageEvent($request->message));

 return redirect()->route('messages.index');
 }
}
ビュー
続いてビューファイルを用意します。 ./src/resources/views/messages/index.blade.php
を作成してください。
メッセージを送信するだけの味気ないものですが、WebSocket通信を確認する分には十分なので、このまま進めます。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Docker Laravel Websocket</title>
 @vite(['resources/css/app.css', 'resources/js/app.js'])
 </head>
 <body>
 <div>
 <form action="{{ route('messages.store') }}" method="POST" >
 @csrf
 <input type="text" name="message" value="">
 <button type="submit">送信</button>
 </form>
 </div>
 </body>
</html>
クライアント側の記述
バックエンドでイベントが発生した際に、クライアント(ブラウザ)でイベントを受け取るための設定をします。
./src/resources/js/bootstrap.js
で事前にインストールしたライブラリを読み込みます。以下を追記してください。
import Echo from 'laravel-echo';
import io from 'socket.io-client';
window.io = io;
window.Echo = new Echo({
 broadcaster: 'socket.io',
 host: window.location.hostname + ':6001'
});
そして、 ./src/resources/views/messages/index.blade.php
の </body>
の下にも追記します。
</body>
 <script>
 window.addEventListener('load', (event) => {
 window.Echo.channel('message-event').listen('MessageEvent', (e) => {
 console.log(e)
 })
 });
 </script>
</html>
これでクライアント(ブラウザ)側の設定は完了です。
動作検証
コンパイル
appコンテナで、以下のコマンドを実行します。このコマンドを実行しないと Vite manifest not found at
とエラーが出てしまいます。
npm run build
ワーカー起動
前述しましたが、Redisによる処理をキューで実行するためワーカーを起動します。
appコンテナで以下のコマンドを実行します。
php artisan queue:work
ブラウザ確認
ブラウザを二つ開いてください。一つをブラウザA、もう一つをブラウザBとします。
ブラウザBでデベロッパーツールのコンソールを開きます。
ブラウザAからフォームにテキストを入力して、メッセージを送信してください。
すると、ブラウザBのコンソールにブラウザAで入力したメッセージが表示されます。

まとめ
最後まで記事を読んでいただきありがとうございます。初めての WebSocket を使った機能実装でしたので、いろいろと苦戦しました。
ただ、 WebSoket によるリアルタイム通信が成功した時は、苦労した分とても嬉しかったです。
次は実案件でも使えるようローカル環境ではなく、本番運用を想定したサーバー構築もしたいなと思っています。まぁぶっちゃけ、リアルタイム性が求められる案件は少なく日の目を浴びることはないかもですが…笑
では、また次回の記事で!