How to host a Node.js app

To host a Node.js app, you need a server that runs the right Node version, your code with its package.json, and a start command. The port and secrets should come from environment variables.

Updated 6 min read

Learning objectives

After reading this article you will be able to:

  • Prepare a Node.js project to run outside your machine
  • Write a server that reads its port from the environment and shuts down cleanly
  • Verify the deploy and fix the most common errors
On this page

A Node.js app works on your machine because that machine has the right Node, the installed dependencies and the variables you configured. Hosting means reproducing that environment on a server that stays on all the time and that other people can reach.

The path is the same for an API, a site with a server or a bot: prepare the project, upload the code, install dependencies and start the process. This guide uses an Express API as the example, but the steps apply to any Node.js app. For the big picture, see what is deployment.

1Local projectpackage.jsonpackage-lock.jsonindex.jsUpload to the serverno node_modules or .env2npm ci --omit=devdeps from the lockfile3node processlistens on the port in PORT4PORTenvironment variable
Upload the project with its lockfile, install with npm ci and start node on the port the environment gives in PORT.

What do you need before hosting?#

  • A defined Node version. Pick an LTS release and use the same one locally and in production.
  • A complete package.json, with dependencies and a start script.
  • A committed package-lock.json, so installs are identical on any machine.
  • A place to run it: a VPS, a container or a hosting platform. The difference between them is covered in VPS vs cloud platform.

Which LTS? For a new project, use the current Active LTS of Node.js, as long as your dependencies support it, and declare it in package.json. The official release schedule shows which version is in that phase and how long each one gets fixes.

How do you set up the project?#

Create the minimal structure:

bash
mkdir my-api && cd my-api
npm init -y
npm install express

Edit package.json for ES modules, with the Node version and scripts:

json
{
  "name": "my-api",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "engines": { "node": ">=24" },
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch --env-file-if-exists=.env index.js"
  },
  "dependencies": {
    "express": "^5.0.0"
  }
}

The engines field documents the expected version, and the start script is the command the server will run. dev uses two built-in Node flags: --watch restarts the process when a file changes, and --env-file-if-exists loads variables from a .env file (no error if it is missing), with no need for the dotenv package.

How do you write a production-ready server?#

A test server becomes a production server when it reads the port from the environment, keeps secrets out of the code and handles the shutdown signal.

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

const app = express();
const port = Number(process.env.PORT) || 3000;

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

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

const server = app.listen(port, "0.0.0.0", () => {
  console.log(`Server listening on port ${port}`);
});

// On SIGTERM (restart or deploy), stop accepting new
// connections and finish the ones in progress.
process.on("SIGTERM", () => {
  server.close(() => process.exit(0));
});

Why each part matters:

  • process.env.PORT: the platform or server chooses the port and passes it in a variable. Hardcoding 3000 only works on your machine. The || 3000 is the development fallback.
  • /health route: gives you (and external monitors) a cheap address to check that the app responds.
  • SIGTERM: the signal the system sends to ask a process to stop. Once you register a listener, Node no longer exits immediately (its default behavior), so you must end the process explicitly, as in the example.

Test locally with npm run dev and open http://localhost:3000/health.

How do you handle environment variables and secrets?#

Tokens, database passwords and API keys live in environment variables, never in the repository. Read them with process.env.NAME. Locally, use a .env file and add it to .gitignore; in production, set each variable in your provider's dashboard or in the server process. What are environment variables covers the pattern in detail.

Fail early when a required variable is missing. Instead of an obscure error later on, you get a clear message at startup:

js
if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is not set");
}

How do you upload the code and start the process?#

The general flow, on any host:

  1. Upload the code without node_modules and without .env (use a .gitignore).
  2. On the server, install the exact lockfile dependencies: npm ci --omit=dev.
  3. Set the environment variables, including NODE_ENV=production.
  4. Start with npm start (or node index.js).
  5. Keep the process alive: on a VPS, with a supervisor such as systemd or PM2; in containers and platforms, with their restart policy. See also how to package it with Docker.

If your app is an API, review the API production checklist before opening it to the public.

How do you check that it worked?#

After deploying, confirm in three steps:

  1. Logs: the "Server listening on port" line should appear with no errors after it.
  2. Request: call /health on the public address and expect {"status":"ok"}.
  3. Restart: restart the app and confirm it comes back on its own.

What are the most common errors?#

  • Error: listen EADDRINUSE: address already in use: another process already holds the port. Stop the old instance or change the port.
  • Cannot find module 'express': dependencies were not installed, or the package is in devDependencies and you installed with --omit=dev. Move it to dependencies.
  • SyntaxError: Cannot use import statement outside a module: "type": "module" is missing from package.json, or you use import in a file treated as CommonJS.
  • App starts but the site won't open: the server listens on a fixed port instead of process.env.PORT, or only on localhost. In containers, listen on 0.0.0.0.
  • Works locally, crashes in production: different Node version, missing environment variable or not enough memory. Compare what changed between environments.

How to do this on Square Cloud#

The flow in this guide barely changes on Square Cloud. You upload the project as a ZIP in the dashboard, with the CLI, through GitHub (a push triggers the deploy) or with the VS Code extension, and the deploy form detects index.js as the main file. The squarecloud.app file is optional: the dashboard generates it on deploy. If you prefer the package.json script, the start command field accepts any shell command, such as npm start or npm run migrate && npm start.

The install follows two rules that explain errors from the list above. The platform runs npm install only when node_modules is missing, so upload the project without that folder. It also installs with NODE_ENV=production: anything in devDependencies is left out, and the result is Cannot find module. There is no build step; to compile, use external CI such as GitHub Actions, or put the build in the start command. The TypeScript runtime runs your code without a separate compile step.

The port comes from the PORT variable, which the platform sets, and the server listens on 0.0.0.0, as in the example. Environment variables are set per application in the dashboard (which imports a .env), already exist during the install and apply on the next restart, with no rebuild. The container stays on, and automatic restart, on by default for dashboard deploys, brings back a process that exited with an error, so you can skip PM2. Running out of memory stops the app until you raise its RAM. The npm install output and the "Server listening on port" line show up in the real-time logs.

Each web application uses at least 512 MB of the plan's RAM, and Hobby starts at R$12.49 per month with 1 GB. Node.js is one of several runtimes, alongside Python, Go and Java: see the runtimes available today on Runtimes and the plans on API hosting.

API hosting

APIs in always-on containers, with no cold starts

Learn more

FAQs

Which Node.js version should I use in production?

Use the current Active LTS of Node.js, which gets security fixes for longer. Check the official Node.js release schedule to see which version that is before you pin it in your project.

Do I need PM2 to keep my app running?

It depends on where you run it. On a server you administer, a process supervisor restarts the app if it crashes. On platforms that already restart the process for you, PM2 is usually unnecessary.

Should I upload the node_modules folder?

Usually not. Upload package.json and package-lock.json and let the environment install dependencies with npm ci. node_modules can be huge and depends on the operating system.

Can I host a Node.js app written in TypeScript?

Yes. You can compile to JavaScript before deploying, or use a platform that runs TypeScript directly. Running the compiled code is usually the most predictable option.

Next articleHow to host a FastAPI, Flask or Django appTo host a FastAPI, Flask or Django app, run it with a production server (Uvicorn or Gunicorn) on 0.0.0.0. Commands, Django settings and common errors included.