Nginx Begineer Guide

Nginx (pronounced "engine-x") is an open-source web server, reverse proxy server, and load balancer. It was designed to handle a large number of simultaneous connections efficiently, making it popular for serving high-traffic websites.

Key Features:

  1. Web Server: Delivers static content (like HTML, images, videos) and dynamic content using FastCGI, SCGI, or uWSGI.

  2. Reverse Proxy: Forwards client requests to backend servers, masking their identity. This is useful for load balancing and caching.

  3. Load Balancer: Distributes incoming traffic among multiple servers to ensure reliability and performance.

  4. HTTP Cache: Caches static and dynamic content to reduce load on application servers.

  5. Security Gateway: Can filter traffic, prevent DDoS attacks, and manage SSL/TLS termination.


How Nginx Works:

1. Handling Requests:

  • Nginx uses an event-driven, asynchronous architecture. Unlike traditional servers that create a new thread/process for each connection, Nginx uses a small number of worker processes.

  • Each worker process can handle thousands of connections concurrently using a non-blocking I/O model.

2. Configuration Files:

  • Nginx is configured through simple text-based configuration files, typically located at /etc/nginx/nginx.conf.

  • The configuration defines directives to manage virtual hosts, server blocks, and proxies.

3. Core Modules:

Nginx supports various modules for functionality like:

  • HTTP module: Handles HTTP requests and responses.

  • Stream module: For TCP/UDP traffic.

  • Mail module: Acts as a proxy for email services.

4. Request Flow Example:

  1. Client Request: A browser sends an HTTP request to Nginx.

  2. Processing by Nginx:

    • Nginx checks its configuration to determine how to handle the request (serve a static file, reverse proxy, etc.).

    • If configured as a reverse proxy, it forwards the request to an upstream application server.

  3. Response: The application server sends a response to Nginx, which passes it back to the client.


Common Use Cases:

  1. Serving Static Content: High-speed delivery of files (images, videos, etc.).

  2. Reverse Proxy for Microservices: Forwarding requests to different backend services.

  3. Load Balancing: Distributing traffic across multiple application servers to improve performance and reliability.

  4. TLS Termination: Handling HTTPS connections, offloading the SSL workload from application servers.


Advantages:

  • High Performance: Can handle tens of thousands of connections with minimal resource consumption.

  • Scalability: Easily scales to support growing traffic.

  • Flexibility: Supports multiple protocols (HTTP, HTTPS, WebSocket, gRPC).

  • Extensible: Customizable with modules and plugins.


Comparison to Apache:

  • Performance: Nginx excels with static content and high concurrency; Apache may be better for dynamic content using its modules.

  • Configuration: Nginx uses simpler, more predictable configurations.

  • Architecture: Nginx's event-driven model offers better efficiency compared to Apache's multi-threaded approach.

Using Nginx as an API Gateway with Docker and Node.js

In this post, I’ll walk you through setting up Nginx as an API Gateway to load balance multiple instances of a Node.js application. We’ll containerize everything using Docker Compose to streamline the setup process. Here’s a step-by-step guide:


Step 1: Create the Node.js Application

We'll create a simple Node.js app using Express.js:

  1. Project Structure:

     nginx-api-gateway/
     ├── node-app/
     │   ├── package.json
     │   ├── server.js
     │   └── Dockerfile
     ├── nginx/
     │   └── nginx.conf
     └── docker-compose.yml
    
  2. Node.js Server (server.js):

     const express = require('express');
     const app = express();
     const PORT = process.env.PORT || 3000;
    
     app.get('/', (req, res) => {
         res.send(`Response from Node.js instance on port ${PORT}`);
     });
    
     app.listen(PORT, () => {
         console.log(`Server is running on port ${PORT}`);
     });
    
  3. Dependencies (package.json):

     {
       "name": "node-api",
       "version": "1.0.0",
       "scripts": { "start": "node server.js" },
       "dependencies": { "express": "^4.18.2" }
     }
    

Step 2: Dockerize the Node.js App

Dockerfile:

FROM node:18
WORKDIR /usr/src/app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

Step 3: Set Up Nginx Configuration

Create an Nginx config to act as the gateway and load balancer:

nginx.conf:

# Define an upstream block to create a load-balanced group of backend servers (Node.js instances)
upstream nodejs_cluster {
    # The first Node.js instance (container) running on port 3000
    server node-app1:3000;

    # The second Node.js instance (container) running on port 3000
    server node-app2:3000;
}

# Server block for handling HTTPS traffic (SSL/TLS)
server {
    # Listen for HTTPS traffic on port 443 with SSL enabled
    listen 443 ssl;
    # Specify the server name (usually the domain or localhost)
    server_name localhost;

    # SSL certificate configuration (self-signed in this case)
    ssl_certificate /etc/nginx/certs/nginx-selfsigned.crt;  # Path to the public SSL certificate
    ssl_certificate_key /etc/nginx/certs/nginx-selfsigned.key;  # Path to the private SSL certificate key

    # Proxy incoming requests to the Node.js instances defined in the upstream block
    location / {
        # Pass the request to the nodejs_cluster upstream group
        proxy_pass http://nodejs_cluster;

        # Set the 'Host' header to match the original host in the incoming request
        proxy_set_header Host $host;

        # Set the 'X-Real-IP' header to the client's IP address to preserve the original client's IP
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Server block for handling HTTP traffic (port 80) and redirecting it to HTTPS
server {
    # Listen for HTTP traffic on port 80
    listen 80;
    # Specify the server name (usually the domain or localhost)
    server_name localhost;

    # This location block handles all incoming requests and redirects them to HTTPS
    location / {
        # Perform a 301 (permanent) redirect to HTTPS version of the site
        return 301 https://$host$request_uri;
    }
}

Step 4: Docker Compose Configuration

Define services for both Node.js instances and Nginx:

docker-compose.yml:

version: '3.8'

services:
  node-app1:
    build: ./app
    container_name: node-app1
    networks:
      - my_network
    environment:
      - APP_NAME=node-app1
    ports:
      - "3001:3000"

  node-app2:
    build: ./app
    container_name: node-app2
    networks:
      - my_network
    environment:
      - APP_NAME=node-app2
    ports:
      - "3002:3000"

  nginx:
    image: nginx:latest
    container_name: nginx-api-gateway
    networks:
      - my_network
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro   # Link Nginx config
      - ./nginx/certs:/etc/nginx/certs:ro             # Link SSL certs
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - node-app1
      - node-app2

networks:
  my_network:
    driver: bridge

Step 5: Generate SSL Certificates (Optional for Testing)

For local testing, create a self-signed certificate:

mkdir nginx-certs
cd nginx-certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout nginx-selfsigned.key -out nginx-selfsigned.crt

Step 6: Build and Run Everything

In your project root, run:

docker-compose up --build