Deploying Your NestJS App to AWS (Without Accidentally Summoning DevOps Demons) 🌩️
So you shipped to Vercel like a champ. Now someone says, “Put it on AWS.” Don’t panic. Breathe. Sip chai. This guide walks you through production-grade AWS deployments with jokes, files, and commands you can paste with confidence.
We’ll cover three popular paths:
- EC2 + Nginx + PM2 (classic, reliable, works everywhere)
- Elastic Beanstalk (managed Node hosting, fewer knobs to turn)
- Lambda + API Gateway (Serverless) (scale-to-zero wizardry)
Pick your vibe. Or collect them all like Pokémon.
🧭 Quick Decision Guide
- EC2: You want a normal server you control. SSH, Nginx, PM2.
- Elastic Beanstalk: “AWS, please parent me.” Easier ops, rolling updates.
- Lambda: Swagger + bills of ₹0 when idle. Cold starts are the plot twist.
🅰️ Path A — EC2 + Nginx + PM2 (The “I’m the boss” setup)
🍱 What you’ll ship
- Either full source and build on server, or
- dist-only (compiled) +
package.json
and run it.
Tip: For AWS/EC2, your
main.ts
should listen on a port. If you previously removedlisten()
for serverless, add a separate bootstrap file for EC2 (below).
1) Create an EC2 instance
- Image: Ubuntu 22.04 LTS
- Size:
t3.micro
(free-tier friendly; upgrade if needed) - Open inbound Security Group ports: 22 (SSH), 80 (HTTP), 443 (HTTPS)
Associate an Elastic IP if you want a stable public IP.
2) SSH into the server
ssh -i /path/to/your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP
3) Install system deps, Node.js, PM2, Nginx
sudo apt update
sudo apt install -y nginx git curl
# install nvm + Node LTS
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.nvm/nvm.sh
nvm install --lts
node -v
npm -v
# process manager
npm i -g pm2
4) Put your app on the server
Option A: Build locally & upload dist-only
Locally:
npm ci
npm run build
# Now ship: dist/, package.json, (and .env if you use it)
On server:
sudo mkdir -p /var/www/nestapp
sudo chown $USER:$USER /var/www/nestapp
cd /var/www/nestapp
# Upload files with scp/rsync or git pull your dist bundle repo
# Example if you zipped:
# unzip nestapp-dist.zip -d /var/www/nestapp
npm ci --omit=dev
Option B: Clone full source & build on server
cd /var/www
git clone https://github.com/you/nestapp.git
cd nestapp
npm ci
npm run build
5) Make sure you actually start an HTTP server
If your main.ts
is the normal HTTP bootstrap (good!):
// src/main.ts (standard HTTP bootstrap for EC2/VMs)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // optional
await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}
bootstrap();
If you previously removed
listen()
for serverless, create a separate filesrc/main-ec2.ts
with the above content and build/start that (dist/main-ec2.js
).
Update package.json
scripts accordingly:
{
"scripts": {
"start:prod": "node dist/main.js",
"start:ec2": "node dist/main-ec2.js"
}
}
6) Run with PM2 (keeps the app alive forever)
Create ecosystem.config.js
(TS is fine in repo, but PM2 reads JS at runtime):
module.exports = {
apps: [
{
name: "nestapp",
script: "dist/main.js",
instances: "max",
exec_mode: "cluster",
env: {
NODE_ENV: "production",
PORT: "3000"
}
}
]
}
Start + save:
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd
Logs:
pm2 logs
7) Reverse proxy with Nginx (for 80/443)
Create site config:
sudo nano /etc/nginx/sites-available/nestapp
Paste:
server {
listen 80;
server_name api.yourdomain.com; # or _ for IP
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable + reload:
sudo ln -s /etc/nginx/sites-available/nestapp /etc/nginx/sites-enabled/nestapp
sudo nginx -t
sudo systemctl reload nginx
8) Get free HTTPS (Let’s Encrypt)
Point your domain api.yourdomain.com
to the EC2 public IP (A record). Then:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d api.yourdomain.com
# follow prompts; auto-renews via systemd timer
9) Health-check like a pro (optional)
Add a simple health endpoint so ALBs/monitors stay happy:
// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import * as os from 'os';
@Controller()
export class AppController {
@Get('health')
health() {
return {
ok: true,
ts: new Date().toISOString(),
uptime: process.uptime(),
os: { platform: os.platform(), arch: os.arch(), release: os.release() }
};
}
}
10) Profit
- App:
https://api.yourdomain.com
- Logs:
pm2 logs
- Nginx errors:
sudo journalctl -u nginx -f
You are now the proud parent of a production server. Please feed it apt update
occasionally.
🅱️ Path B — Elastic Beanstalk (EB) (The “AWS, please help” path)
EB wraps EC2, Load Balancer, autoscaling, and deploys your Node app.
1) Install EB CLI
pip install --user awsebcli
# or use homebrew/choco—any method you like
2) Prepare app for EB
Ensure a start script that listens on the port provided by EB:
{
"scripts": {
"build": "nest build",
"start": "node dist/main.js",
"start:prod": "node dist/main.js"
}
}
main.ts
should listen(process.env.PORT || 8081)
(EB sets PORT
env).
3) Initialize and create environment
eb init -p node.js -r ap-south-1
eb create nest-prod-env --single
4) Deploy
npm run build
eb deploy
5) Set env vars
eb setenv NODE_ENV=production API_KEY=shhh
That’s it—EB gives you a load-balanced URL. Map your domain via Route 53 or any DNS provider.
🅲️ Path C — Lambda + API Gateway (Serverless)
(For when you want to pay ₹0 while nobody uses your app and scale to infinity when they do.)
1) Add serverless deps
npm i @vendia/serverless-express express
npm i -D serverless serverless-offline
We’ll use the Express adapter inside Nest and wrap it.
2) Create src/lambda.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ExpressAdapter } from '@nestjs/platform-express';
import express from 'express';
import serverlessExpress from '@vendia/serverless-express';
let cached: ReturnType<typeof serverlessExpress> | null = null;
async function bootstrap() {
const expressApp = express();
const nest = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
await nest.init();
return serverlessExpress({ app: expressApp });
}
export const handler = async (event: any, context: any) => {
if (!cached) {
cached = await bootstrap();
}
return cached(event, context);
};
3) serverless.yml
service: nest-on-lambda
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
region: ap-south-1
memorySize: 512
timeout: 15
environment:
NODE_ENV: production
functions:
api:
handler: dist/lambda.handler
events:
- httpApi:
path: /{proxy+}
method: ANY
plugins:
- serverless-offline
package:
patterns:
- '!**/*'
- 'dist/**'
- 'node_modules/**'
- 'package.json'
4) Build & Deploy
npm run build
npx serverless deploy
You’ll get an HTTP API URL like https://abc123.execute-api.ap-south-1.amazonaws.com
.
Bonus: Use API Gateway custom domain + ACM for a pretty URL.
🎯 Cheatsheet: What to change in main.ts
- EC2 / EB (server-based) → you must call
listen()
- Lambda (serverless) → do NOT call
listen()
; use Express adapter + handler
If you need both in one repo, create two entry files:
main-ec2.ts
(callslisten()
)lambda.ts
(no listen; exports handler)
🏁 Final Words
- EC2: You’re the captain now.
- Elastic Beanstalk: AWS holds your hand (nicely).
- Lambda: Cloud sorcery, perfect for spiky workloads.
Whichever path you choose, your NestJS app is now ready to glow on AWS like a Bollywood hero in a slow-mo entry shot. 🎬✨