Skip to content

Deploying Express Applications

Getting an Express app running in production reliably involves more than just node server.js, process management, environment configuration, and often a reverse proxy. This lesson covers the essential pieces.

Why You Need a Process Manager

Running node server.js directly means the process dies, and stays dead, if it crashes, and there's no automatic restart, log rotation, or clustering. A process manager like PM2 keeps your app running, restarts it automatically on crashes, and can run multiple instances across CPU cores.

Most hosting platforms (Render, Railway, Heroku, AWS Elastic Beanstalk) handle some of this for you automatically; when managing your own server (a VPS), a process manager is essential.

npm install -g pm2

pm2 start server.js --name my-app -i max
pm2 save
pm2 startup

-i max runs one instance per available CPU core (clustering); pm2 startup configures PM2 to restart your app automatically after a server reboot.

Environment-Aware Configuration

NODE_ENV=production node server.js

// in code
if (process.env.NODE_ENV === 'production') {
  app.set('trust proxy', 1);
}
  • Set NODE_ENV=production in production, some libraries (like Express itself) enable optimizations based on it.
  • Never run with development-only middleware (verbose error pages, permissive CORS) in production.
  • Store all production secrets as real environment variables, not a committed .env file.
  • Ensure app.set('trust proxy', 1) is set when running behind any reverse proxy or load balancer.

Deployment Cheatsheet

Common deployment tools and what problem each solves.

Tool Solves
PM2 Process management, auto-restart, clustering
Docker Consistent, reproducible runtime environment
Nginx (reverse proxy) TLS termination, load balancing, static file offloading
GitHub Actions / CI Automated testing and deployment on every push
Platform-as-a-Service (Render, Railway) Managed deployment without server administration

Dockerizing an Express App

A Dockerfile packages your app and its exact dependency versions into a reproducible image, eliminating "works on my machine" deployment issues.

# Dockerfile
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000
CMD ["node", "server.js"]

npm ci --omit=dev installs exact dependency versions from the lockfile, skipping devDependencies for a smaller production image.

Using Nginx as a Reverse Proxy

Running Express directly on port 80/443 is uncommon in production. A reverse proxy like Nginx typically terminates TLS, handles static file caching, and forwards requests to your Node process running on an internal port.

# nginx.conf (simplified)
server {
  listen 443 ssl;
  server_name myapp.com;

  location / {
    proxy_pass http://localhost:3000;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Remember to set app.set('trust proxy', 1) in Express so req.ip and secure cookies work correctly behind this proxy.

Common Mistakes

  • Running node server.js directly in production with no process manager and no automatic restart on crash.
  • Forgetting trust proxy when deployed behind Nginx or a load balancer, breaking req.ip and secure cookies.
  • Committing a .env file with real production secrets instead of configuring them on the hosting platform.
  • Not setting NODE_ENV=production, missing framework-level optimizations and accidentally leaving dev-only behavior enabled.

Key Takeaways

  • A process manager (PM2) or managed platform keeps your app running and restarts it automatically on crashes.
  • Docker packages your app into a reproducible, consistent runtime environment.
  • A reverse proxy (Nginx) typically handles TLS termination and forwards traffic to your Node process.
  • Always set trust proxy and NODE_ENV=production appropriately for a proxied production deployment.

Pro Tip

Before deploying to a real server, test your production Docker image locally with the exact same environment variables and NODE_ENV=production set, catching configuration mistakes before they reach a live environment.