Skip to content
NewHost
Menu

How to Host a Node.js App - From Localhost to Production in SA

How to host a Node.js app - reading PORT, a proper start script, restarts, graceful shutdown, logs and HTTPS, on managed hosting or your own VPS.

By NewHost team · · 5 min read

To host a Node.js app, you need four things: an app that reads its port and settings from environment variables, a start script that runs it, something that keeps the process alive and restarts it when it crashes, and a way to route HTTPS traffic from your domain to it. A managed Node.js host provides the last two for you; on a VPS you set them up yourself. This guide covers the code changes every app needs, then both hosting routes.

1. Read the port from the environment

On your laptop the app probably listens on a fixed port like 3000. In production the platform decides the port and passes it in, conventionally as PORT. Read it, with a fallback for local development:

// server.js
import express from "express";

const app = express();

app.get("/", (req, res) => {
  res.send("Hello from production");
});

const port = Number(process.env.PORT) || 3000;
const server = app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

Do not bind to 127.0.0.1 or localhost explicitly. When you omit the host, Node.js listens on all interfaces, which is what a platform or reverse proxy needs to reach the app.

2. Add a start script and pin Node.js

The host runs whatever your start script says. Keep it simple and do not use development tools like nodemon here:

{
  "name": "my-api",
  "type": "module",
  "scripts": {
    "dev": "node --watch --env-file=.env server.js",
    "start": "node server.js"
  },
  "engines": {
    "node": ">=24"
  }
}

Node.js has built-in --watch and --env-file flags, so for local development you no longer need nodemon or dotenv. In production, set real environment variables on the host instead of shipping a .env file.

If your app has a build step (TypeScript, Next.js, Nuxt), add a build script and make start run the compiled output:

{
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  }
}

3. Move configuration into environment variables

Anything that differs between your laptop and production belongs in an environment variable: database URLs, API keys, allowed CORS origins, email credentials. Validate them at startup so a missing value fails loudly instead of halfway through a request:

const required = ["DATABASE_URL", "SESSION_SECRET"];
for (const name of required) {
  if (!process.env[name]) {
    console.error(`Missing environment variable ${name}`);
    process.exit(1);
  }
}

Set NODE_ENV=production. Express and many libraries switch off verbose error pages and enable caching when they see it.

4. Install only what production needs

Commit your lockfile and install with npm ci, which uses exact versions and fails if the lockfile is out of date. If you have no build step, you can skip development dependencies:

npm ci --omit=dev

If you do have a build step, install everything, build, then start.

5. Shut down gracefully

When a host redeploys or restarts your app, it sends SIGTERM first. Finish in-flight requests and close database connections before exiting, or users see dropped requests during every deploy:

function shutdown(signal) {
  console.log(`${signal} received, closing server`);
  server.close(() => {
    // close database pools here, e.g. await pool.end()
    process.exit(0);
  });
  // Force exit if connections do not close in time
  setTimeout(() => process.exit(1), 10_000).unref();
}

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

6. Log to stdout and add a health check

Write logs with console.log / console.error (or a logger like pino writing to stdout). Platforms collect stdout and stderr; writing your own log files to disk just fills the disk.

A small health endpoint helps you and any monitoring tool see whether the app is alive:

app.get("/healthz", (req, res) => {
  res.json({ status: "ok", uptime: process.uptime() });
});

7. Trust the proxy

In production your app sits behind a reverse proxy that terminates HTTPS. Tell Express so req.ip, req.protocol and secure cookies work correctly:

app.set("trust proxy", 1);

Route A - Managed Node.js hosting

With managed hosting, the platform handles processes, restarts, routing and SSL. On NewHost the steps are:

  1. Push your code to GitHub or GitLab.
  2. Create an app in the dashboard and connect the repository and branch.
  3. Set the install command (npm ci), build command (if any) and start command (npm start).
  4. Add your environment variables; they are stored encrypted.
  5. Deploy, then add your domain. A free Let's Encrypt certificate is issued once DNS points to the app.

Every later push to that branch redeploys automatically. Check docs.newhost.co.za for platform specifics such as the port your app should listen on. If you need a database, managed MySQL is included on every plan.

Route B - A VPS you manage

On a VPS you build the same pieces yourself. A minimal setup on Ubuntu:

Keep the process alive with systemd:

# /etc/systemd/system/my-api.service
[Unit]
Description=My Node.js API
After=network.target

[Service]
User=deploy
WorkingDirectory=/srv/my-api
EnvironmentFile=/srv/my-api/.env.production
ExecStart=/usr/bin/node server.js
Restart=always

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now my-api
journalctl -u my-api -f

(PM2 is a popular alternative: pm2 start server.js --name my-api, then pm2 startup and pm2 save.)

Route traffic with Nginx:

server {
    server_name api.example.co.za;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Then add SSL with Certbot, set up a firewall, schedule OS updates and arrange backups. It works well, but it is ongoing work; we compare the two routes in Node.js on a VPS vs managed hosting.

Before you go live

  • npm start works locally with NODE_ENV=production
  • All config comes from environment variables, with no secrets in Git
  • Graceful shutdown handles SIGTERM
  • Errors are logged, and not sent to users as stack traces
  • HTTPS works and HTTP redirects to it
  • You know how to restore a backup

For a deeper security pass, work through the Node.js security checklist.

Frequently asked questions

What is the cheapest way to host a Node.js app?

For a small app, entry-level managed app hosting is usually the lowest total cost once you count your own time. A VPS can look cheaper on paper, but you pay in hours for setup, patching and troubleshooting.

Why does my Node.js app work locally but not on the server?

The usual causes are a hard-coded port, a missing environment variable, a dependency listed under devDependencies that production needs, or file-name case differences, because Linux is case-sensitive while macOS and Windows usually are not.

Do I still need PM2?

On managed hosting, no, because the platform keeps your process running. On a VPS, you need something to restart the app after crashes and reboots, which can be PM2 or a systemd service.

Should I commit my .env file?

No. Add .env* to .gitignore and set production values in your host's environment variable settings, which keeps secrets out of your repository history.

Ready to skip the server setup? NewHost Node.js hosting runs your app from Git on South African servers, from R99 a month.

Related guides

Ready to launch on NewHost?

Choose a plan and go live today, or tell us what you need and we'll recommend the right setup.