Deploying NestJS to a Linux VPS with PM2 and Nginx
Getting a NestJS application running in production involves more than copying files. Here's the exact process I use — from build to SSL.
Getting a NestJS application running reliably in production on a Linux VPS involves more than just copying files. You need process management, a reverse proxy, and SSL. This is the exact setup I've used across multiple production deployments.
Prerequisites
- A Linux VPS running Ubuntu 22.04 or later
- Node.js 20+ installed on the server
- A domain name pointing to your server's IP
- SSH access with sudo privileges
1. Build the application locally
Always build locally and transfer the compiled output — never build on the production server. This keeps the server lean and your build process reproducible.
npm run build
# Outputs compiled JS to dist/2. Transfer files to the server
Use rsync to transfer only what the server needs. Skip node_modules — install production dependencies directly on the server.
rsync -avz --exclude node_modules \
dist/ package.json package-lock.json \
user@your-server:/var/www/myapp/On the server, install only production dependencies:
cd /var/www/myapp
npm install --omit=dev3. Configure PM2
PM2 manages your Node.js process — it restarts the app on crash, persists across server reboots, and centralizes logs.
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp-api',
script: './dist/main.js',
instances: 1,
autorestart: true,
env: { NODE_ENV: 'production', PORT: 3001 },
}],
};pm2 start ecosystem.config.js
pm2 save
pm2 startup4. Configure Nginx as a reverse proxy
server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx5. SSL with Certbot
apt install certbot python3-certbot-nginx
certbot --nginx -d api.yourdomain.comFor zero-downtime deployments, use `pm2 reload myapp-api --update-env` instead of `pm2 restart`.