Docker Compose — Nginx Reverse Proxy Project

2 minute read

Build an Nginx reverse proxy that load-balances traffic across multiple backend application instances — a fundamental production pattern.

Architecture

┌──────────────────────────────────────────────────────┐
│               Docker Network: proxy-net               │
│                                                       │
│              ┌─────────────┐                          │
│   Client ──→ │    Nginx    │                          │
│   :80        │ Reverse     │                          │
│              │ Proxy       │                          │
│              └──────┬──────┘                          │
│                     │                                 │
│          ┌──────────┼──────────┐                      │
│          ▼          ▼          ▼                      │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐            │
│   │  App 1   │ │  App 2   │ │  App 3   │            │
│   │  :5000   │ │  :5000   │ │  :5000   │            │
│   └──────────┘ └──────────┘ └──────────┘            │
└──────────────────────────────────────────────────────┘

Project Setup

mkdir nginx-proxy && cd nginx-proxy
mkdir app nginx

Step 1 — Create the Backend App

Create app/server.py:

from http.server import HTTPServer, BaseHTTPRequestHandler
import os, json

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        hostname = os.environ.get('HOSTNAME', 'unknown')
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({
            'message': 'Hello from backend',
            'hostname': hostname,
            'path': self.path
        }).encode())

HTTPServer(('0.0.0.0', 5000), Handler).serve_forever()

Create app/Dockerfile:

FROM python:3.12-alpine
WORKDIR /app
COPY server.py .
EXPOSE 5000
CMD ["python", "server.py"]

Step 2 — Create Nginx Configuration

Create nginx/nginx.conf:

upstream backend {
    server app:5000;
}

server {
    listen 80;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /health {
        access_log off;
        return 200 'healthy';
        add_header Content-Type text/plain;
    }
}

Step 3 — The Compose File

Create compose.yml:

services:
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
    networks:
      - proxy-net

  app:
    build:
      context: ./app
    deploy:
      replicas: 3
    expose:
      - "5000"
    networks:
      - proxy-net

networks:
  proxy-net:
    driver: bridge

Key detail: The app service uses expose (not ports) because only Nginx needs to reach it — no host-level port mapping required.

Step 4 — Launch the Stack

docker compose up -d --build

Expected output:

[+] Running 5/5
 ✔ Network nginx-proxy_proxy-net  Created
 ✔ Container nginx-proxy-app-1    Started
 ✔ Container nginx-proxy-app-2    Started
 ✔ Container nginx-proxy-app-3    Started
 ✔ Container nginx-proxy-nginx-1  Started

Step 5 — Test Load Balancing

Send multiple requests and observe different hostnames in the response:

for i in 1 2 3 4 5 6; do
  curl -s http://localhost/ | python3 -m json.tool
  echo "---"
done

You should see different hostname values as Nginx distributes requests across the three app replicas:

{
    "message": "Hello from backend",
    "hostname": "a1b2c3d4e5f6",
    "path": "/"
}
---
{
    "message": "Hello from backend",
    "hostname": "f6e5d4c3b2a1",
    "path": "/"
}
---

Step 6 — Scale Up and Down

Scale the backend to 5 instances:

docker compose up -d --scale app=5
docker compose ps
NAME                   STATUS    PORTS
nginx-proxy-app-1      Up        5000/tcp
nginx-proxy-app-2      Up        5000/tcp
nginx-proxy-app-3      Up        5000/tcp
nginx-proxy-app-4      Up        5000/tcp
nginx-proxy-app-5      Up        5000/tcp
nginx-proxy-nginx-1    Up        0.0.0.0:80->80/tcp

Scale back down:

docker compose up -d --scale app=2

Enhancements to Try

  1. Add SSL termination with a self-signed certificate
  2. Add static file serving from Nginx directly
  3. Add rate limiting in the Nginx configuration
  4. Add a monitoring dashboard with Nginx status module

Quick Reference

Item Detail
Nginx image nginx:alpine
Proxy port 80:80
Backend port 5000 (exposed, not published)
Replicas 3 (configurable via --scale)
Load balancing Round-robin (default Nginx upstream)
Config mount ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro