Blog
3 min read

How to Deploy an Express.js App to Production

Getting an Express API from localhost to a real server: production settings, listening on PORT, trust proxy, security middleware, a process manager, a reverse proxy with HTTPS, Docker as an option, and the checks that prevent the usual 502s and crashes.

Express is the most common way to build a Node.js backend. It runs the same in production as in development — node server.js — but a few settings and supporting pieces make the difference between "works" and "stays up". (What is Express?, What is Node.js?)

Step 1: make the app production-ready

import express from 'express'
import helmet from 'helmet'

const app = express()

app.set('trust proxy', 1)          // behind Nginx/Caddy/Cloudflare
app.use(helmet())                  // sensible security headers
app.use(express.json({ limit: '1mb' }))

app.get('/health', (req, res) => res.json({ ok: true }))

// ...your routes

app.use((err, req, res, next) => {
  console.error(err)
  res.status(500).json({ error: 'Something went wrong' })   // don't leak stack traces
})

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

What each bit is for:

  • process.env.PORT — hosts tell your app which port to use.
  • trust proxy — so req.ip and req.secure reflect the real client behind a reverse proxy (needed for rate limiting and secure cookies).
  • helmet — security headers. (HTTP security headers)
  • A /health route — for uptime checks and zero-downtime deploys. (Health check endpoints)
  • An error handler — users see a generic message; details go to your logs.

Step 2: environment variables

Set NODE_ENV=production (Express and many libraries behave more efficiently with it) and keep secrets — database URL, API keys — in environment variables, not code. (Environment variables explained)

Step 3: install and build on the server

git clone https://github.com/you/api.git /srv/api
cd /srv/api
npm ci --omit=dev
npm run build      # if you use TypeScript

npm ci installs exactly what's in your lock file. (package-lock.json explained)

Step 4: keep it running

If you just run node server.js over SSH, it stops when you log out or it crashes. Use a process manager:

PM2:

npm install -g pm2
pm2 start dist/server.js --name api
pm2 save
pm2 startup        # follow the printed command so it starts on boot

or systemd (built in) with Restart=always. (PM2 vs systemd, systemd service file)

Step 5: HTTPS with a reverse proxy

Don't expose Node directly on port 80/443. Put Caddy or Nginx in front:

api.example.com {
    reverse_proxy 127.0.0.1:3000
}

(What is Caddy?, Nginx reverse proxy config)

Then close every port except 22, 80 and 443 in the firewall. (UFW firewall basics)

Option: Docker

FROM node:24-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
USER node
CMD ["node", "server.js"]

Listen on 0.0.0.0 inside the container. (Production Dockerfile for Node.js)

Before you call it done

Common problems


EasySpawn runs your Express API behind HTTPS, keeps it running and restarts it on crashes, with Postgres and daily backups alongside. See how it works or join the waitlist.

Related: Deploy a Node.js App to a VPS · What Is Express? · PM2 vs systemd · Designing a REST API

Keep reading