Note: I'm migrating from gonzalo123.com to here. When I finish I'll swap the DNS to here. The "official" blog will be always gonzalo123.com

      Working with SAPUI5 locally (part 3). Adding more services in Docker

      In the previous project we moved one project to docker. The idea was to move exactly the same functionality (even without touching anything within the source code). Now we’re going to add more services. Yes, I know, it looks like overenginering (it’s exactly overenginering, indeed), but I want to build something with different services working together. Let start.

      We’re going to change a little bit our original project. Now our frontend will only have one button. This button will increment the number of clicks but we’re going to persists this information in a PostgreSQL database. Also, instead of incrementing the counter in the backend, our backend will emit one event to a RabbitMQ message broker. We’ll have one worker service listening to this event and this worker will persist the information. The communication between the worker and the frontend (to show the incremented value), will be via websockets.

      With those premises we are going to need:

      • Frontend: UI5 application
      • Backend: PHP/lumen application
      • Worker: nodejs application which is listening to a RabbitMQ event and serving the websocket server (using socket.io)
      • Nginx server
      • PosgreSQL database.
      • RabbitMQ message broker.

      As the previous examples, our PHP backend will be server via Nginx and PHP-FPM.

      Here we can see to docker-compose file to set up all the services

      version: '3.4'
       
      services:
        nginx:
          image: gonzalo123.nginx
          restart: always
          ports:
          - "8080:80"
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-nginx
          volumes:
          - ./src/backend:/code/src
          - ./src/.docker/web/site.conf:/etc/nginx/conf.d/default.conf
          networks:
          - app-network
        api:
          image: gonzalo123.api
          restart: always
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-lumen-dev
          environment:
            XDEBUG_CONFIG: remote_host=${MY_IP}
          volumes:
          - ./src/backend:/code/src
          networks:
          - app-network
        ui5:
          image: gonzalo123.ui5
          ports:
          - "8000:8000"
          restart: always
          volumes:
          - ./src/frontend:/code/src
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-ui5
          networks:
          - app-network
        io:
          image: gonzalo123.io
          ports:
          - "9999:9999"
          restart: always
          volumes:
          - ./src/io:/code/src
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-io
          networks:
          - app-network
        pg:
          image: gonzalo123.pg
          restart: always
          ports:
          - "5432:5432"
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-pg
          environment:
            POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
            POSTGRES_USER: ${POSTGRES_USER}
            POSTGRES_DB: ${POSTGRES_DB}
            PGDATA: /var/lib/postgresql/data/pgdata
          networks:
          - app-network
        rabbit:
          image: rabbitmq:3-management
          container_name: gonzalo123.rabbit
          restart: always
          ports:
          - "15672:15672"
          - "5672:5672"
          environment:
            RABBITMQ_ERLANG_COOKIE:
            RABBITMQ_DEFAULT_VHOST: /
            RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
            RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
          networks:
          - app-network
      networks:
        app-network:
          driver: bridge
      

      We’re going to use the same docker files than in the previous post but we also need new ones for worker, database server and message queue:

      Worker:

      FROM node:alpine
       
      EXPOSE 8000
       
      WORKDIR /code/src
      COPY ./io .
      RUN npm install
      ENTRYPOINT ["npm", "run", "serve"]
      

      The worker script is simple script that serves the socket.io server and emits a websocket within every message to the RabbitMQ queue.

      var amqp = require('amqp'),
        httpServer = require('http').createServer(),
        io = require('socket.io')(httpServer, {
          origins: '*:*',
        }),
        pg = require('pg')
      ;
       
      require('dotenv').config();
      var pgClient = new pg.Client(process.env.DB_DSN);
       
      rabbitMq = amqp.createConnection({
        host: process.env.RABBIT_HOST,
        port: process.env.RABBIT_PORT,
        login: process.env.RABBIT_USER,
        password: process.env.RABBIT_PASS,
      });
       
      var sql = 'SELECT clickCount FROM docker.clicks';
       
      // Please don't do this. Use lazy connections
      // I'm 'lazy' to do it in this POC 🙂
      pgClient.connect(function(err) {
        io.on('connection', function() {
          pgClient.query(sql, function(err, result) {
            var count = result.rows[0]['clickcount'];
            io.emit('click', {count: count});
          });
       
        });
       
        rabbitMq.on('ready', function() {
          var queue = rabbitMq.queue('ui5');
          queue.bind('#');
       
          queue.subscribe(function(message) {
            pgClient.query(sql, function(err, result) {
              var count = parseInt(result.rows[0]['clickcount']);
              count = count + parseInt(message.data.toString('utf8'));
              pgClient.query('UPDATE docker.clicks SET clickCount = $1', [count],
                function(err) {
                  io.emit('click', {count: count});
                });
            });
          });
        });
      });
       
      httpServer.listen(process.env.IO_PORT);
      

      Database server:

      FROM postgres:9.6-alpine
      COPY pg/init.sql /docker-entrypoint-initdb.d/
      

      As we can see we’re going to generate the database estructure in the first build

      CREATE SCHEMA docker;
       
      CREATE TABLE docker.clicks (
      clickCount numeric(8) NOT NULL
      );
       
      ALTER TABLE docker.clicks
      OWNER TO username;
       
      INSERT INTO docker.clicks(clickCount) values (0);
      
      

      With the RabbitMQ server we’re going to use the official docker image so we don’t need to create one Dockerfile

      We also have changed a little bit our Nginx configuration. We want to use Nginx to serve backend and also socket.io server. That’s because we don’t want to expose different ports to internet.

      server {
          listen 80;
          index index.php index.html;
          server_name localhost;
          error_log  /var/log/nginx/error.log;
          access_log /var/log/nginx/access.log;
          root /code/src/www;
       
          location /socket.io/ {
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection "upgrade";
              proxy_pass "http://io:9999";
          }
       
          location / {
              try_files $uri $uri/ /index.php?$query_string;
          }
       
          location ~ \.php$ {
              try_files $uri =404;
              fastcgi_split_path_info ^(.+\.php)(/.+)$;
              fastcgi_pass api:9000;
              fastcgi_index index.php;
              include fastcgi_params;
              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
              fastcgi_param PATH_INFO $fastcgi_path_info;
          }
      }
      

      To avoid CORS issues we can also use SCP destination (the localneo proxy in this example), to serve socket.io also. So we need to:- change our neo-app.json file

      "routes": [
          ...
          {
            "path": "/socket.io",
            "target": {
              "type": "destination",
              "name": "SOCKETIO"
            },
            "description": "SOCKETIO"
          }
        ],
      

      And basically that’s all. Here also we can use a “production” docker-copose file without exposing all ports and mapping the filesystem to our local machine (useful when we’re developing)

      version: '3.4'
       
      services:
        nginx:
          image: gonzalo123.nginx
          restart: always
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-nginx
          networks:
          - app-network
        api:
          image: gonzalo123.api
          restart: always
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-lumen
          networks:
          - app-network
        ui5:
          image: gonzalo123.ui5
          ports:
          - "80:8000"
          restart: always
          volumes:
          - ./src/frontend:/code/src
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-ui5
          networks:
          - app-network
        io:
          image: gonzalo123.io
          restart: always
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-io
          networks:
          - app-network
        pg:
          image: gonzalo123.pg
          restart: always
          build:
            context: ./src
            dockerfile: .docker/Dockerfile-pg
          environment:
            POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
            POSTGRES_USER: ${POSTGRES_USER}
            POSTGRES_DB: ${POSTGRES_DB}
            PGDATA: /var/lib/postgresql/data/pgdata
          networks:
          - app-network
        rabbit:
          image: rabbitmq:3-management
          restart: always
          environment:
            RABBITMQ_ERLANG_COOKIE:
            RABBITMQ_DEFAULT_VHOST: /
            RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
            RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
          networks:
          - app-network
      networks:
        app-network:
          driver: bridge
      

      And that’s all. The full project is available in my github account

      comments powered by Disqus