Setting up a multi-container application using Docker-compose

I’m writing this post as a guide to setup a multi-container application using Docker-compose. The application will be running two services, a Node application and a RabbitMQ server.

The github repo for this blog is hosted here

Initial Setup

I’ll be using node-project-boilerplate for the initial setup.

Clone node-project-boilerplate into a folder ‘learn-docker-compose’

Setup docker-compose.dev.yml

version: '3'
services:


Configuration to setup RabbitMQ

  rabbitmq:
    image: 'rabbitmq:3.6.6-management'
    ports:
      - "4369:4369"
      - "5672:5672"
      - "15672:15672"
      - "25672:25672"
      - "35197:35197"
    volumes:
      - ./data:/var/lib/rabbitmq
      - ./data/logs:/var/log/rabbitmq
    hostname: rabbit

Additional configuration for RabbitMQ


Configuration to setup Node application

  node-application:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "9229:9229"
    volumes:
      - /usr/app/node_modules
      - .:/usr/app


Configuration to setup Tests

  tests:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - /usr/app/node_modules
      - .:/usr/app
    command: ["yarn", "run", "tests"]


Setup Dockerfile.dev

FROM node:8-alpine

WORKDIR /usr/app/

COPY package*.json ./
RUN yarn install

CMD ["./node_modules/nodemon/bin/nodemon.js", "--inspect=0.0.0.0:9229", "index.js"]


Setup index.js to receive messages from RabbitMQ

Install amqplib as a dependency using yarn add amqplib

I’m using the code from RabbitMQ tutorial [2] to setup index.js which will receive messages from RabbitMQ

const amqp = require('amqplib/callback_api');

amqp.connect('amqp://guest:guest@rabbitmq:5672', (err, conn) => {
    if (err) {
        console.log(`Error ${err}`);
    }
    conn.createChannel((error, ch) => {
        if (error) {
            console.log(`Error ${err}`);
        }
        const q = 'hello';

        ch.assertQueue(q, { durable: true });
        console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", q);
        ch.consume(q, (msg) => {
            console.log(" [x] Received %s", msg.content.toString());
        }, { noAck: true });
    });
});

The connection URL uses the default username and password to connect amqp://guest:guest@rabbitmq:5672. @rabbitmq is taken from the service name used in docker-compose.dev.yml


Setup sendMessage.js to send messages to RabbitMQ

const amqp = require('amqplib/callback_api');

amqp.connect('amqp://guest:guest@rabbitmq:5672', (err, conn) => {

    if (err) {
        console.log(`Error ${err}`);
    }
    conn.createChannel((error, ch) => {
        if (error) {
            console.log(`Error ${err}`);
        }

        const q = 'hello';
        const msg = 'Hello World!';

        ch.assertQueue(q, { durable: true });
        ch.sendToQueue(q, Buffer.from(msg), { persistent: true });
        console.log(`Sent ${msg}`);
    });
    setTimeout(() => { conn.close(); process.exit(0); }, 500);
});


Running multiple containers using docker-compose

docker-compose -f docker-compose.dev.yml up --build

When running the setup using the above command, I ran into the situation where the Node application to receive messages was started before the RabbitMQ server was accepting connections on port 5672.

In order to resolve the order of execution, I used a bash script wait-for-it.sh [3] which ensures that the startup command for the Node application is run only after RabbitMQ server starts listening for connections on port 5672


Setup bash script

Add wait-for-it.sh to scripts folder

Run chmod 755 ./scripts/wait-for-it.sh to make the bash file an executable

Changes to Dockerfile.dev

Setup bash on the alpine image (it isn’t available by default). Add the following command after pulling the base image

RUN apk add --no-cache bash

Modify startup command to the following to invoke the node application only after RabbitMQ starts listening for connections on port 5672

CMD ["./scripts/wait-for-it.sh", "rabbitmq:5672", "--", "./node_modules/nodemon/bin/nodemon.js", "--inspect=0.0.0.0:9229", "index.js"]


Run the container setup again using docker-compose


Run sendMessage.js inside the node-application container


References

[1] RabbitMQ Docker Image

[2] RabbitMQ Tutorial

[3] wait-for-it.sh

Setting up a RabbitMQ Cluster on Docker

Modifying default username and password on RabbitMQ