DEV Community

Nainish Rai
Nainish Rai

Posted on

How I Deployed a NestJS App on a VPS (and AWS) Without Losing My Mind

Hey, devs!

Sharing my experience at a startup (let’s call it “VidStack”), I got tasked with deploying a NestJS microservices app on a Ubuntu VPS and AWS. Sounds intimidating? Yeah, it was a bit. But I got through it without setting any servers on fire (mostly). Here’s how I pulled it off—step-by-step, with honest explanations and zero fluff. If you’re a dev figuring out deployment for the first time, this one’s for you. 🍜


🧱 The Setup: What We Were Dealing With

Our app had multiple services (aka microservices). Picture this: 6 microservices yelling at each other over a walkie-talkie (NATS), with an API Gateway playing babysitter. Here’s the crew:

Services:

  • API Gateway: The babysitter keeping things in order.
  • User Management: Handles signup/login stuff.
  • Video Management: Manages video uploads.
  • Clips Management: Chops videos into shareable clips.
  • Clip Actions: Tracks likes/comments on clips.
  • Action Management: Logs all interactions.

All built with NestJS, a slick Node.js framework for backend development.


🧪 Local Dev Setup

Before we go live, we need to test locally. Here’s how I got it running.

🛠 Step 1: Clone the Code

git clone <repo-url>
cd project-spaghetti
Enter fullscreen mode Exit fullscreen mode

What’s this?

We’re grabbing the project code from GitHub and moving into its folder. Think of it as downloading a recipe before cooking. I think I am overexplaining but nvm :).


📦 Step 2: Install Dependencies

npm install
Enter fullscreen mode Exit fullscreen mode

This pulls in all the packages (like express, dotenv, etc.) listed in package.json.


🧪 Step 3: Set Up .env Files

cp .env.example .env
cp api-gateway/.env.example api-gateway/.env
Enter fullscreen mode Exit fullscreen mode

What are .env files?

They store sensitive stuff like database passwords. Never commit them to GitHub unless you want hackers sending you thank-you notes.


🐘 Step 4: Start PostgreSQL with Docker

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

Docker: A tool that runs apps in lightweight containers (like tiny virtual machines).

PostgreSQL: Our database for storing data.

Why Docker? Installing Postgres manually is a hassle. Docker makes it as easy as reheating leftovers.


🗃 Step 5: Run Database Migrations

npx prisma migrate deploy
Enter fullscreen mode Exit fullscreen mode

Prisma: A tool that makes talking to databases feel like chatting with a friend.

Migrations: These create database tables based on your schema (like setting up shelves for your data).


🏃 Step 6: Start the Services

Open a terminal for each service and run:

cd api-gateway && npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Repeat for each microservice. Your laptop will sound like a jet engine, but it works.


🚀 Deploying to a VPS

Now we move from local to real-world. I used a VPS on DigitalOcean (you could use AWS EC2 too).


🌐 Step 1: VPS Initial Setup

sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git docker.io docker-compose nginx
Enter fullscreen mode Exit fullscreen mode

What’s happening?

  • apt update/upgrade: Updates the server’s software.
  • curl/git: Tools for downloading and version control.
  • docker/docker-compose: For running our database in containers.
  • nginx: A web server to handle incoming traffic (we’ll use it soon).

Install Node.js & PM2

curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
Enter fullscreen mode Exit fullscreen mode
  • Node.js: Runs our NestJS app.
  • PM2: A process manager that keeps our services running, even if the server restarts.

🏗 Step 2: Clone Project on VPS

mkdir -p /opt/project-spaghetti
cd /opt/project-spaghetti
git clone <repo-url> .
npm install
npm run install:all
Enter fullscreen mode Exit fullscreen mode

This sets up the app folder and installs all dependencies.


⚙️ Step 3: Configure .env Files

echo "DATABASE_URL='postgresql://user:pass@localhost:5432/db'" > .env
Enter fullscreen mode Exit fullscreen mode

Set up .env files again, but with production values (e.g., real database credentials).


🐘 Step 4: Run Database and Migrations

docker-compose up -d
npx prisma migrate deploy
Enter fullscreen mode Exit fullscreen mode

This starts Postgres in Docker and builds the database tables.


🧱 Step 5: Build the Code

npm run build:all
Enter fullscreen mode Exit fullscreen mode

This converts TypeScript code to JavaScript, which Node.js can run.


🧙‍♂️ Step 6: Keep Services Alive with PM2

pm2 start ecosystem.config.json
pm2 save
Enter fullscreen mode Exit fullscreen mode
  • ecosystem.config.json: A file listing all microservices and how to run them.
  • pm2 start: Launches all services.
  • pm2 save: Ensures they restart if the server reboots.

🌍 Step 7: Set Up Nginx (The Traffic Cop)

sudo nano /etc/nginx/sites-available/project-spaghetti
Enter fullscreen mode Exit fullscreen mode

Paste this:

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://localhost: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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nginx: A web server that directs internet traffic to your app.

proxy_pass: Sends traffic from port 80 (standard web port) to port 3000 (where our app lives).

Enable the site:

sudo ln -s /etc/nginx/sites-available/project-spaghetti /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

🔒 Step 8: Add Free SSL with Let’s Encrypt

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d your-domain.com
Enter fullscreen mode Exit fullscreen mode

This gives your site HTTPS (the browser lock icon). It’s free, and Google loves it.


🤖 Automating with GitHub Actions (CI/CD)

Manually SSH-ing into the server for updates? Nah, let’s automate it.

Set Up SSH for GitHub

ssh-keygen -t ed25519 -C "github-deploy"
Enter fullscreen mode Exit fullscreen mode

Add the public key to your VPS:

echo "<your-public-key>" >> ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Add the private key and server details to GitHub Secrets:

  • SSH_PRIVATE_KEY
  • SERVER_HOST
  • SERVER_USER

Now, every push to the main branch will:

  1. SSH into the server.
  2. Pull the latest code.
  3. Rebuild and restart services.
  4. Let you sip coffee. ☕

🩺 Monitoring & Debugging

PM2 Commands:

pm2 status
pm2 logs
pm2 restart all
Enter fullscreen mode Exit fullscreen mode

Database Backups:

pg_dump -U user db > backup.sql
psql -U user db < backup.sql
Enter fullscreen mode Exit fullscreen mode

Renew SSL:

sudo certbot renew
Enter fullscreen mode Exit fullscreen mode

⚠️ Bonus Tips (Lessons from My Screw-Ups)

  • Don’t run migrations before the database is up. You’ll cry.
  • Check disk space. My server died once because it was stuffed with logs 😵‍💫.
  • Use a firewall (UFW) to allow only ports 22 (SSH), 80, and 443.

🧠 Final Thoughts

Deploying a NestJS microservices app isn’t trivial, but it’s doable if you break it down:

  • Docker runs Postgres.
  • Prisma sets up the database.
  • PM2 keeps services running.
  • Nginx handles traffic.
  • GitHub Actions automates updates.

Take it one step at a time, and you’ll be fine. If you’re staring at a blank terminal wondering where to start, I hope this helps. Share it with a dev friend who’s in the same boat! 😅


💭 My Personal Experience & Takeaways

When I first saw the task—"Deploy NestJS microservices to a VPS"—I legit opened 20 tabs, panicked, and drank way too much diet coke.

Challenges I Faced:

  • Configuring PM2 to handle multiple services wasn’t easy. Each service needed its own setup, and I kept mixing up ports.
  • Forgot to set up a .env file once. Debugged for 2 hours. Rookie mistake.
  • Nginx gave me a 502 Bad Gateway error. Turns out, I forgot to restart it after tweaking the config 🤦‍♂️.
  • Debugging Docker/Postgres issues was brutal without logs. Lesson: always check docker logs.

What I Learned:

  • Break problems down. One service at a time. One port at a time.
  • Deployment is more about configuration than code.
  • Tools like Docker, PM2, and Nginx sound scary—but they’re not once you use them.
  • CI/CD is a blessing. Automate everything you can.

This task forced me out of my comfort zone, but I came out way more confident with DevOps, deployment, and infrastructure. If you’re reading this and thinking “I can’t do it”—you totally can. Just Google, break it down, and keep pushing.

👋 Final Words

Thanks for sticking around! Hope this helped demystify the world of NestJS deployment for you. If you want more dev logs like this or want to see the GitHub Actions CI/CD setup I used, feel free to reach out or drop a DM!

Happy deploying! ⚙️🚀

Nainish Rai

Top comments (0)