🚀
Hosting

Deploying NestJS to Production: Build, PM2, Nginx and SSL

11.11.2026
← All articles

Locally, NestJS is a pleasure to work with: npm run start:dev restarts on every change and errors show up right in the terminal. Production asks for something else entirely. The app must run around the clock, come back by itself after a reboot, keep secrets out of the code and talk to the outside world only over HTTPS. Below we deploy the order API of a small Tashkent delivery service to a VPS, step by step, and point out what tends to go wrong at each stage.

VPS or shared hosting

NestJS is a long-running Node.js process that listens on its own port, unlike PHP scripts that start fresh for every request. A VPS is the most reliable home for it: you choose the Node.js version, install PM2 and configure Nginx however you need. Whether Node.js apps can run on a shared plan depends on the plan and panel settings, so check that in the sayt.uz cabinet or with support before you start. In our example the API lives at api.example.uz, uses PostgreSQL and sends courier notifications through Telegram.

Preparing the code

The app should read its port from the environment, bind only to the loopback interface and shut down cleanly. enableShutdownHooks() lets database connections close properly when PM2 stops the process, and trust proxy makes the real client IP visible behind Nginx.

const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy', 1);
app.enableShutdownHooks();
await app.listen(Number(process.env.PORT ?? 3000), '127.0.0.1');

Database passwords, JWT keys and bot tokens belong in a server-side .env file read through @nestjs/config, never in the code or in Git. Run chmod 600 .env so only the app user can read it, and make sure Nginx never serves it.

Building and running on the server

Production runs compiled JavaScript from the dist folder, not TypeScript. Use npm ci rather than npm install so the exact versions from package-lock.json are installed and a surprise package update cannot break the build.

cd /var/www/api
git pull
npm ci
npm run build
npm prune --omit=dev
NODE_ENV=production node dist/main.js

If the app starts without errors and curl http://127.0.0.1:3000 answers, you can move on to the next step.

Migrations instead of synchronize

TypeORM's synchronize: true rewrites tables on its own and can destroy data, so disable it in production and apply migrations explicitly. Take a database backup before every migration: for a delivery service, a lost orders table is a business problem, not just a technical one.

npx prisma migrate deploy
# or, for TypeORM
npx typeorm migration:run -d dist/data-source.js

Keeping it alive with PM2

PM2 keeps the process in the background, restarts it after crashes and starts it on boot. Cluster mode spreads the app over several CPU cores, which helps under load.

npm install -g pm2
NODE_ENV=production pm2 start dist/main.js --name api -i 2
pm2 save
pm2 startup systemd

pm2 startup prints a sudo command that you must copy and run, otherwise the app will not come back after a reboot. In cluster mode each instance has its own memory, so move in-memory sessions or caches to a shared store such as Redis.

Nginx reverse proxy and HTTPS

Nginx accepts public traffic and forwards it to 127.0.0.1:3000, taking care of TLS, compression and request size limits while NestJS focuses on business logic.

server {
    listen 80;
    server_name api.example.uz;
    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_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Test with sudo nginx -t, reload with sudo systemctl reload nginx, then run sudo certbot --nginx -d api.example.uz to get a Let's Encrypt certificate with automatic renewal.

Checks after go-live

For updates, a simple routine works well: pull, npm ci, build, migrate and pm2 reload api. In cluster mode reload swaps instances one by one, so users notice no downtime during the release.

Related articles

🐧 Choosing a VPS Operating System: Ubuntu, Debian or AlmaLinux 🖥️ New VPS: what to do in the first 30 minutes ⏱️ max_execution_time: handling long-running PHP scripts 🥟 Bun Instead of Node.js? When It Works and How to Run It on a Server
🌐 Language
🇺🇿 O'zbek 🇺🇿 Ўзбек 🇷🇺 Русский 🇬🇧 English ✓