Deploying Express Applications
1. Setting NODE_ENV to Production
| Effect of NODE_ENV=production | Notes |
|---|---|
| View cache enabled | Templates compiled once |
| Verbose error stacks suppressed (in your handler) | You implement |
| npm install skips devDependencies | npm ci --omit=dev |
2. Using Process Managers
| Tool | Use case |
|---|---|
| PM2 | VM / bare-metal Node clustering + restarts |
| systemd | Linux service manager |
| Docker + orchestrator | Kubernetes, 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
| Pillar | Tooling |
|---|---|
| Metrics | Prometheus + Grafana, Datadog, New Relic |
| Logs | Loki, ELK, CloudWatch |
| Traces | OpenTelemetry → Jaeger / Tempo |
| Errors | Sentry |
| Uptime | BetterStack, 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
| Platform | Notes |
|---|---|
| AWS ECS / Fargate | Container; ALB in front; CloudWatch logs |
| AWS Lambda | Wrap with @codegenie/serverless-express or AWS Lambda Web Adapter |
| Google Cloud Run | Container; auto-scale to zero |
| Azure App Service / Container Apps | Managed PaaS |
| Fly.io | Container at edge; simple deploy |
| Render / Railway | Git-push deploy |
| Heroku | Buildpack; legacy but simple |
| Kubernetes | Full 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