How to host an Express app 24/7 on your own domain

An Express app that works on localhost:3000 needs four small changes to live on a server: the host's port, every interface, the proxy's headers, and secrets outside the code.

6 min readUpdated

1. Listen on the host's port, on every interface

A host gives your app a port in an environment variable and expects it to listen on 0.0.0.0. Hard-coding 3000, or binding to localhost, gives you an app that starts and that nobody can reach. On SnowServers the variable is SERVER_PORT; most hosts call it PORT:

JavaScript
// server.js
const express = require("express");

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

app.get("/", (req, res) => res.send("Hello"));

app.listen(port, "0.0.0.0", () => console.log(`Listening on ${port}`));

2. Say how it starts

Give package.json a start script. Hosts run npm start, so this is the one place that decides what runs:

JSON
{
  "name": "my-site",
  "private": true,
  "scripts": {
    "start": "NODE_ENV=production node server.js"
  },
  "dependencies": {
    "express": "^5.1.0"
  }
}

NODE_ENV=production makes Express cache its view templates and send shorter error pages. Commit package-lock.json too, so the server installs exactly the versions you tested.

TypeScript or a framework with a build step? Build in the start script, for example "start": "npm run build && node dist/server.js", or "next build && next start -H 0.0.0.0 -p $SERVER_PORT" for Next.js.

3. Trust the proxy, once

When HTTPS is handled in front of your app, Express sees plain HTTP from the proxy: req.secure is false, req.ip is the proxy's address, and cookies marked secure are never set. One line fixes all three:

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

The 1 means "believe the one proxy in front of me". Don't use true: it believes every hop, so a visitor could put any address they like in X-Forwarded-For and get past a rate limiter keyed on req.ip.

4. Keep secrets out of Git

Session secrets, database passwords and API keys go in a .env file, listed in .gitignore. Node 20.6 and later read it themselves, with no package:

JSON
"start": "NODE_ENV=production node --env-file=.env server.js"

Then read them as process.env.SESSION_SECRET and so on.

5. You don't need pm2

On your own VPS, pm2 or systemd restarts the app when it crashes. A managed host does that part already, so the app can simply be node server.js. If you do want several processes, Node's own cluster module does it inside the app.

Hosting it on SnowServers

  1. Choose a plan on the website plans page and pick A website and Node.js at checkout.
  2. Upload server.js, package.json and package-lock.json (not node_modules) in Files, or paste your GitHub repository into Git repository on the Startup tab. Add .env by hand.
  3. Restart from the console. It shows npm installing your dependencies, then npm start running and your Listening on line.
  4. On the Website tab, add your domain and its two DNS records. HTTPS follows on its own.

Dependencies install again whenever package.json or the lockfile changes, and the app is started again if it exits. The website docs have the rest.

Common problems

  • "This site is not answering": the app listens on localhost or a fixed port. Use 0.0.0.0 and SERVER_PORT.
  • EADDRINUSE: two things want the same port, usually a second app.listen or a leftover process.
  • Login works locally but not on the site: trust proxy is missing, so the secure session cookie is never sent.
  • JavaScript heap out of memory: a build step is using more memory than the plan has. Build on your machine and upload the output, or pick a bigger plan.