Deployment
Running node server.js on your laptop is not production. Production needs a process manager or container, a reverse proxy with HTTPS, configuration from the environment, health checks and safe restarts.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Prepare a Node app for production
- Run it with PM2 or Docker
- Put Nginx in front with HTTPS
- Add health checks and graceful shutdown
Explanation#
Install exactly what is locked: npm ci --omit=dev. Set NODE_ENV=production (Express and libraries enable optimisations) and load config from environment variables.
A process manager restarts the app if it crashes and on server reboot: PM2 (pm2 start ecosystem.config.cjs, pm2 save, pm2 startup) or systemd. In containers, Docker/Kubernetes do this job.
Run Node behind a reverse proxy (Nginx, Caddy, a cloud load balancer) that terminates HTTPS, serves static files, compresses and forwards to localhost:3000. Set app.set("trust proxy", 1) so req.ip and secure cookies work.
Add GET /health (and /ready that checks the database) for load balancers and orchestrators.
Handle SIGTERM: stop accepting connections, finish in-flight requests, close the database pool, then exit — so deploys and scaling never cut off users.
Platforms: a VPS (Hostinger VPS, DigitalOcean) with PM2 + Nginx, containers on AWS ECS / Azure Container Apps, or PaaS like Azure App Service and Render.
Syntax#
npm ci --omit=dev
NODE_ENV=production node --env-file=.env server.js
# or with PM2
pm2 start server.js --name shop-api -i max
pm2 save && pm2 startup
Code example: A production-ready server entry point#
import express from 'express';
const config = {
port: Number(process.env.PORT ?? 3028),
env: process.env.NODE_ENV ?? 'development',
};
const db = { // stand-in for a MySQL pool
ok: true,
async ping() { return this.ok; },
async end() { console.log('database pool closed'); },
};
const app = express();
app.set('trust proxy', 1); // behind Nginx / a load balancer
app.get('/health', (req, res) => res.json({ status: 'ok', uptime: Math.round(process.uptime()) >= 0 }));
app.get('/ready', async (req, res) => ((await db.ping()) ? res.json({ ready: true }) : res.status(503).json({ ready: false })));
app.get('/api/slow', async (req, res) => { await new Promise(r => setTimeout(r, 300)); res.json({ done: true }); });
const server = app.listen(config.port, () => console.log(`listening on ${config.port} (${config.env})`));
let shuttingDown = false;
async function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
console.log(`${signal} received: refusing new connections, finishing current requests`);
server.close(async () => {
await db.end();
console.log('shutdown complete');
process.exit(0);
});
setTimeout(() => { console.error('forced exit after 10 s'); process.exit(1); }, 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Demo: health checks, then a deploy arrives while a slow request is running
const base = `http://localhost:${config.port}`;
console.log(await (await fetch(base + '/health')).text());
console.log(await (await fetch(base + '/ready')).text());
const inFlight = fetch(base + '/api/slow').then(r => r.text());
setTimeout(() => process.emit('SIGTERM'), 50);
console.log('in-flight request finished with:', await inFlight);
listening on 3028 (development)
{"status":"ok","uptime":true}
{"ready":true}
SIGTERM received: refusing new connections, finishing current requests
in-flight request finished with: {"done":true}
database pool closed
shutdown completeThis example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- Configuration comes from environment variables with safe defaults.
- trust proxy makes req.ip and secure cookies work behind a reverse proxy.
- /health says the process is alive; /ready also checks dependencies.
- On SIGTERM, server.close stops new connections but lets the slow request finish.
- After the last request, the database pool closes and the process exits; a 10-second timer forces exit if something hangs.
Real-world use case#
A Node.js API on a Hostinger VPS runs under PM2 in cluster mode behind Nginx with a Let’s Encrypt certificate. Deploys run git pull && npm ci && pm2 reload, and PM2’s graceful reload uses exactly this SIGTERM handling for zero downtime.
Common mistakes#
Run Node as an unprivileged user on a high port and let Nginx handle 80/443.
It stops when you log out or the app crashes. Use PM2, systemd or containers.
Every deploy drops in-flight requests and can leave transactions half-done.
Best practices#
- npm ci --omit=dev and NODE_ENV=production
- Process manager or container orchestrator
- Reverse proxy with HTTPS
- Health/readiness endpoints and graceful shutdown
Remember this#
npm ci → NODE_ENV=production → PM2/Docker → Nginx/HTTPS.
/health = alive. /ready = dependencies OK.
SIGTERM: close server → finish requests → close DB → exit.
Practice exercise#
Your task: Write an Nginx server block that redirects HTTP to HTTPS and proxies https://api.shop.example.com to a Node app on localhost:3000.
Need a hint?
Show solution
server {
listen 80;
server_name api.shop.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.shop.example.com;
ssl_certificate /etc/letsencrypt/live/api.shop.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.shop.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Mini quiz#
Which command installs production dependencies exactly as locked?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.