Deploying Node.js
Node in production is straightforward once you stop running it the way you run it on your laptop.
Run it as a service
Not npm start in a terminal, not screen. A systemd unit
gives you restart on failure, start at boot, journald logging and sandboxing —
see running your application as a service.
pm2 does some of the same things, and then you have a process manager being managed by a process manager. If you already use it, run pm2 itself under systemd. For new deployments, systemd alone is simpler.
In the unit: Environment=NODE_ENV=production, an unprivileged
User=, and ExecStart=/usr/bin/node /srv/app/server.js with
an absolute path to the binary.
Put a proxy in front
Let nginx terminate TLS, serve static files and buffer slow clients; let Node do
your logic. Listen on 127.0.0.1:3000 and never on a public address.
See nginx pitfalls for the proxy headers, and add
WebSocket support if you need it:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Use all the cores
Node is single-threaded. On a machine with sixteen cores, one process uses one of them. Two options:
- Several systemd instances on different ports, balanced by nginx upstream. Explicit, and each can be restarted independently.
- The cluster module, which forks workers that share a socket. Fewer moving parts, and a crash in the primary takes everything.
Either way, do not run more workers than cores; you add context switching, not throughput. See choosing a CPU.
Memory
The V8 heap has a default ceiling that is unrelated to how much memory your machine has. On a large box, raise it deliberately:
ExecStart=/usr/bin/node --max-old-space-size=4096 /srv/app/server.js
And bound it from the other side with MemoryMax=6G in the unit, so one
leaking process cannot take the machine down with it.
Restarts without dropping requests
Handle SIGTERM: stop accepting new connections, finish the ones in
flight, then exit. Give systemd time to let that happen with
TimeoutStopSec=30. Without it, every deploy drops whatever was in
progress.
Practical habits
npm ci --omit=devin deployment, so the lockfile decides and dev dependencies stay off the server- Log to stdout and let journald handle it — see log management
- Never commit secrets; read them from the environment, see managing secrets
- Pin the Node major version and upgrade deliberately; LTS releases only
npm auditin your pipeline, and act on the results
Still stuck? Mail support@novogara.com — an engineer answers, at any hour. Back to the knowledge base