Layer 4 Load Balancing Using NGINX and Docker

This post will guide you through how to use NGINX and Docker for setting up a Layer 4 (transport layer) load balancer. We want to balance the incoming TCP traffic into two servers using NGINX as load balancer. We’ll also make a custom NGINX image that goes along with the configuration inside nginx.conf and our choice is Docker Compose for the orchestration.

Step 1: Initialize Go Projects

First, let’s create two Go projects for the TCP server and client respectively.

The project structure should be following:

├── client
│   ├── Dockerfile
│   ├── go.mod
│   └── main.go
├── docker
│   └── docker-compose.yaml
├── nginx
│   ├── Dockerfile
│   └── nginx.conf
└── server
    ├── Dockerfile
    ├── go.mod
    └── main.go

1.1 TCP Server

Create a directory structure for the TCP server:

mkdir server
cd server
go mod init tcp-server

Now, create the main.go file with the following content:

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    port := "8000"
    hostname, err := os.Hostname()
    listener, err := net.Listen("tcp", ":"+port)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Golang TCP server listening on port %sn", port)

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Connection error:", err)
            continue
        }

        go func(c net.Conn) {
            defer c.Close()
            c.Write([]byte("Hello from " + hostname + " on port " + port + "n"))
        }(conn)
    }
}

This is a server listening on port 8000 that responds with a message and the hostname of the current server.

Now create a Dockerfile for the server:-

FROM golang:1.24-alpine AS builder

WORKDIR /app

COPY go.mod ./
RUN go mod tidy
COPY . .
RUN go build -o tcp-server .

FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

COPY --from=builder /app/tcp-server .

EXPOSE 8000

CMD ["./tcp-server"]

1.2 TCP Client Proje

Create a TCP client:

mkdir -p client
cd client
go mod init tcp-client

Now, create the main.go file with the following content:

package main

import (
    "bufio"
    "fmt"
    "net"
)

func main() {
    port := "8080"
    address := "localhost:" + port // NGINX LB port

    conn, err := net.Dial("tcp", address)
    if err != nil {
        fmt.Printf("Error connecting to %s: %vn", address, err)
        return
    }
    defer conn.Close()

    fmt.Printf("Connected to %sn", address)

    // Read response
    message, err := bufio.NewReader(conn).ReadString('n')
    if err != nil {
        fmt.Println("Error reading response:", err)
        return
    }

    fmt.Printf("Received: %sn", message)
}

This client connects to the NGINX load balancer at port 8080 and reads it.

Step 2: Setting Up NGINX to Use Layer 4 Load Balancing

NGINX supports Layer 4 load balancing using the stream module. Create a directory structure for NGINX:

mkdir -p nginx
cd nginx

Create the nginx.conf file with the following content:

worker_processes 1;

events {
    worker_connections 1024;
}

stream {
    upstream backend {
        server server-1:8000;
        server server-2:8000;
    }

    server {
        listen 8080;
        proxy_pass backend;
    }
}

This configuration defines an upstream block with two servers (server-1 and server-2) running on port 8000. The server block listens on port 8080 and forwards incoming TCP connections to the upstream servers.

Next, create a Dockerfile for NGINX:

FROM nginx:alpine

RUN apk add --no-cache nginx

# Create directory for custom config
RUN mkdir -p /etc/nginx

# Copy your nginx.conf
COPY nginx.conf /etc/nginx/nginx.conf

# Create required dirs
RUN mkdir -p /run/nginx

EXPOSE 8080

CMD ["nginx", "-g", "daemon off;"]

Step 3: Build and Push The Docker Images

  1. Build the Docker images:
   docker build -t olymahmudmugdho/go-tcp-server -f server/Dockerfile server/
   docker build -t olymahmudmugdho/tcp-nginx -f nginx/Dockerfile nginx/
  1. Push the Docker images:
   docker push olymahmudmugdho/go-tcp-server
   docker push olymahmudmugdho/tcp-nginx

Step 4: Use Docker Compose to Orchestrate the Setup

Create a docker-compose.yaml file in the root directory:

services:
  nginx:
    image: olymahmudmugdho/tcp-nginx
    container_name: nginx
    ports:
      - "8080:8080"
    depends_on:
      - server-1
      - server-2

  server-1:
    image: olymahmudmugdho/go-tcp-server
    container_name: server-1
    hostname: server-1

  server-2:
    image: olymahmudmugdho/go-tcp-server
    container_name: server-2
    hostname: server-2

This configuration defines three services:

  1. nginx: The NGINX load balancer.
  2. server-1 and server-2: Two instances of the TCP server.

Step 5: Run

  1. Run the setup using Docker Compose:
   docker compose -f docker/docker-compose.yaml up -d
  1. Test the setup:
    Run the client to send requests to the load balancer:
   cd client
   go run main.go
   go run main.go

You should see responses alternating between server-1 and server-2, indicating that the load balancer is working correctly.

Conclusion

This post provides a guide to creating a Layer 4 load balancer with Docker and NGINX to distribute TCP requests between two different servers. NGINX stream module, Docker Compose.

Similar Posts