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:
// 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:
{
"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:
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:
"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
- Choose a plan on the website plans page and pick A website and Node.js at checkout.
- Upload
server.js,package.jsonandpackage-lock.json(notnode_modules) in Files, or paste your GitHub repository into Git repository on the Startup tab. Add.envby hand. - Restart from the console. It shows
npminstalling your dependencies, thennpm startrunning and yourListening online. - 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
localhostor a fixed port. Use0.0.0.0andSERVER_PORT. EADDRINUSE: two things want the same port, usually a secondapp.listenor a leftover process.- Login works locally but not on the site:
trust proxyis missing, so thesecuresession 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.