Home

The Goals

This will create a system where nginx acts as a reverse-proxy that passes requests along to some web servers.
There will be 3 web-servers, all clones of each other. Nginx passes requests along, as a load-balancer, with nginx's default "round-robin" approach - continuously "rotating" requests between the web servers.
The web servers are skeleton node http servers.
The web servers and the nginx instance will all run in docker.

A Diagram

:8081
:8080
:8080
:8080
node server I
node server II
node server III
nginx proxy
outside world

The Node Server

Build The Node Server

Here, a little node+express server.

The directory and file:

mkdir node-server
cd node-server 
touch index.js

npm init -y

The Javascript file:

const e = require("express")
const expObj = e()
const { hostname } = require("os")
const THIS_HOST = hostname()
const PORT = process.env.API_PORT || 8080
function rootHandler(req, res) {
  return res.send(`Hello from ${THIS_HOST}!`)
}
expObj.get("/", rootHandler)
const api_server = expObj.listen(PORT, () => console.log(`app running on ${PORT} on ${THIS_HOST}`));

process.on('SIGTERM', () => {
  console.log('SIGTERM signal received: closing HTTP server');
  api_server.close(() => {
    console.log('express server closed');
  });
});

Build A Dockerfile

FROM node:18

# workdir - here, where the app will run
WORKDIR /node-api

# From the sibling app dir on the host to the docker image workdir
COPY app /node-api

# command to run during the image build process
RUN npm i

# NOTE: entrypoint OVER CMD to pass along process signals to the node process!
ENTRYPOINT [ "node", "index.js" ]

Verify The Node Server

To test the node server without docker and just node on your machine

  • run npm i in the node-server directory
  • run node . in the node-server directory
  • use a browser & enter the url localhost:8080
  • the browser should return the "Hello from (the name of your machine)"

Verify The Node Server In Docker

To test the node server with docker

  • build an image by running docker build -t nodebox . in the node-server directory
  • run the image as a container with docker run -d -rm --name node-box -p 8080:8080 nodebox
  • use a browser & enter the url localhost:8080
  • the browser should return the "Hello from (the name of the docker "host", which is probably a bit of garbly-gook to look at)"

The NGINX Load-Balancer

Build the Nginx config

nginx docs have a great write-up on load-balancing with nginx, including notes on...

  • http context: where the http server conf (directive) is specified
  • upstream: defining a group of servers and naming them
  • server inside the "upstream" block here: defines an address for each server - the port is optional and when not present defaults to 80. this can have more config details (hope to cover elsewhere)
    • server as the server block, configuring a "virtual server"
  • listen: where to "listen" for requests
  • location: request-url-specific config settings
  • proxy_pass: the url (protocol+address) of the server that gets proxied to

Here's the nginx config:

# the context here: http context
http {

  # defaults to a "round-robin" method for load balancing across servers
  # here, the servers are named "nodebackend"
  upstream nodebackend {

    server nodeapp1:8080;
    server nodeapp2:8080;
    server nodeapp3:8080;

  }

  server {
    # here, listening on port 8081
    listen 8081;
    location / {
      
      proxy_pass http://nodebackend/;
    }
  }
}

# can set things like maximum-worker-connections...
events {}

The nginx config is setup to listen on port 8081 and round-robin balance the requests to 3 different apps: nodeapp1 on port 8080, nodeapp2 on port 8080, and nodeapp3 on port 8080.

Build a Docker Network

In order for the containers to be able to "talk to" each other, they all will get put on the same network. Here, this network will be called nxnet: docker network create nxnet.

Run And Connect All Of the Containers

3x the node app

This showcase one nice detail of working with docker: spinning up 3 node apis from the same image:

  • docker run --hostname nodeapp1 --network nxnet name nodeapp1 --env API_PORT=8080 -d nodebox
  • docker run --hostname nodeapp2 --network nxnet name nodeapp2 --env API_PORT=8080 -d nodebox
  • docker run --hostname nodeapp3 --network nxnet name nodeapp3 --env API_PORT=8080 -d nodebox

The above commands include the --network nxnet flag.
Containers can be started without that flag and later joined to a network...

  • create a container in one command: docker run --hostname nodeapp4 name nodeapp4 -d nodebox
  • join the network in another command: docker network connect nxnet nodeapp4

Also, these containers do not expose ports! These containers will only "talk to" the nginx container, and the nginx container will be accessible from the host machine.

The NGINX container

  • docker run --name nxproxy --hostname ng1 -p 8081:8081 --network nxnet -v $PWD/nginx.conf:/etc/nginx/nginx.conf nginx:alpine

Note the volume mount: the config file made earlier gets mounted to the default location in the container, /etc/nginx/nginx.conf.

Test The Setup

use a browser and go to localhost:8081.
This will connect to nginx, which passes the request, round-robin, to the 3 instances of the node api.
The UI should show Hello from ______.
Refreshing the page should show "rotating" values in the Hello from ______ - 3 different values.
This rotation of values illustrates that nginx is having the 3 instances of the node server "handle" the requests and responses, as the 3 rotating values represent the 3 "hosts" of each docker container.

Compose For A Different Approach

Docker compose can change how this gets put together:

  • in the cli versions above...
    • A PRO: there are no "artifacts" - no files, no documents, no stored port numbers or app names - that is unless you "store" all of the cli commands needed to run each container, spin up the network, etc.
    • A CON: without artifacts, there are no "static" references to the details
  • in docker-compose
    • A PRO: all "config" for the containers and the network are stored in the docker-compose file! 1 file with all the deets
Tags: