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
2. Convert App to Monorepo
We'll do this by generating a new app within our current app
nest generate app app-b
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"
}
}
}
}
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!';
}
}
and
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello from service B!';
}
}
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
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"]
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"]
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
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
We should be able to reach both apps at localhost:3000 and localhost:3001 respectively as shown below
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;
}
}
}
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
Step 7: Rebuild Images
Now we rebuild the images and start up the containers like so
docker compose down -v
docker compose up --build
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
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)