'Creating Docker container for a micro-service based on node.js & grpc

I am trying to build a chat-service by using node.js and grpc, where two services running on different port can communicate to each other. I have created a single ".js" file where I wrote the code for server part as well as client part and I run that file by passing env variables. To run two different services from one single file I use this command SERVER_PORT=2000 CLIENT_PORT=5000 node filename(in one terminal) and SERVER_PORT=5000 CLIENT_PORT=2000 node filename(in another) after this two services were able to chat.

In my local machine it was working but after building docker image, two docker containers and docker-compose.yml I am getting "Circular Dependency" error. I recently started working on these things and I am stuck.

Please share your views on this. Thank you in advance.

my .js file

var PROTO_PATH = "./allenchat.proto";
//var vv = require('./allenchat.proto');
var dotEnv = require("dotenv").config();

var grpc = require("grpc");
var protoLoader = require("@grpc/proto-loader");
var packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
var protoDescriptor = grpc.loadPackageDefinition(packageDefinition);

var grpcChat = protoDescriptor.io.mark.grpc.grpcChat;
var clients = new Map();
const Server_Add = process.env.ADD_PORT;
const Client_Add = process.env.FRND_PORT;

function chat(call) {
  call.on("data", function (ChatMessage) {
    user = call.metadata.get("username");
    msg = ChatMessage.message;
    console.log(`${user} ==> ${msg}`);
    for (let [msgUser, userCall] of clients) {
      if (msgUser != user) {
        userCall.write({
          from: user,
          message: msg,
        });
      }
    }
    if (clients.get(user) === undefined) {
      clients.set(user, call);
    }
  });
  call.on("end", function () {
    call.write({
      fromName: "Chat server",
      message: "Nice to see ya! Come back again...",
    });
    call.end();
  });
}

var host = "0.0.0.0";
var server = new grpc.Server();
server.addService(grpcChat.ChatService.service, {
  chat: chat,
});
server.bind(`${host}:${Server_Add}`, grpc.ServerCredentials.createInsecure());
server.start();
console.log("Chat Server started on", Server_Add);

function callService() {
  var client = new grpcChat.ChatService(
    `${host}:${Client_Add}`,
    grpc.credentials.createInsecure()
  );
  const readline = require("readline");
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  var user = process.env.USER;
  var metadata = new grpc.Metadata();
  metadata.add("username", user);
  var call = client.chat(metadata);

  call.on("data", function (ChatMessage) {
    console.log(`${ChatMessage.from} ==> ${ChatMessage.message}`);
  });
  call.on("end", function () {
    console.log("Server ended call");
  });
  call.on("error", function (e) {
    console.log(e);
  });

  rl.on("line", function (line) {
    if (line === "quit") {
      call.end();
      rl.close();
    } else {
      call.write({
        message: line,
      });
    }
  });

  console.log("Enter your messages below:");
}

setTimeout(callService, 7000);

Dockerfile

FROM node:14
RUN mkdir /app
ADD . /app
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5000 8000
CMD ["node", "server"]

docker-compose.yml file

version: '2.2'
services:
  Allen:
    image: grpc
    ports:
      - "8000:8000"
    networks:
      - proxynet
    depends_on:
      Bob:
        condition: service_healthy 
    healthcheck:
      test: ["CMD"]
      timeout: 20s
      retries: 10
  Bob:
    image: grpc
    ports:
      - "5000:5000"
    networks:
      - proxynet
    healthcheck:
      test: ["CMD"]
      timeout: 20s
      retries: 10
networks:
  proxynet:
    driver: bridge


Solution 1:[1]

I had to make a few changes to get your project to work, I'll explain them as I go.

In server.js I hardcoded the address the server binds to be 0.0.0.0 so the server will listen on all addresses.

I also made the address the client connects to configurable via a HOST environment variable.

server.js

// Set client host from environment variable.
var host = process.env.HOST;
var server = new grpc.Server();
server.addService(grpcChat.ChatService.service, {
  chat: chat,
});

// Hardcoded server address
server.bind(`0.0.0.0:${Server_Add}`, grpc.ServerCredentials.createInsecure());
server.start();
console.log("Chat Server started on", Server_Add);

function callService() {
  var client = new grpcChat.ChatService(
    `${host}:${Client_Add}`,
    grpc.credentials.createInsecure()
  );

I had to do this because you're using a bridge network in your docker-compose.yml therefore they're no longer sharing the same network address as they would if you were running them on your host or using the host network type made available by Docker.

Because the server's effectively have their own IP addresses there is no need to use different ports such as 5000 and 8000 instead you can have them both run on the same port I've used 9090 as this is the typical port gRPC server's listen on.

However, the 9090 ports of the containers have been published on the host as 5000 and 8000 ports respectively in the docker-compose.yml file.

I have also installed netcat as a dependency in your Dockerfile so I could implement the healthcheck correctly in the docker-compose.yml by checking the server ports were accessible on localhost and 9090 internally relative to the container.

Dockerfile

FROM node:14
RUN apt-get update && apt-get install -y \
netcat \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /app
ADD . /app
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 9090
CMD ["node", "server"]

I have added comments here to explain the other changes.

docker-compose.yml

version: '2.2'
services:
  # Hostname of the container on bridge network
  allen:
    image: grpc
    environment:
      USER: Allen
      # Server port to bind to
      ADD_PORT: 9090
      # Client port
      FRND_PORT: 9090
      # Client hostname
      HOST: bob
    ports:
      - "8000:9090"
    networks:
      - proxynet
    depends_on:
      bob:
        condition: service_healthy 
    healthcheck:
      # Check server is listening on expected address/port
      test: ["CMD", "nc", "-zv", "localhost", "9090"]
      # The default is 30s which takes ages to detect health status
      interval: 1s
      timeout: 20s
      retries: 10
    # Required in order to attach to the container and write messages
    stdin_open: true
    tty: true
  # Hostname of the container on bridge network
  bob:
    image: grpc
    environment:
      USER: Bob
      # Server port to bind to
      ADD_PORT: 9090
      # Client port
      FRND_PORT: 9090
      # Client hostname
      HOST: allen
    ports:
      - "5000:9090"
    networks:
      - proxynet 
    healthcheck:
      # Check server is listening on expected address/port
      test: ["CMD", "nc", "-zv", "localhost", "9090"]
      # The default is 30s which takes ages to detect health status
      interval: 1s
      timeout: 20s
      retries: 10
    # Required in order to attach to the container and write messages
    stdin_open: true
    tty: true
networks:
  proxynet:
    driver: bridge

I also guessed the definition of your gRPC service defined in allenchat.proto, probably not exactly correct but good enough to test.

syntax = "proto3";

package io.mark.grpc.grpcChat;

message ChatMessage {
  string message = 1;
  string from = 2;
  string fromName = 3;
}

service ChatService {
  rpc chat(stream ChatMessage) returns (stream ChatMessage);
}

Once I had both containers started I ran docker attach <CONTAINER> to attach my shell to the respective containers in order to send messages to confirm both containers were connected and could send messages back and fourth.

Terminal log

[+] Running 2/1
 ? Container shubhi-bob-1    Recreated                                                                                                                                                  0.1s
 ? Container shubhi-allen-1  Recreated                                                                                                                                                  0.1s
Attaching to shubhi-allen-1, shubhi-bob-1
shubhi-bob-1    | Chat Server started on 9090
shubhi-allen-1  | Chat Server started on 9090
shubhi-bob-1    | Enter your messages below:
shubhi-allen-1  | Enter your messages below:
> Test
> Allen ==> Test
shubhi-bob-1    | Test
shubhi-allen-1  | Bob ==> Test

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1