Skip to content
Node.js nd deployment 5 min read

Deploying Node.js Applications

Getting a Node.js app running on your laptop is easy; running it reliably in production for real users is a different discipline. Deployment is the set of decisions about where your process runs, how it gets built and started, and how it reads configuration and secrets in each environment. This page surveys the four main hosting models — virtual private servers, platform-as-a-service, serverless, and containers/Kubernetes — and the build-then-run pattern that underpins all of them.

Choosing a hosting model

There is no single “right” way to host Node.js. The best choice depends on how much infrastructure you want to own, how spiky your traffic is, and how your team prefers to ship. The table below summarizes the trade-offs.

ModelYou manageBest forScalingEffort
VPS (e.g. DigitalOcean, EC2)OS, runtime, process managerFull control, predictable costManual / verticalHigh
PaaS (Render, Railway, Heroku)App + config onlySmall teams, fast iterationAutomatic, horizontalLow
Serverless (Lambda, Cloud Functions)Function code onlyBursty / event-driven workloadsPer-request, to zeroLow–medium
Containers / KubernetesImage + manifestsPolyglot fleets, scale, portabilityDeclarative, automatedMedium–high

A useful rule of thumb: start on a PaaS, move to containers when you need portability or custom infrastructure, and only adopt Kubernetes when you genuinely have many services to orchestrate. Don’t pay the operational tax of Kubernetes for a single web app.

The build-then-run pattern

Almost every deployment target expects the same two-phase lifecycle: a build step that turns your source into a runnable artifact, and a run step that starts the process. Keeping these separate makes builds reproducible and lets you bake a single artifact once and promote it through staging to production.

A typical package.json encodes both phases as scripts:

{
  "type": "module",
  "engines": { "node": ">=22.0.0" },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "start": "node dist/server.js"
  }
}

The engines field pins the Node.js version so the platform installs a compatible LTS release (use 20 or 22). The build compiles TypeScript (or bundles assets); the run step executes the compiled output. For a plain JavaScript project the build can be a no-op and start points straight at server.js.

Production installs should skip dev-only tooling and use the lockfile for deterministic dependencies:

npm ci --omit=dev
npm run build
npm start

Output:

> [email protected] start
> node dist/server.js

Server listening on http://0.0.0.0:8080 (production)

Bind to the host and port the platform provides rather than hard-coding them. Cloud hosts inject a PORT and expect you to listen on 0.0.0.0:

import { createServer } from "node:http";

const port = Number(process.env.PORT) || 3000;

const server = createServer((req, res) => {
  res.writeHead(200, { "content-type": "application/json" });
  res.end(JSON.stringify({ status: "ok" }));
});

server.listen(port, "0.0.0.0", () => {
  console.log(`Server listening on http://0.0.0.0:${port}`);
});

Environment configuration

The same artifact must behave differently per environment without code changes. The twelve-factor approach stores all environment-specific config in environment variables, read through process.env. Node.js 20.6+ can load a local .env file natively, while real platforms inject variables directly:

node --env-file=.env dist/server.js

Always read NODE_ENV and set it to production on your servers — this flips frameworks like Express into faster, less verbose modes and disables development-only checks:

const config = {
  env: process.env.NODE_ENV ?? "development",
  port: Number(process.env.PORT) || 3000,
  databaseUrl: process.env.DATABASE_URL,
};

if (config.env === "production" && !config.databaseUrl) {
  console.error("DATABASE_URL is required in production");
  process.exit(1);
}

Validate required variables at startup and exit fast on a missing secret, rather than crashing mid-request. Never commit secrets — supply them through the platform’s secret store or environment settings.

VPS and process managers

On a bare VPS you own everything above the kernel. After provisioning Node.js, you need a process manager to keep the app alive across crashes and reboots, run multiple workers, and handle log rotation. PM2 is the most common choice:

npm install -g pm2
pm2 start dist/server.js --name api -i max
pm2 startup   # generate the boot script
pm2 save      # persist the current process list

The -i max flag forks one worker per CPU core using Node’s cluster module, so a single host uses all available cores. Put Nginx in front as a reverse proxy to terminate TLS and serve static assets.

PaaS, serverless, and containers

PaaS platforms (Render, Railway, Heroku) detect your package.json, run npm ci && npm run build, then npm start. You push a Git branch and the platform builds, deploys, and scales — no servers to patch. Configure env vars in the dashboard and health checks against a /health route.

Serverless uploads individual handler functions that the provider invokes on demand and scales to zero when idle. You pay only for execution time, but watch for cold starts and per-request timeouts on long jobs.

Containers package your app and its exact runtime into a portable image. A Dockerized app runs identically on a laptop, a VPS, or a Kubernetes cluster, which is why containers have become the common currency of modern deployment. Kubernetes then schedules those images, restarts failed pods, and rolls out new versions declaratively.

Best Practices

  • Pin the Node.js version with engines and deploy on an active LTS (20 or 22).
  • Keep build and run as separate, scripted phases so one artifact promotes cleanly across environments.
  • Install with npm ci --omit=dev for reproducible, lean production dependencies.
  • Drive all environment differences through process.env; set NODE_ENV=production and validate required vars at boot.
  • Bind to 0.0.0.0 and the platform-provided PORT instead of hard-coding networking.
  • Add a lightweight /health endpoint so platforms and load balancers can detect a healthy instance.
  • Run behind a process manager or orchestrator that restarts crashed processes and uses every CPU core.
Last updated June 14, 2026
Was this helpful?