The tutorial, revised August 8, presents two routes: Hostinger’s managed Node.js Web App hosting or a self-managed VPS. The managed route may be appropriate for a small application where the provider’s build and restart behavior is acceptable. A VPS remains the right choice where an IT team needs OS access, custom services, its own monitoring agent, firewall policy, private networking, or a predictable release process.
The important distinction is operational responsibility. Connecting GitHub to a hosting dashboard can deploy code, but it does not prove that the same dependencies, runtime version, secrets, migrations, health checks, and rollback path will behave consistently on the next release.
Production installs need a lockfile, not npm install
Hostinger tells readers to run
npm installafter cloning a repository to the VPS. That command is convenient for development, but it is a weak default for a production release. npm’s own documentation identifies
npm cias the command intended for automated environments and deployments: it requires a committed
package-lock.json, removes any existing
node_modulesdirectory, refuses to proceed if the manifest and lockfile disagree, and does not rewrite either file.
That difference prevents a familiar failure mode: a server installs a newer package version that still satisfies a permissive range in
package.json, while the developer’s working machine continues using an older tree. A dependency expressed as
"express": "^4.21.0"is not a record of the exact package set that was tested. The lockfile is.
A deployment-ready repository should therefore include all of the following:
package.jsonshould declare a realstartscript and place runtime packages underdependencies, notdevDependencies.package-lock.jsonshould be committed and reviewed whenever dependencies change.- A production release should run
npm ci --omit=dev, except where the application’s build process genuinely needs development tooling on the target host. - The build, test, and database-migration stages should occur before traffic is switched to the new process.
The last point is where dashboard-driven deployments commonly disappoint. If a platform runs a build and immediately replaces the serving process, a failed build is visible; a successful build with a broken migration or missing secret may only become visible after users begin receiving errors. A health endpoint that only returns a static
200 OKdoes not catch that class of failure.
“Latest LTS” is not a deployment version
The guide recommends NVM and
nvm install --lts, which is sensible for interactive setup. It is not enough for a release definition. As of August 9, 2026, the Node.js project lists Node.js 24.18.0 as the latest LTS release and Node.js 26.5.0 as the latest Current release; Node.js 26 is not scheduled to become LTS until October 2026. Production services should be on a supported LTS or Maintenance LTS line, rather than an unpinned moving target.
The practical problem with
nvm install --ltsis that the same command can install a different major or patch line next month. That may be desirable during a planned upgrade, but it is not a substitute for one. Native modules, framework support policies, OpenSSL changes, and altered defaults can turn a routine rebuild into an outage.
Set the runtime policy in the repository and enforce it on the host. For example, use the
enginesfield in
package.jsonto define an approved range, retain the exact installed version in deployment records, and make the service execute Node from a known path. A team can choose Node 24 LTS today and schedule Node 26 validation separately; it should not silently receive Node 26 because a server was rebuilt.
NVM adds another operational wrinkle that Hostinger only partly acknowledges. PM2’s startup mechanism generates a systemd command that includes the active Node binary path. PM2’s own documentation warns that changing Node versions requires regenerating that startup configuration. An administrator who upgrades Node under NVM but does not refresh the generated service can find that the application starts under the old runtime—or fails to start after a reboot.
Shell exports disappear when systemd takes over
The VPS walkthrough uses:
export NODE_ENV=production
export PORT=3000
Those variables apply to the current shell. They are not a durable production configuration. A process started manually from that terminal may work, while the same application started later by PM2’s systemd integration does not inherit the variables. That is exactly the kind of discrepancy that produces a successful test immediately after installation and a broken service after the first reboot.
Express documentation recommends setting production variables through the operating system’s init system rather than relying on an interactive shell. PM2 can also carry environment values in an ecosystem file, which is a better fit than scattering
exportcommands through an operator’s shell history.
A minimal
ecosystem.config.cjsmakes the process definition reviewable:
module.exports = {
apps: [{
name: "node-app",
script: "./app.js",
cwd: "/var/www/node-app",
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
PORT: 3000
},
max_memory_restart: "500M"
}]
};
Secrets should still not be committed to that file. Put database passwords, OAuth client secrets, signing keys, and API tokens in a root-readable systemd environment file, the hosting platform’s encrypted variables store, or a dedicated secrets-management service. Ensure the application account—not
root—owns the working directory and can read only the secrets it needs.
PM2 remains a reasonable process supervisor, but it is not magic availability. Run
pm2 startup, execute the exact privileged command PM2 prints, then run
pm2 save. Test the result with an actual reboot before declaring the deployment complete. PM2’s
reloadcan provide a rolling replacement in cluster mode, but only if the application can shut down cleanly and does not keep sessions, WebSocket state, or jobs solely in process memory.
The supplied NGINX block omits the headers applications use
Hostinger’s NGINX configuration forwards the host header and includes WebSocket upgrade headers. It does not forward the original client address or the original request scheme. That omission is easy to miss because a simple Express “Hello World” page will load normally.
Behind TLS-terminating NGINX, an application needs to know that the original browser request was HTTPS. It also often needs the real client IP for audit logging, rate limiting, abuse controls, and geographic or security policy. NGINX’s documentation provides
X-Real-IPand
X-Forwarded-Forpatterns; Express’s proxy guidance requires the application to trust only the proxy topology it actually controls.
A more complete local reverse-proxy block is:
location / {
proxy_pass [url]http://127.0.0.1:3000[/url]
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
For an Express application reached only through that local NGINX instance, configure the proxy trust explicitly—for example,
app.set('trust proxy', 'loopback')—rather than blindly trusting all forwarded headers. A broad
truesetting is dangerous when the application can also be reached through another route, because a client may be able to spoof the headers the app treats as authoritative.
This is more than a logging nicety. Without the scheme header and correct Express trust setting, code that sets secure session cookies may conclude the request is plain HTTP and decline to set them. Authentication failures that appear only in production frequently trace back to that proxy boundary.
Health checks must test readiness, not merely process survival
The tutorial suggests checking a
/healthresponse and using
pm2 logsor
pm2 monit. Those are necessary operator checks, but the proposed resource figures—under 50 ms event-loop latency, roughly 300–500 MB of memory for a “standard Express app,” and CPU below 70 percent—are not useful universal health thresholds. A small API may correctly use far less memory; an image-processing or reporting service may need more. Memory growth, error rate, queue depth, database latency, and p95 or p99 request latency are meaningful only against that application’s own established behavior and host limits.
Express’s own production guidance draws a useful line between liveness and readiness. A process can be alive while its database pool is exhausted, its migrations are incomplete, its required upstream API is unavailable, or its cache is disconnected. A readiness endpoint should verify the dependencies required to accept new traffic, while a liveness endpoint should remain simple enough to tell the supervisor when a process is irrecoverably stuck.
Deployments also need graceful shutdown handling. PM2 or systemd sends a termination signal when replacing a process; the application should stop accepting new connections, finish in-flight requests within a bounded timeout, close database and queue connections, and then exit. Restarting a process without that behavior turns every rollout into a small, unpredictable packet of dropped requests.
Finally, Certbot cannot turn an IP address into the domain validation path most teams expect. Let’s Encrypt’s HTTP-01 challenge validates control through port 80 for the requested name, so public DNS must point to the server and both firewall and upstream network policy must permit HTTP during issuance and renewal. The correct post-deployment check is therefore not just a browser padlock: confirm external DNS resolution, test the HTTP-to-HTTPS redirect, inspect certificate renewal, and verify the application’s real authentication and write paths through the public hostname.
A Node.js service is deployed when it survives the second release and the first reboot with the same runtime, dependencies, configuration, traffic behavior, and observability as the first. The PM2-plus-NGINX pattern can achieve that on a VPS, but only after the shell commands in a basic tutorial are turned into a defined, repeatable release system.