Deploying Express Applications

1. Setting NODE_ENV to Production

Example: env

NODE_ENV=production node --enable-source-maps dist/server.js
Effect of NODE_ENV=productionNotes
View cache enabledTemplates compiled once
Verbose error stacks suppressed (in your handler)You implement
npm install skips devDependenciesnpm ci --omit=dev

2. Using Process Managers

ToolUse case
PM2VM / bare-metal Node clustering + restarts
systemdLinux service manager
Docker + orchestratorKubernetes, ECS, Nomad RECOMMENDED

3. Configuring PM2

Example: ecosystem.config.cjs

module.exports = {
  apps: [{
    name: "api",
    script: "dist/server.js",
    instances: "max",
    exec_mode: "cluster",
    env: { NODE_ENV: "production", PORT: 3000 },
    max_memory_restart: "512M"
  }]
};

4. Setting Up Reverse Proxy

Example: Nginx

server {
  listen 443 ssl http2;
  server_name api.example.com;

  ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header Connection        "upgrade";
  }
}

5. Implementing Graceful Shutdown

Example: SIGTERM handler

let shuttingDown = false;
const server = app.listen(PORT);

async function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  logger.info({ signal }, "shutting down");
  // 1. stop accepting new requests
  server.close(() => logger.info("http closed"));
  // 2. drain dependencies
  await Promise.allSettled([pool.end(), redis.quit()]);
  // 3. force-exit after grace period
  setTimeout(() => process.exit(1), 10_000).unref();
}
["SIGTERM","SIGINT"].forEach(s => process.on(s, () => shutdown(s)));

6. Using Environment-Specific Config

Example: Validated config

export const config = z.object({
  NODE_ENV:     z.enum(["development","staging","production"]),
  PORT:         z.coerce.number().int().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET:   z.string().min(32)
}).parse(process.env);

7. Enabling HTTPS

Example: TLS termination

// Recommended: terminate TLS at the load balancer / CDN.
// If running TLS in Node directly:
import https from "node:https";
import fs from "node:fs";
https.createServer({
  key:  fs.readFileSync("./key.pem"),
  cert: fs.readFileSync("./cert.pem")
}, app).listen(443);

8. Implementing Health Check Endpoints

Example: For Kubernetes

app.get("/livez",  (req, res) => res.sendStatus(200));
app.get("/readyz", async (req, res) => {
  const ok = await checkAll();
  res.sendStatus(ok ? 200 : 503);
});

9. Setting Up Logging

Example: pino → stdout

// Log JSON to stdout; let the platform (CloudWatch, Stackdriver, Loki) aggregate.
import pino from "pino";
export const logger = pino({ level: process.env.LOG_LEVEL || "info" });

10. Monitoring Application

PillarTooling
MetricsPrometheus + Grafana, Datadog, New Relic
LogsLoki, ELK, CloudWatch
TracesOpenTelemetry → Jaeger / Tempo
ErrorsSentry
UptimeBetterStack, Pingdom, Uptime Kuma

Example: OpenTelemetry auto-instrumentation

// preload before app: node --import @opentelemetry/auto-instrumentations-node/register dist/server.js
// Set OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME via env

11. Containerizing with Docker

Example: Multi-stage Dockerfile

FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM deps AS build
COPY . .
RUN npm run build

FROM node:20-alpine AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/livez || exit 1
CMD ["node","--enable-source-maps","dist/server.js"]

12. Deploying to Cloud Platforms

PlatformNotes
AWS ECS / FargateContainer; ALB in front; CloudWatch logs
AWS LambdaWrap with @codegenie/serverless-express or AWS Lambda Web Adapter
Google Cloud RunContainer; auto-scale to zero
Azure App Service / Container AppsManaged PaaS
Fly.ioContainer at edge; simple deploy
Render / RailwayGit-push deploy
HerokuBuildpack; legacy but simple
KubernetesFull control; declarative

Production Checklist

  • NODE_ENV=production, source-maps enabled
  • helmet, CORS allowlist, rate limiting
  • HTTPS at edge; HSTS preload
  • Structured JSON logs to stdout
  • /livez and /readyz health endpoints
  • Graceful SIGTERM shutdown ≤ container grace
  • OpenTelemetry traces + metrics + Sentry errors
  • Database connection pool sized for replicas
  • Secrets from manager (not env files in image)
  • Read-only root FS, non-root user, minimal base image