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:
"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