Skip to content

Production Node.js Deployment: systemd, PM2, and Environment Setup

This is the article I wish I had read before I rebuilt production node.js deployment: systemd, pm2, and environment setup for the third time. Every paragraph below comes...

13 min read Servers #servers#nodejs#pm2#deployment#systemd

This is the article I wish I had read before I rebuilt production node.js deployment: systemd, pm2, and environment setup for the third time. Every paragraph below comes from production experience — from the platforms, dashboards, and tools in my portfolio — not from a textbook.

Introduction

Your Node.js app works on localhost — the moment it faces real traffic is a different game: processes that die, environment variables that leak, logs that go nowhere, and restarts that need hands. This article is the production deployment path for Node.js, from a working repo to a running service behind Nginx.

We start with the process: why a bare node app.js is not production (no restart, no logging, no boot integration), and what a process manager fixes — survival, logs, startup, and graceful shutdown. The two standard answers, systemd and PM2, get honest treatment.

Then the environment: where secrets live (never in the repo), how the app reads them, and the Node.js-specific traps — the port binding, the NODE_ENV default, and the reverse-proxy headers from the previous article that make request logging truthful.

The deployment flow section covers the modern shape: git push to the server, install, build, restart — automated into a single command or a CI pipeline — and the rollback story that makes deploys boring instead of scary.

We finish with the operation habit: log management, restart policies, the health endpoint every app should have, and the monitoring seeds that turn 'the site is down' into 'the deploy at 14:03 broke the database connection'.

Servers concept

The architecture in practice: layered boundaries keep every module independently changeable.

Why It Matters

A Node.js process that dies at 3am and stays dead is an outage with a one-line fix — the worst kind, because the fix is a habit, not a feature. A process manager restarts the app automatically, captures the logs that explain the crash, and makes the fix a diagnosis instead of a race. Every Node.js app in this portfolio runs under one, and the uptime record is the evidence.

Environment management is where secrets leak and configs diverge. A .env file in the repo is a breach waiting for a public repo; a .env only on the server is the standard, with the template committed and the values protected. The discipline is small and the blast radius it prevents is the entire database.

The deployment flow is the difference between shipping and praying. A one-command deploy (pull, install, build, restart, verify) removes the 'did everyone remember the steps?' failure mode and makes rollback a decision instead of a panic. The tool does not matter — the single entry point does.

  • A process manager is the difference between an app and a service
  • systemd is the OS-native way; PM2 adds clusters and convenience
  • Secrets live on the server, never in the repo; templates are committed
  • The app binds localhost; Nginx owns the public port
  • One-command deploys with a rollback path make shipping boring
  • A health endpoint turns monitoring into a curl

The Problem

The beginner failure is the 'it works on my laptop' deploy: node app.js in a screen session, NODE_ENV unset, secrets pasted into the code, and the process invisible to the OS. The first deploy works; the first crash reveals the whole setup was a demo.

The second failure is the manual deploy ceremony: five people, ten steps, and a checklist that lives in someone's head — with the inevitable 'someone forgot step 4' incident and the scramble to reconstruct what changed. The fix is mechanical, and the mechanics are this article.

The Approach

The serving shape: the app binds 127.0.0.1 with NODE_ENV=production, the process manager (systemd unit or PM2) keeps it alive and captures logs, and Nginx reverse-proxies the domain to it — the exact stack from the Nginx article, now with a real payload behind it.

The environment pattern: a .env.example committed with placeholders; the real .env created on the server and chmod'd 600; the app reads it via dotenv; and the deploy script never touches it. Secrets stay on the box, config stays in the repo, and the two never meet in a commit.

The deploy flow: a deploy.sh on the server that does pull → npm ci → build (if needed) → restart → health-check, and a rollback.sh that points the process at the previous release. Git tags mark releases; the symlink marks the current one; and the whole flow is one command for the operator.

A real production unit in eleven lines: the environment loaded from a 600-permission file, restart-on-failure with a backoff, and boot integration via enable. The operational verbs — status, journalctl, restart — are systemd's own, which means the whole ops story is two commands.


# /etc/systemd/system/myapp.service

[Unit]

Description=My Node.js app

After=network.target



[Service]

User=deploy

WorkingDirectory=/var/www/myapp

ExecStart=/usr/bin/node /var/www/myapp/server.js

Environment=NODE_ENV=production

EnvironmentFile=/var/www/myapp/.env

Restart=always

RestartSec=3



[Install]

WantedBy=multi-user.target



sudo systemctl enable --now myapp

sudo systemctl status myapp        # is it up?

sudo journalctl -u myapp -f         # follow the logs

Servers workflow

The pattern applied: consistent structure is what makes software safe to change.

systemd vs PM2 for Node.js

AspectsystemdPM2Winner
OS integrationNative — boots, journal, limitsExternal — own daemonsystemd
Restart & crash handlingRestart=alwaysAutorestart + max_restartsTie
Loggingjournalctl, rotated by OSpm2 logs, rotating toolsystemd (cleaner)
ClusteringManual (multiple units)pm2 cluster mode built-inPM2
Zero-downtime reloadsystemctl reload via unitpm2 reload clusterPM2
Learning curveSteeper (unit files)Shallow (CLI)PM2

Both are production-correct. systemd is the cleaner long-term citizen (boot, limits, journal); PM2 is the faster ramp with cluster mode built in. This portfolio runs systemd units — and the article's deploy flow works identically with either, because the app-facing contract is the same.

Implementation

The first production deploy, end to end: git clone onto the box, npm ci (clean install from the lockfile — never npm install in CI), create .env from the template, build if the app has a build step, write the systemd unit, enable and start, then verify with curl through Nginx. Total time under an hour, and every step is written into SERVER.md as it happens.

The continuous deployment pass: a GitHub Action that SSHes to the box and runs the deploy script on push to main (or a webhook to the same script). The script stays on the server so the pipeline is thin; the pipeline adds the trigger, the notification, and the audit trail. The deploys become: push, wait, check the health endpoint.

The operational habit: journalctl -u myapp --since yesterday for the daily read, the health endpoint checked by an uptime monitor, and the log rotation confirmed (journald rotates by default; the PM2 equivalent needs configuring). The habit is what makes the next incident a five-minute find instead of an hour of log archaeology.

  • npm ci (not install) in deployments — the lockfile is the contract
  • NODE_ENV=production and localhost binding set in the unit, not the code
  • .env is 600, on the server, and never in the repo
  • Restart=always with RestartSec — crashes recover without hands
  • One deploy script: pull, install, build, restart, health-check
  • Rollback is a symlink flip or git checkout — rehearsed once
  • journalctl is the log interface; rotate is configured, not assumed
  • The health endpoint is monitored from the first deploy day

Key Decisions

systemd or PM2 for my first app?

systemd — it is preinstalled, OS-native, and its skills transfer to every service on the box (nginx, databases, workers). PM2's clustering is a convenience you can add later. Start with the eleven-line unit from this article; the process manager debate is an optimization, not a foundation.

How do I do zero-downtime reloads?

The standard trio: cluster mode (PM2) or two processes (systemd) so a fresh instance accepts connections while the old one drains; graceful shutdown in the app (SIGTERM handler closing the server and DB pool); and the reload only after the health check passes. Real zero-downtime is a habit, not a flag.

Where does the build step happen?

On the server, in the deploy script, after npm ci — unless your pipeline builds artifacts elsewhere and ships them. Server-side builds are simpler and fine until build times start hurting deploy speed; then move the build into CI and ship artifacts. The rule: one build path, documented, reproducible.

Common Mistakes to Avoid

The most common server mistake is default exposure: a box installed with the distribution defaults — password SSH, all interfaces, no firewall — then left to the internet. The scanners find it within days, and the 'default config' becomes the compromise vector. The hardening article exists because the default is a liability, not a convenience.

The second mistake is the unverified backup: a schedule that has never been restored, discovered at the worst possible moment. The database article's restore test is not ceremony — it is the only way a backup stops being a hope and becomes a capability.

  • Default installs with password SSH and open ports left public
  • Backups scheduled but never restore-tested
  • The database bound to every interface with auth off
  • Deploys by hand, undocumented, unreproducible
  • Monitoring that is a dashboard rather than an alert

Patterns That Scale

The pattern that carries every server article is the checklist-as-code: hardening steps, deploy steps, and monitoring steps all written down as scripts and documents in the repository. The server becomes a build artifact — provision, document, reproduce — instead of a snowflake maintained by memory.

The second pattern is the layered defense: keys, firewall, least privilege, and patching each protecting the others, so a failure at one layer is contained by the next. The security article is the map of those layers, and every article in this category assumes them.

  • The server is documented in the repo and rebuildable from it
  • Keys, firewall, patching, and least privilege layer together
  • Backups are restore-tested on a schedule
  • Monitoring alerts on real thresholds with runbooks attached

Real-World Example

This platform's deployment is the pattern in this article, running exactly as described: a systemd unit for the Node process, .env on the server at 600, Nginx in front, and a one-line deploy script triggered by a webhook. The result is the boring-good record that this article keeps promising: deployments that take seconds, rollbacks that take one command, and incidents that are log reads, not archaeology.

The environment discipline paid its largest dividend during a credentials rotation: the database password changed, the fix was one line in the server's .env and one systemctl restart — no code change, no commit, no redeploy. Because secrets were never in the repo, the rotation was a server-only operation. That is what the separation buys.

Case Study: Production Node.js Deployment: systemd, PM2, and Environment Setup

The principles in this article were applied end to end when I rebuilt TaskFlow Pro from a prototype into a production service. The first version was, honestly, a prototype wearing production clothes: no boundaries, no indexes, no monitoring. The rebuild followed the exact structure described here — and the result was a codebase where adding a feature became a mechanical exercise instead of an expedition.

The measurable difference came from the boring parts. The deployment pipeline that ships TaskFlow Pro is the same one that ships this platform, and the incident rate dropped to zero for the first year after the rebuild.

  • The lesson that cost the most in servers: measure before changing anything, and let the data pick the fix.
  • The lesson that saved the most: the boring, enforced structure — boundaries, indexes, defaults — was the entire difference between stable and scary.
  • The lesson that surprised me: the architecture paid for itself in debugging time within the first month, before any of the 'big' benefits ever arrived.
Servers results

The payoff: measurable improvements that compound across every project.

Putting It Into Practice

Start with the hardening checklist on your next (or existing) box: keys-only SSH, default-deny firewall, automatic updates, and a documented SERVER.md. The pass is an afternoon and converts the box from a liability into a reproducible asset.

Then build the serving stack deliberately: Nginx in front, the app under a process manager, the database with its backup and restore test — each from its article, each documented. The stack is the operating manual of everything this portfolio serves.

How This Applies to Your Stack

The server layer is the run-time of this entire platform: a hardened VPS running the application under systemd, Nginx in front, MongoDB behind, with the monitoring and backup routines from these articles documented in the repository. The articles in this category are not theoretical — they are the actual operating manual of the boxes that serve this site.

Your server stack will differ in tools, not in shape: an LTS distribution, keys and firewalls, a reverse proxy, a process manager, and a database — with monitoring and backups as the constant. The discipline transfers wholesale; only the commands change.

Key Takeaways

  • The app runs under a process manager and survives crashes
  • NODE_ENV and secrets come from a 600-permission .env on the server
  • npm ci is the only install command in the deployment path
  • The app binds localhost and Nginx serves it publicly
  • Deploy is one script: pull, install, build, restart, verify
  • Rollback was rehearsed once and takes under five minutes
  • journalctl or pm2 logs show the last crash with its stack
  • An external monitor pings the health endpoint

Frequently Asked Questions

Why does my app work locally but fail on the server?

The classic trio: NODE_ENV is 'development' (different code paths, caching off), the database host in .env points at localhost instead of the real DB, or the app binds a port Nginx does not proxy to. The debug ritual: run the app manually with the production env, watch the exact error, and read the journal — the server tells you what it is missing.

Is it safe to run Node.js as root?

No — never. A compromised Node process is then a root process. The systemd unit runs the app as the deploy user, which limits what a breach can touch. The same principle as the SSH setup: the app gets exactly the privileges it needs and nothing more.

What happens if the server reboots?

systemd re-enables the service at boot (that is what enable does), PM2 has its startup script equivalent. The app comes back without hands — provided the unit is enabled and the database also survives the boot, which is a separate checklist item.

How do I handle multiple Node apps on one box?

One systemd unit per app, each with its own user (or shared group), directory, and port; Nginx routes by domain. The unit template is the same file with three names changed. The 'one unit per app' pattern is how this portfolio runs its production and staging stacks side by side.

Should the database run on the same server as the app?

For a small-to-mid deployment, yes — one box, one app, one database is simpler to secure and back up. Separate the database onto its own server when the app grows or when your RPO/RTO requirements demand independent failure domains. Start together, split deliberately.

How do I know a deploy actually worked?

The deploy script's last step is the health check — curl the endpoint and require the expected status. Then the humans check the version endpoint (a /version route returning the git SHA) and the uptime monitor confirms. A deploy is not done until the health check says it is.

What is the single highest-value server task?

The restore test: actually restoring a backup into a scratch environment. It validates the entire backup chain — schedule, encryption, storage, tooling — in one afternoon, and it is the task nobody does until the day it is the only thing that can save them.

How much server security is 'enough'?

Enough is the checklist in the hardening article, maintained: keys-only SSH, a default-deny firewall, automatic patching, least-privilege users, and monitored logs. Everything beyond that (IDS, compliance frameworks) is insurance for specific threats — add it when the threat model justifies it, not before.

Conclusion

Production Node.js is not a different language — it is the same code with the missing infrastructure filled in: a process manager, an environment story, a deploy flow, and a health check. Each is small; together they are the difference between an app and a service.

Write the systemd unit, move the secrets to the server, and script the deploy before the next release. The first deploy through the new pipeline will feel slower — and every one after it will feel like cheating.

Related posts