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.