The Node.js Production Readiness Checklist (2026)

"It works locally" and "it's ready for production" are two very different claims. The gap between them is a short list of things that are easy to forget and expensive to discover after a bad deploy: a missing start script, an unpinned Node version, a secret in the wrong file, no health endpoint for the load balancer to poll. This is that list — every category walked as prose you can read and act on. When you want the fast version, the Deployment Readiness Checker runs all of these checks against your own package.json in your browser.

Don't read — just check yours:

Check my package.json →

Runs 100% in your browser — nothing is uploaded.

1. Scripts: can the host actually start your app?

A platform-as-a-service boots your app by running npm start. If your package.json has no start script, there is nothing to run and the deploy fails at boot. The subtler failure is a start script that points at a development tool:

// ❌ nodemon/ts-node-dev are dev tools — they watch files and restart
"scripts": {
  "start": "nodemon src/server.js"
}

// ✅ start runs the built app the way production will
"scripts": {
  "start": "node dist/server.js",
  "dev":   "nodemon src/server.js",
  "build": "tsc",
  "test":  "vitest run",
  "lint":  "eslint ."
}

Keep start for production and dev for the watch-mode workflow. A real build script matters the moment your app compiles (TypeScript, a bundler, a framework build) — CI and your host both call it. A real test script (not the npm placeholder that just prints "Error: no test specified") lets CI stop a broken build before it ships, and a lint script catches error-prone patterns in review.

2. Configuration: pin the runtime and declare your intent

The most valuable configuration field is engines.node. Without it, your host runs your code on whatever Node version it happens to default to — and a mismatch between that and the version you tested is the classic "works on my machine, crashes in production" bug.

{
  "engines": { "node": ">=20 <21" },
  "packageManager": "npm@10.8.1",
  "type": "module",
  "private": true
}

If any of these are missing, the Deployment Readiness Checker points them out field-by-field with the fix.

3. Documentation: the metadata that saves you later

A missing license leaves usage rights ambiguous and makes npm warn on every install; add an SPDX id like "MIT", or "UNLICENSED" for a closed-source app. A repository field ties the package to its source for teammates and tooling, and a one-line description documents intent for whoever reads the manifest next — often future you. None of these break a deploy on their own, but together they are the difference between a manifest that explains itself and one that doesn't.

4. Security: keep secrets out of git

This is the category with the highest blast radius. Three rules cover most incidents:

  1. Real values live in .env, and .env is in .gitignore. A committed .env leaks every secret it holds to anyone with repo access — the single most common credential leak.
  2. Commit only .env.example with placeholders. It documents which variables exist without exposing their values. A real key in .env.example is a secret in version control.
  3. Inject real secrets at runtime through your platform's environment settings, never baked into an image or a committed file.

If a secret was ever committed, rotate it. Deleting it in a later commit does not help — git history keeps the old value forever. Before you paste logs into a bug report or an AI chat, strip secrets with the Log Redactor.

The exact rules for .env syntax — quoting, multiline values, comments, duplicate keys — are in The .env File Format guide. And remember that an application should set "private": true: an app is not a library, and without that flag a stray npm publish can push your whole app, secrets included, to the public registry.

5. Health checks: tell the platform when you're ready

If anything sits in front of your app — a load balancer, Kubernetes, a PaaS — it polls a health-check endpoint to decide whether to route traffic to your instance and whether to restart it. Without one, the platform can send users to a process that is technically up but unable to serve requests.

// Minimal Express health endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

A health endpoint should return 200 OK when the app can serve traffic and 503 Service Unavailable when a critical dependency (database, cache) is down, so the platform stops routing to it. The framework-specific versions — Express, NestJS with @nestjs/terminus, and Next.js — are covered with full code in Adding a Health Check Endpoint: Express, NestJS, and Next.js. The Deployment Readiness Checker detects your framework from package.json and reminds you which one to add.

6. Docker: small, pinned, non-root

If you ship a container, the readiness bar is: pin the base image to a specific tag (never :latest), use a multi-stage build so the toolchain doesn't ship with the app, run as a non-root USER, and add a .dockerignore so node_modules, .git and your local .env never enter the build context. Getting the layer order and base image right is its own topic — the Docker Image Optimizer analyses a pasted Dockerfile for exactly these issues, and the Docker optimization guide explains the reasoning.

7. CI: make the checks run automatically

Everything above is only reliable if it runs on every change. A minimal pipeline installs with npm ci (not npm install, so the lockfile is honored exactly), runs your test and lint scripts, and performs a build to catch compile errors before they reach a branch anyone deploys. If npm ci fails on a dependency conflict, the ERESOLVE guide covers the usual fixes.

Check your project against this whole list

Paste your package.json — and optionally your .env, .env.example and .gitignore — and get a scored, per-category report with framework-aware fixes. Nothing is uploaded.

Open the Deployment Readiness Checker →

The checklist, condensed

CategoryMust-have
ScriptsReal start (not nodemon); build/test/lint where relevant
Configurationengines.node pinned; type declared; packageManager set
Documentationlicense, repository, description
Security.env gitignored; only placeholders in .env.example; private: true
HealthA /health endpoint returning 200/503
DockerPinned base, multi-stage, non-root, .dockerignore
CInpm ci + test + lint + build on every change

Frequently Asked Questions

What is a production readiness checklist?

A production readiness checklist is the set of things a service needs before it can safely take real traffic: a reliable way to start, a pinned runtime, a health endpoint, sensible logging, secrets kept out of source control, and a repeatable build. Working through one turns "it runs on my machine" into "it runs, and keeps running, on the host".

What is the single most important thing to check before deploying a Node app?

A correct start script paired with a pinned engines.node range. The host boots your app with npm start, so a missing or dev-only start script (nodemon, ts-node-dev) means it never launches; and without engines.node the host may run a Node version you never tested, which is the classic cause of a deploy that crashes only in production.

Do I really need a health check endpoint?

If anything sits in front of your app — a load balancer, Kubernetes, a PaaS — yes. It polls a health endpoint to decide whether to send traffic to your instance and whether to restart it. Without one, the platform can route users to a process that is up but not actually able to serve requests.

Should engines.node be pinned in package.json?

Yes. Declaring engines.node (for example >=20 <21) tells your host and your teammates exactly which Node versions the app supports. It prevents a host from silently running your code on a newer or older Node than you tested, and many platforms will honor it when selecting a runtime.

How do I keep secrets out of production source control?

Keep real values in a .env that is listed in .gitignore, commit only a .env.example with placeholder values, and inject the real secrets at runtime through your platform's environment settings. Never put a real key in .env.example, and if one was ever committed, rotate it — git history keeps it forever.

About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds developer tools at JSON Dev Tools.