Imarc

Real-time Notifications: Laravel Echo Server with Docker and Traefik Jeff Turcotte

Written on: March 29th, 2018 in engineering, open source

One of my favorite projects in the Laravel ecosystem is Echo. Echo enables real-time web applications through the use of WebSockets and hooks directly into Laravel's event broadcasting features. This means developers can use a familiar PHP API to send real-time data. A very common use-case for this type of functionality would be a notification or chat system.

So let's do it! Here's how to build a simple notification system, using only web standards and open source tools.

Start with an open source WebSocket server and proxy

The Laravel documentation tends to point developers towards integrating with Pusher, a commercial product which provides the WebSocket connection to your users, but there is an open source, self-hosted alternative based around Socket.IO: Laravel Echo Server. If you have strict project parameters or need a free alternative, Echo server is for you.

Laravel Echo Logo

Let's configure Laravel and Echo Server to run on Docker behind a Traefik proxy. Traefik is a load balancer that auto-configures itself based on container labels. In my opionion, the best part about Traefik is that you can offload all TLS termination to it. With a little information from each running container, it automatically takes care of all certificate management with Let's Encrypt. That means hands-off, free, and auto-renewing HTTPS, and in the case of Echo, a secure WebSocket connection over HTTPS.

Traefik project logo

Instead of doing a step by step walkthrough, here is a reference repository (https://github.com/imarc/laravel-echo-example) with installation instructions below. We'll then dive a little deeper into a few of the pieces that make this work.

If you're looking for a peek into every piece of the setup, browse the commit log.

How To Install And Run

# clone repo
git clone https://github.com/imarc/laravel-echo-example
cd laravel-echo-example

# install php dependencies
composer install
composer run-script install-tasks

# install npm dependencies and build
npm install
npm run prod

# start docker services
DOMAIN=localtest.me docker-compose up

# Advanced/Optional: to deploy to a real public domain with Traefik's TLS management enabled, do this:
DOMAIN=your-domain.com docker-compose -f docker-compose.yml up

Open up http://www.localtest.me in your browser to see it in action.

Docker Compose Configuration

Below is the docker-compose.yml configuration. We are doing a few things here:

  • Using container labels to configure Traefik. You can use these to configure hostnames, ports, and even things like basic auth. The labels tell Traefik to proxy those containers. We then can limit the open ports to the Traefik container.
  • Echo is being built from a local Dockerfile and not an existing image due to the lack of community supported images for Echo Server.
  • Due to Docker's internal DNS, we can reference all our services with simple hostnames such as 'redis' or 'mysql' from any other service. This comes in very handy for doing authentication between Echo Server and Laravel, where Echo Server can reference Laravel as 'www' directly in its configuration.
version: '2'

services:
  proxy:
    image: traefik
    command: --docker.domain=${DOMAIN} --logLevel=DEBUG
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/traefik.toml:/etc/traefik/traefik.toml
      - ./traefik/acme.json:/etc/traefik/acme/acme.json

  www:
    image: imarcagency/php-apache:2
    environment:
      - "APACHE_ROOT=/var/www/public"
      - "DOMAIN=${DOMAIN}"
    labels:
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:www.${DOMAIN}"
      - "traefik.port=80"
    volumes:
      - "./:/var/www"
      - "./docker-configure.sh:/usr/local/bin/docker-configure"
      - "app_storage:/var/www/storage"

  mysql:
    image: "mariadb:10.3"
    environment:
      - "MYSQL_DATABASE=app"
      - "MYSQL_USER=app"
      - "MYSQL_PASSWORD=app"
      - "MYSQL_RANDOM_ROOT_PASSWORD=yes"
    volumes:
      - "mysql_data:/var/lib/mysql"

  redis:
    image: "redis:3.2"
    command: "redis-server --appendonly yes"
    volumes:
      - "redis_data:/data"

  echo:
    build: ./echo
    labels:
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:echo.${DOMAIN}"
      - "traefik.port=80"
    working_dir: "/usr/src/app"
    volumes:
      - "./:/usr/src/app"

volumes:
  app_storage:
  mysql_data:
  redis_data:

Echo Configuration

Because everything is behind Traefik, we don't have to worry about setting up HTTPS or certs for production deployments. Traefik will terminate TLS so Echo can run on port 80 and be easily made available under its own separate hostname.

Within our welcome.blade.php file, we'll pass in some of our .env vars to a global JS variable and ensure the client side Echo library can configure itself properly.

# excerpt from welcome.blade.php

<script>
window.echoConfig = {
host: {!! json_encode(env('ECHO_HOST')) !!},
port: {!! json_encode(env('ECHO_PORT')) !!}
};
</script>

<!-- Scripts -->
<script src="//{{ env('ECHO_HOST') }}/socket.io/socket.io.js"></script>
<script src="{{ asset('js/app.js') }}"></script>

And in bootstrap.js:

# excerpt from bootstrap.js

import Echo from 'laravel-echo'

window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: `${window.echoConfig.host}:${window.echoConfig.port}`
});

Triggering & Listening for Events

I created a very simple Laravel Test Event to play with. Laravel Event objects broadcast all public properties to the specified channels. It's important that all Echo events implement the ShouldBroadcast interface. I also made a quick HTTP handler to trigger the event:

# excerpt from routes/web.php

Route::get('/broadcast-test', function() {
   event(new TestEvent('The server time is now ' . date('H:i:s')));
});

This handler will trigger the event with a message that displays the server time. The event is listened for by a Vue component that displays a list of the returned messages. The component also container a trigger to hit the HTTP handler.

const app = new Vue({
    el: '#app',

    data() {
        return {
            messages: []
        }
    },

    mounted() {
        window.Echo.channel('global')
            .listen('TestEvent', (e) => {
                this.messages.unshift(e.data);
            });
    },

    methods: {
        broadcast() {
            axios.get('/broadcast-test');
        }
    }
});

Result

Here it is. Nothing flashy, just a simple PoC to trigger and receive events. If you open it up in two browsers at once, you get to see the full power and real-time capability of Laravel Echo. Click the button in one window, and the messages will simultaneously update across all windows.

Screenshot of Laravel Echo notifications in action

Now go impress your friends and clients!

Share:

Let's Talk.