No artigo anterior, movemos um projeto para o Docker. A ideia era mover exatamente a mesma funcionalidade (mesmo sem tocar em nada dentro do código-fonte).
Agora vamos adicionar mais serviços. Sim, eu sei, parece overenginering (é exatamente isso, na verdade), mas eu quero construir algo com diferentes serviços trabalhando juntos. Vamos começar.
Mudaremos um pouco nosso projeto original. Agora nosso front-end terá apenas um botão. Esse botão aumentará o número de cliques, mas persistiremos essas informações em um banco de dados do PostgreSQL. Além disso, ao invés de incrementar o contador no back-end, nosso back-end emitirá um evento para um broker de mensagem RabbitMQ.
Teremos um serviço do worker para ouvir esse evento, e esse worker persistirá as informações. A comunicação entre o worker e o front-end (para mostrar o valor incrementado) será feita via websockets.
Com essas premissas, vamos precisar de:
- Frontend: aplicação UI5
- Backend: aplicativo PHP/lumen
- Worker: aplicativo nodejs que está ouvindo um evento RabbitMQ e atendendo ao servidor websocket (usando socket.io)
- Servidro Nginx
- Banco de dados PosgreSQL
- Broker de mensagens RabbitMQ
Como nos exemplos anteriores, nosso back-end PHP será servidor via Nginx e PHP-FPM.
Aqui podemos ver o arquivo docker-compose para configurar todos os serviços:
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
Vamos usar os mesmos arquivos do Docker utilizados no artigo anterior, mas também precisamos de novos para o worker, o servidor de banco de dados e a fila de mensagens:
Worker:
FROM node:alpine
EXPOSE 8000
WORKDIR /code/src
COPY ./io .
RUN npm install
ENTRYPOINT ["npm", "run", "serve"]
O script do worker é um script simples, que serve ao servidor socket.io e emite um websocket dentro de cada mensagem para a fila do RabbitMQ.
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 <img draggable="false" class="emoji" alt="?" src="https://s0.wp.com/wp-content/mu-plugins/wpcom-smileys/twemoji/2/svg/1f642.svg">
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);
Servidor de banco de dados:
FROM postgres:9.6-alpine
COPY pg/init.sql /docker-entrypoint-initdb.d/
Como podemos ver, vamos gerar a estrutura do banco de dados na primeira compilação.
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);
Com o servidor RabbitMQ, vamos usar a imagem docker oficial, por isso não precisamos criar um Dockerfile. Nós também mudamos um pouco nossa configuração Nginx. Queremos usar o Nginx para servir back-end e também o servidor socket.io. Isso porque não queremos expor portas diferentes à 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;
}
}
Para evitar problemas de CORS, também podemos usar o destino SCP (o proxy localneo neste exemplo), para servir socket.io também. Então precisamos:
- Mudar nosso arquivo neo-app.json:
"routes": [
...
{
"path": "/socket.io",
"target": {
"type": "destination",
"name": "SOCKETIO"
},
"description": "SOCKETIO"
}
],
E é basicamente isso. Aqui também podemos usar um arquivo docker-compose de “produção” sem expor todas as portas e mapear o sistema de arquivos para a nossa máquina local (útil quando estamos desenvolvendo).
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
E isso é tudo! O projeto completo está disponível na minha conta do GitHub.
***
Gonzalo Ayuso faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela Redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://gonzalo123.com/2018/10/08/working-with-sapui5-locally-part-3-adding-more-services-in-docker/