DEV Community

Cedar Daniel
Cedar Daniel

Posted on

Setting Up NGINX as A Reverse Proxy for your Nest JS Microservices

When building applications with NestJS, it's common to break features into microservices for better scalability, maintainability, and fault isolation. While this approach brings flexibility, it also introduces complexity in how clients communicate with multiple services. That's where NGINX comes in.

What is NGINX?
NGINX is a high-performance web server and reverse proxy widely used to handle routing, load balancing, SSL termination, and request buffering. By placing NGINX in front of your NestJS microservices, you create a single entry point that can manage traffic efficiently, hide internal service structure from the outside world, and improve overall security and performance.

In this article, we'll walk through the process of setting up NGINX as a reverse proxy for your NestJS microservices. We'll cover why this setup is useful, how to configure NGINX to route traffic to different services, and best practices for production-ready deployments. Whether you're running on Docker, Kubernetes, or a bare-metal server, the same principles apply.

1. Spin Up New Nest JS Application
We'll do this by leveraging the Nest JS CLI

nest new nestjs-nginx
Enter fullscreen mode Exit fullscreen mode

2. Convert App to Monorepo

We'll do this by generating a new app within our current app

nest generate app app-b
Enter fullscreen mode Exit fullscreen mode

Now, let's rename the original app to app-a for clarity.

1.Rename the folder apps/my-monorepo to apps/app-a.

2.Open the nest-cli.json file in the root directory. You will see an entry for "my-monorepo". Change its name to "app-a".

Your nest-cli.json should now look something like this:

{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "apps/app-a/src",
  "compilerOptions": {
    "deleteOutDir": true,
    "webpack": true,
    "tsConfigPath": "apps/app-a/tsconfig.app.json"
  },
  "monorepo": true,
  "root": "apps/app-a",
  "projects": {
    "app-a": {
      "type": "application",
      "root": "apps/app-a",
      "entryFile": "main",
      "sourceRoot": "apps/app-a/src",
      "compilerOptions": {
        "tsConfigPath": "apps/app-a/tsconfig.app.json"
      }
    },
    "app-b": {
      "type": "application",
      "root": "apps/app-b",
      "entryFile": "main",
      "sourceRoot": "apps/app-b/src",
      "compilerOptions": {
        "tsConfigPath": "apps/app-b/tsconfig.app.json"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We'll update the code in the app.service.ts of service A and B respectively to

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello from service A!';
  }
}
Enter fullscreen mode Exit fullscreen mode

and

import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello from service B!';
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Dockerize Both Applications

Step 1: Create a .dockerignore File

First, we'll create a .dockerignore file in the root directory of our nestjs-nginx project. This prevents unnecessary files from being copied into our Docker images, which speeds up the build process.

# Dependency directories
node_modules/

# Build artifacts
dist/

# Git
.git
.gitignore

# Docker
Dockerfile
docker-compose.yml

# IDE/Editor specific
.idea
.vscode

# Log files
*.log
Enter fullscreen mode Exit fullscreen mode

Step 2: Create a Dockerfile for app-a

Next, we'll create a Dockerfile inside the apps/app-a/ directory.
apps/app-a/Dockerfile

# ---- Build Stage ----
FROM node:20-alpine AS builder

WORKDIR /usr/src/app

# Copy root dependencies and config
COPY package*.json tsconfig*.json nest-cli.json ./

# Copy specific app and shared libs
COPY apps/app-a ./apps/app-a
COPY libs ./libs

# Install dependencies and build the app
RUN npm install
RUN npm run build app-a

# ---- Production Stage ----
FROM node:20-alpine AS production

WORKDIR /usr/src/app

# Copy only necessary files from builder
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/dist/apps/app-a ./dist

EXPOSE 3000

CMD ["node", "dist/main.js"]
Enter fullscreen mode Exit fullscreen mode

Step 3: Create a Dockerfile for app-b

Now, create a nearly identical Dockerfile inside the apps/app-b/ directory. The only changes are the lines for building app-b and copying its specific dist folder.
apps/app-b/Dockerfile

# ---- Build Stage ----
FROM node:20-alpine AS builder

WORKDIR /usr/src/app

COPY package*.json tsconfig*.json nest-cli.json ./
COPY apps/app-b ./apps/app-b
COPY libs ./libs

RUN npm install
RUN npm run build app-b

# ---- Production Stage ----
FROM node:20-alpine AS production

WORKDIR /usr/src/app

COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/dist/apps/app-b ./dist

EXPOSE 3001

CMD ["node", "dist/main.js"]
Enter fullscreen mode Exit fullscreen mode

Step 4: Create the docker-compose.yml File

In the root of your nestjs-nginx project, create a docker-compose.yml file. This file will define and orchestrate your services.
docker-compose.yml

services:
  app-a:
    build:
      context: .
      dockerfile: apps/app-a/Dockerfile
    ports:
      - '3000:3000'
    restart: always

  app-b:
    build:
      context: .
      dockerfile: apps/app-b/Dockerfile
    ports:
      - '3001:3001'
    restart: always
Enter fullscreen mode Exit fullscreen mode

Step 5: Build and Run the Containers 🚀

You're all set! Open your terminal at the root of the project and run the following command:

docker compose up --build -d
Enter fullscreen mode Exit fullscreen mode

We should be able to reach both apps at localhost:3000 and localhost:3001 respectively as shown below

Service A

Service B

Step 6: Set Up Nginx!
In the root of our nestjs-nginx project, we create an nginx folder and inside we create a nginx.conf file and we set it up like so.

events {}

http {
  # Define an "upstream" for each of your services.
  # Docker Compose lets you use the service name ('app-a', 'app-b') as a hostname.
  upstream app_a_server {
    server app-a:3000;
  }

  upstream app_b_server {
    server app-b:3001;
  }

  server {
    listen 80;

    # Route requests starting with /api/a to app-a
    location /app-a/ {
      # The rewrite rule strips the /api/a prefix before forwarding
      # e.g., /api/a/users becomes /users
      rewrite /app-a/(.*) /$1 break;
      proxy_pass http://app_a_server;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # Route requests starting with /api/b to app-b
    location /app-b/ {
      rewrite /app-b/(.*) /$1 break;
      proxy_pass http://app_b_server;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Then we update the docker-compose.yaml to spin up an nginx container and reference this file like so

services:
  app-a:
    build:
      context: .
      dockerfile: apps/app-a/Dockerfile
    ports:
      - '3000:3000'
    restart: always

  app-b:
    build:
      context: .
      dockerfile: apps/app-b/Dockerfile
    ports:
      - '3001:3001'
    restart: always

  nginx:
    image: nginx:latest
    container_name: nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    ports:
      - '80:80'
    depends_on:
      - app-a
      - app-b
Enter fullscreen mode Exit fullscreen mode

Step 7: Rebuild Images
Now we rebuild the images and start up the containers like so

docker compose down -v

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

Step 8: Test Nginx Integration
We can proceed to reach the apps through Nginx again through http://localhost/app-a and http://localhost/app-b respectively

Service A

Service B

Conclusion
In this guide, we walked through how to use NGINX as a reverse proxy to route traffic between multiple NestJS services. You’ve learned how to structure your Docker containers, configure the nginx.conf, and test that each service is reachable through clean, unified endpoints. With this setup in place, you can now add more services or introduce HTTPS with minimal changes — making your microservice architecture production-ready.

Top comments (0)