Skip to content

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:

  1. EC2 + Nginx + PM2 (classic, reliable, works everywhere)
  2. Elastic Beanstalk (managed Node hosting, fewer knobs to turn)
  3. 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 removed listen() 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

bash
ssh -i /path/to/your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP

3) Install system deps, Node.js, PM2, Nginx

bash
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:

bash
npm ci
npm run build
# Now ship: dist/, package.json, (and .env if you use it)

On server:

bash
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

bash
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!):

ts
// 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 file src/main-ec2.ts with the above content and build/start that (dist/main-ec2.js).

Update package.json scripts accordingly:

json
{
  "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):

js
module.exports = {
  apps: [
    {
      name: "nestapp",
      script: "dist/main.js",
      instances: "max",
      exec_mode: "cluster",
      env: {
        NODE_ENV: "production",
        PORT: "3000"
      }
    }
  ]
}

Start + save:

bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd

Logs:

bash
pm2 logs

7) Reverse proxy with Nginx (for 80/443)

Create site config:

bash
sudo nano /etc/nginx/sites-available/nestapp

Paste:

nginx
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:

bash
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:

bash
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:

ts
// 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

bash
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:

json
{
  "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

bash
eb init -p node.js -r ap-south-1
eb create nest-prod-env --single

4) Deploy

bash
npm run build
eb deploy

5) Set env vars

bash
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

bash
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

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

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

bash
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 (calls listen())
  • 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. 🎬✨

Built by noobs, for noobs, with love 💻❤️