'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 |