Dockerizing Socket.IO using Laradock (with Redis and Nginx)

This is a guide on how we implemented a Socket.IO server using Laravel as our backend application and posting our events through Redis.

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:

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 our broadcastOn() 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:

  1. Run docker ps.
  2. Find in the list the workspace container.
  3. Copy the Container ID.
  4. 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!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.