Documentation · Deploying your app

Deploy a Node.js application

Binding the right port, a startup command that survives restarts, and keeping install time off your request path.

Updated 2026-09-25 · 7 min read

Node.js is the easiest runtime to get running and the easiest to get subtly wrong, because the failure mode is silence: the app starts, logs a URL, and is unreachable.

Bind to 0.0.0.0

Plain http server:

const http = require("node:http");

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

http
  .createServer((req, res) => {
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify({ status: "ok" }));
  })
  .listen(port, host, () => {
    console.log(`listening on http://${host}:${port}`);
  });

Express is the same idea with one extra option, and the setting people miss:

const app = require("express")();

app.get("/", (_req, res) => res.send("ok"));

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

Express will not warn you. Omit the host and it defaults to loopback, so the console shows a healthy-looking line while nothing outside the container can reach it.

Read the port from the environment

Do not hardcode the port. Read process.env.PORT and fall back to a default. That way the same code runs locally, in CI, and in the container regardless of which port it was given.

A startup command that survives restarts

On the Startup tab, set the command to match your entry point. For a small service that is usually all you need:

npm install --omit=dev && node index.js

This is correct and slightly wasteful: it reinstalls on every restart. Once restarts become frequent — and with scheduled tasks and restarts configured, they will — install once and start many times instead:

node index.js

Run npm install --omit=dev manually the first time via the console, then commit to the fast command. If you deploy from Git, make it a scheduled task that installs and restarts, so the container only restarts once the install succeeds.

Use a process manager for long-running services

The panel restarts a container that exits, but it will not restart a process that is still alive and has stopped being useful. If your app handles long-lived connections, a worker pool or a database that blinks, run it under a supervisor so it recovers on its own:

npx pm2 start index.js --name app --instances max
npx pm2 save
npx pm2 startup

Use --instances max only if you have spare memory — each instance costs a full baseline. On a 512 MB Starter plan, one instance is usually the right answer and two will fight over memory.

PM2 and the container exit problem

Watch for the reverse case: if your app is a bot with no HTTP listener, npm start exits and the container stops. Keep the process in the foreground. With PM2, start the daemon and then keep a foreground process attached so the container stays up:

npx pm2 start bot.js --name bot
npx pm2 logs bot --nostream && tail -f /dev/null

Discord bots are covered in more detail in Deploying a Discord bot.

Memory limits on small containers

Node's default heap can exceed a small container's limit, and the failure arrives as an abrupt kill rather than a JavaScript error. Cap it explicitly:

NODE_OPTIONS="--max-old-space-size=384"

Set that as an environment variable on the service. Keeping the heap ceiling below the container limit leaves room for buffers and native allocations outside the heap — otherwise the process is killed before V8 gets a chance to collect.

These guides describe behaviour that is common across container platforms. Where a setting is specific to your service — your assigned port, your SFTP credentials, your startup command — it is shown in the panel rather than here, so check the Startup and Files tabs for your own values.

Found an error or something unclear? Let us know so we can correct it.