Dockerizing Socket.IO using Laradock (with Redis and Nginx)
In one of our projects, it was necessary to implement WebSockets, our backend was using Laravel, and our frontend React, so we wanted an alternative that works well in both ways.
Although it’s not just a WebSocket implementation, we decided to use Socket.IO, because it’s open-source, well-maintained, well-documented, easy to implement, and fits our requirements.
There are a lot of tutorials on the internet about using Socket.IO with Laravel, but as we wanted to integrate it into an existing project, I’ve encountered some issues following them:
- Laravel echo server lost support on Nov 15, 2023.
- They don’t use Laradock.
- They don’t use Nginx to provide SSL support.
So, no more talk, let’s see how we’ve implemented it.
Laravel Side
The first we need to do is to configure Laravel to broadcast events using Redis, it’s required to add in our .env file the following line:
BROADCAST_DRIVER=redis
We need to create a new event (we can just follow the documentation).
<?php namespace App\Events; use App\Models\User; use Illuminate\Broadcasting\Channel; use Illuminate\Broadcasting\InteractsWithSockets; use Illuminate\Broadcasting\PresenceChannel; use Illuminate\Broadcasting\PrivateChannel; use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Queue\SerializesModels; class UserUpdated implements ShouldBroadcast { use SerializesModels; /** * Create a new event instance. */ public function __construct( public User $user, ) {} /** * Get the channels the event should broadcast on. * * @return array<int, \Illuminate\Broadcasting\Channel> */ public function broadcastOn(): array { return [ new Channel('user-updated'), ]; } }
Once the event is finally created, we can dispatch it. As our example event is a user getting updated, we can dispatch it from the UserObserver.
On the updated() function we just need to add the following:
event(new UserUpdated($user));
Now, anytime a user is updated, our Laravel application will broadcast the event on Redis.
Creating the Socket.IO server
Now, we need to develop a WebSocket server using Socket.IO, to do that we can follow the official documentation.
Considering we’re developing it on the same server as our Laravel application, we can use the existing .env files to store environmental information.
import { createServer } from "http"; import { createAdapter } from "@socket.io/redis-adapter"; import { createClient } from "redis"; import { Server } from "socket.io"; import "dotenv/config"; // Initializing the Socket.IO server const httpServer = createServer(); const io = new Server(httpServer, { path: '/socket-io/', cors: { credentials: true } }); io.listen(process.env.SOCKET_IO_PORT); // Creating a redis client // We'll use the configuration defined in the .env file cause this will change depending on the environment const redisUrl = process.env.REDIS_HOST_SOCKET + ":" + process.env.REDIS_PORT; const pubClient = createClient({url: redisUrl, password: process.env.REDIS_PASSWORD}); const subClient = pubClient.duplicate(); io.on('connect', (socket) => { // Add your connection logic here }) // Now we're connecting the pub/sub clients Promise.all([pubClient.connect(), subClient.connect()]).then(() => { io.adapter(createAdapter(pubClient, subClient)); }); // We'll subscribe to the channels defined in the array below subClient.subscribe(['laravel_database_user-updated'], (message, channel) => { // If we detect a message in these channels, we know they belong to Laravel events const event = JSON.parse(message); // Add here all the events and the corresponding logic for each if (event.event === 'App\\Events\\UserUpdated') { // In this case, in the message we have the ID of the user which was updated const id = event.data.user.id; io.emit('user-updated-'+id, 'User was updated.'); } });
Let’s check some important things to take care of in this example:
- We’re starting the server using the default Socket.IO server, but it can be done using Express, Koa, etc.
- We’re initializing an HTTP server, the SSL encryption will be provided using Nginx in the next step.
- Our server will be listening to a port defined in our .env file
- We will create (as in the official documentation) a Publish and a Subscribe client.
- When we subscribe to the Redis channel, the name of the channel will be defined in
config('database.redis.prefix')
followed by the name of the channel we added to ourbroadcastOn()
function in the Laravel Event. - Once we detect the event, we emit that event to a channel on our HTTP server with the name
user-updated-{userId}
. - Remember this is just an example and for that reason, this is a very simple implementation. We just want to set up the server.
Using Nginx to provide SSL encryption
Now, we need to add a configuration on Nginx to use the same SSL encryption we’re using on our Laravel server. For that, we need to customize our nginx.conf file, adding an upstream:
upstream workspace-socketio { server laradock-workspace-1:3000; }
And also, adding the following location on server
:
location /ccl-socket-io/ { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_pass http://workspace-socketio; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
Dockerizing the server with Laradock
If you need to do a temporary execution of the Socket.IO server, you can just get inside the workspace
container and execute the server.
If you don’t know how to open a shell inside the workspace container, you can follow these steps:
- Run
docker ps
. - Find in the list the
workspace
container. - Copy the
Container ID
. - Run the following command:
docker exec -it <containerId> bash
Once you’re inside, you can just run the following command: node <name-of-websocket-server-file.js>
. And your server should start.
If you want to start the server when your application starts, you’ll need to create a cron job or, as in our case, use the Supervisor.
For that, we’ll need to add a configuration file (we called it socket-io-server.conf
).
[program:socket-io-server] process_name=%(program_name)s_%(process_num)02d command=node /var/www/websocketServer.js autostart=true autorestart=true stopasgroup=true killasgroup=true user=laradock numprocs=1 redirect_stderr=true stdout_logfile=/var/www/storage/logs/socket_io_server.log stopwaitsecs=3600
This configuration file should be located inside the `supervisor.d` folder, in the following path laradock/php-worker/supervisord.d/
.
Once that’s done, we just need to start the php-worker container.
Conclusion
Finally, we can test our configuration on Postman or directly connect to the frontend application.
As we said previously, this is just an example of how to implement WebSockets on a Laravel application using Socket.IO as the server, so there are a lot of improvements to make, but this is a nice approach to implement it.
Don’t hesitate to let us any questions you have in the comment section below. Have a nice coding session!