nginx-and-docker-project-intro: Nginx and Docker Project Intro: Turning One Node App into a Load-Balanced Cluster

A compact DevOps template that shows how Compose, least-connections routing, and failover retries make a simple Express app behave like a resilient service.

7 min read • View on GitHub • More from bhar-gavi1029

An overhead view of a gateway feeding requests into three container blocks, with one block dimmed to suggest failure while the others stay active. The scene explains how a proxy can distribute traffic across replicas and keep serving when one backend drops out.
One proxy, three replicas, and a fallback path that keeps the system alive.
Key Takeaways

The smallest possible cluster that still feels real

This repository is not trying to be a platform. It is a teaching setup that shows how one Node app can become a resilient service with a proxy in front and multiple replicas behind it.

This project is a simple introduction to Nginx and Docker.

That understatement is the point. The repo is small, but it demonstrates a full operational pattern: Compose starts the pieces, Nginx decides where traffic goes, and the app tells you which replica answered.

Three app containers, one proxy, one truth

This diagram makes the proxy decision visible. You can watch traffic move, see a node fail, and see the retry land somewhere healthy.

The Compose file spins up three app services from the same image, then places Nginx in front. That means the cluster is not a pile of special snowflakes. It is one image, three names, and one reverse proxy.

services:
  app1:
    build: .
    environment:
      - APP_NAME=app1
    ports:
      - "3001:3000"

  app2:
    build: .
    environment:
      - APP_NAME=app2
    ports:
      - "3002:3000"

  app3:
    build: .
    environment:
      - APP_NAME=app3
    ports:
      - "3003:3000"

  nginx:
    build: .
    depends_on:
      - app1
      - app2
      - app3
    ports:
      - "8080:80"

Those host port mappings matter. They let you bypass the proxy and talk to each container directly, which is useful when you want to debug routing, app behavior, or a bad mount without guessing.

A close-up of a layered build bench with stacked image layers, a cache chamber, and a hand moving a project from build stage to runtime stage. The scene explains why the Dockerfile is more than boilerplate, because it uses caching and a non-root handoff to shape how the image is built and run.
The Dockerfile teaches modern container habits, not just syntax.

Why Nginx makes this feel smarter than a demo

The real upgrade is not that Nginx sits in front of the app. It is that the proxy uses least_conn, so traffic goes to the least busy backend instead of merely the next one in line.

That matters when requests are uneven. A busy container stops monopolizing new work, and the system looks more like an actual service than a classroom example.

upstream app_cluster {
  least_conn;
  server app1:3000 max_fails=3 fail_timeout=10s;
  server app2:3000 max_fails=3 fail_timeout=10s;
  server app3:3000 max_fails=3 fail_timeout=10s;
}

server {
  location / {
    proxy_pass http://app_cluster;
    proxy_next_upstream error timeout http_502 http_503 http_504;
  }
}

The failover settings are the other half of the story. If a backend starts returning errors, Nginx can skip it and retry the request on another healthy upstream, which is the simplest possible kind of resilience.

The app is a tracer bullet, not just a server

The Express app does one useful thing beyond serving content. It tags responses with APP_NAME, which turns load balancing into something you can inspect in logs and browser output.

app.get('/', (req, res) => {
  console.log(`request served by ${process.env.APP_NAME}`)
  res.sendFile(path.join(__dirname, 'index.html'))
})

That is a small move with a big payoff. Instead of trusting the proxy, you can watch the replica identity change as requests move around the cluster.

The repository also checks for the front-end file explicitly, which is a practical debugging habit. It makes path and mount mistakes easier to catch when you are still learning how containers see the filesystem.

What this teaches better than a generic Nginx tutorial

SetupTraffic selectionFailure handlingWhat the app revealsOperational lesson
Single container tutorialOne backend onlyNo real fallbackNothing interesting changesGood for basics, weak on resilience
This repo's Compose plus Nginx setupLeast connections across three replicasFailed upstreams can be retried automaticallyEach replica identifies itself in logsShows observable load balancing and recovery
Naive round robin proxyNext backend in sequenceMay keep sending traffic to a hot or failing nodeUsually no replica visibilityTeaches proxying, but not load-aware routing

This is why the repository stands out. It stays small enough to understand in one sitting, but it teaches a real pattern: one app, several replicas, visible routing, and a proxy that keeps the service flowing when one node slips.