Deployment & DevOps for Node.js 🚀

1. Introduction 👋

Writing a working Node.js application is only half the job — getting it running reliably in production is the other half. This tutorial covers the full deployment lifecycle: build processes, containerization, orchestration, CI/CD, monitoring, and zero-downtime deployment strategies.

Information

Deployment practices vary by team size and infrastructure, but the underlying principles — reproducibility, observability, and graceful failure handling — apply everywhere.

2. Preparing for Production đŸ§ŗ

Before deploying, an application should be audited against a production readiness checklist.

  • All secrets loaded from environment variables or a secrets manager — none hardcoded.
  • NODE_ENV=production set correctly.
  • Dependencies audited and locked via package-lock.json.
  • Logging, health checks, and graceful shutdown implemented.

Tip

Run npm ci instead of npm install in production and CI environments — it installs exactly what's in the lockfile.

3. Production Build 🔨

For TypeScript projects, compile source into plain JavaScript before deploying — never ship ts-node or run .ts files directly in production.

Terminal

npm run build
node dist/index.js

package.json (scripts)

{
  "scripts": {
    "build": "tsc -p tsconfig.build.json",
    "start": "node dist/index.js"
  }
}

4. Environment Configuration âš™ī¸

Configuration should vary only through environment variables, never through code branches per environment.

src/config.ts

interface Config {
  port: number;
  env: "development" | "staging" | "production";
  databaseUrl: string;
}

export const config: Config = {
  port: Number(process.env.PORT ?? 3000),
  env: (process.env.NODE_ENV as Config["env"]) ?? "development",
  databaseUrl: process.env.DATABASE_URL as string,
};

Best Practice

Follow the Twelve-Factor App methodology: strict separation of config from code makes the same build artifact deployable anywhere.

5. Process Managers đŸ§‘â€âœˆī¸

Process managers keep a Node.js application running, automatically restarting it on crashes and managing logs.

  • PM2 — feature-rich, widely used for traditional VM/bare-metal deployments.
  • systemd — OS-level service management, no extra dependency.
  • Container orchestrators (Docker, Kubernetes) — handle restarts at the infrastructure level instead.

6. PM2 🔄

PM2 is a popular process manager for Node.js, offering clustering, auto-restart, and built-in log management.

Terminal

npm install pm2 -g
pm2 start dist/index.js --name my-app -i max
pm2 save
pm2 startup

ecosystem.config.json

{
  "apps": [
    {
      "name": "my-app",
      "script": "dist/index.js",
      "instances": "max",
      "exec_mode": "cluster",
      "env": { "NODE_ENV": "production" }
    }
  ]
}

Tip

The -i max flag runs one instance per CPU core in cluster mode, taking advantage of all available cores automatically.

7. Docker đŸŗ

Docker packages an application with its runtime and dependencies into a portable, reproducible container image.

Dockerfile

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

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

Best Practice

Use a multi-stage build to keep the final image small, and run the container as a non-root user for defense in depth.

8. Docker Compose 🧱

Docker Compose defines multi-container applications (app, database, cache) declaratively for local development or simple deployments.

docker-compose.yml

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb
    depends_on:
      - db
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_PASSWORD=pass
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Terminal

docker compose up -d

9. Kubernetes â˜¸ī¸

Kubernetes orchestrates containers at scale, handling scheduling, scaling, self-healing, and rolling updates automatically.

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: myregistry/my-app:1.0.0
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet: { path: /health, port: 3000 }
          resources:
            limits: { memory: "256Mi", cpu: "500m" }

Information

Kubernetes' readinessProbe and livenessProbe rely directly on health check endpoints (see Section 21).

10. Nginx 🌐

Nginx is commonly placed in front of Node.js applications to handle TLS termination, static file serving, and request routing.

/etc/nginx/sites-available/my-app

server {
    listen 443 ssl;
    server_name example.com;

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

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

11. Reverse Proxy 🔀

A reverse proxy sits between clients and your Node.js application, handling concerns that don't belong in application code.

Client
Reverse Proxy (Nginx / Caddy)
Node.js App
TLS termination
Static files
Load balancing

Tip

Terminating TLS at the proxy layer keeps your Node.js process simpler and lets it focus purely on application logic.

12. HTTPS & SSL 🔏

Production traffic must always be encrypted. Let's Encrypt provides free, automatically renewable TLS certificates.

Terminal

sudo certbot --nginx -d example.com -d www.example.com

Important

Certificates from Let's Encrypt expire every 90 days — ensure automatic renewal is configured (certbot sets up a cron job or systemd timer by default).

13. Domains & DNS 🌍

DNS records map human-readable domain names to server IP addresses or other resource records.

Record TypePurpose
AMaps a domain to an IPv4 address
AAAAMaps a domain to an IPv6 address
CNAMEAliases one domain to another
TXTVerification records, SPF/DKIM for email

Note

DNS changes can take time to propagate globally due to caching (TTL) — plan cutovers with this delay in mind.

14. Load Balancing âš–ī¸

Load balancers distribute incoming traffic across multiple application instances to improve throughput and resilience.

nginx load balancing

upstream node_app {
    least_conn;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;
}

server {
    location / {
        proxy_pass http://node_app;
    }
}

Tip

Cloud providers offer managed load balancers (AWS ALB, GCP Load Balancer) that also handle health checks and TLS automatically.

15. Horizontal Scaling â†”ī¸

Horizontal scaling adds more instances of your application rather than making a single instance bigger.

  1. Ensure the application is stateless — no in-memory session or cache that other instances can't access.
  2. Move shared state to Redis, a database, or a distributed cache.
  3. Add instances behind a load balancer as traffic grows.

Best Practice

Design for horizontal scaling from day one — retrofitting statelessness into an existing app is significantly harder than starting with it.

16. Vertical Scaling â†•ī¸

Vertical scaling increases the resources (CPU, memory) of a single instance rather than adding more of them.

Caution

Vertical scaling has a hard ceiling and creates a single point of failure — it's best used as a short-term fix, not a long-term scaling strategy.

17. CI/CD Pipelines 🔁

Continuous Integration and Continuous Deployment automate testing, building, and releasing code on every change.

Push to Git
Run tests & linting
Build Docker image
Push image to registry
Deploy to production

18. GitHub Actions âš™ī¸

GitHub Actions is a widely used CI/CD platform integrated directly into GitHub repositories.

.github/workflows/deploy.yml

name: Deploy
on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - run: npm run build
      - name: Build & push Docker image
        run: |
          docker build -t myregistry/my-app:${{ github.sha }} .
          docker push myregistry/my-app:${{ github.sha }}

19. Monitoring 📡

Monitoring provides visibility into an application's health and performance after deployment, catching problems before users report them.

  • APM tools — New Relic, Datadog, Dynatrace for request-level tracing.
  • Metrics — Prometheus + Grafana for custom dashboards and alerting.
  • Uptime monitoring — Pingdom, UptimeRobot for external availability checks.

20. Logging 📝

Centralized, structured logging is essential once you have more than one instance — you can't ssh into every server to check logs individually.

src/logger.ts

import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  formatters: { level: (label) => ({ level: label }) },
});

logger.info({ requestId: "abc123", path: "/api/users" }, "Request handled");

Tip

Ship logs to a centralized platform (ELK stack, Datadog, CloudWatch) rather than relying on local files that disappear when a container restarts.

21. Health Checks â¤ī¸â€đŸŠš

Health check endpoints let load balancers and orchestrators know whether an instance is ready to serve traffic.

src/routes/health.ts

router.get("/health", async (req, res) => {
  try {
    await db.query("SELECT 1");
    res.status(200).json({ status: "ok" });
  } catch {
    res.status(503).json({ status: "unavailable" });
  }
});

Best Practice

Distinguish between a liveness check (is the process running?) and a readiness check (can it serve traffic right now?) — Kubernetes treats them differently.

22. Graceful Shutdown 🛑

On shutdown, an application should stop accepting new requests but finish in-flight ones before exiting — abruptly killing the process can drop active connections.

src/index.ts

const server = app.listen(3000);

process.on("SIGTERM", () => {
  console.log("SIGTERM received, shutting down gracefully...");
  server.close(() => {
    console.log("Closed remaining connections.");
    process.exit(0);
  });

  setTimeout(() => process.exit(1), 10_000);
});

Important

Always set a timeout as a safety net — if connections don't close naturally within a reasonable window, force an exit anyway.

23. Zero-Downtime Deployment 🔄

Zero-downtime deployment ensures users never see errors or interruptions while a new version is being rolled out.

  1. Rolling updates — replace instances one at a time, keeping others serving traffic.
  2. Blue-green deployment — run two identical environments, switch traffic instantly once the new one is verified.
  3. Canary releases — gradually shift a small percentage of traffic to the new version before a full rollout.

Tip

Kubernetes' default RollingUpdate strategy combined with readiness probes achieves zero-downtime deploys with minimal extra configuration.

24. Backup & Recovery 💾

Regular, tested backups are a critical safety net — a backup that has never been restored isn't a real backup.

  • Automate database backups on a fixed schedule (e.g. daily snapshots).
  • Store backups in a separate region or provider from the primary database.
  • Periodically test restoring from backup to verify the process actually works.
  • Document a clear disaster recovery runbook with defined RTO/RPO targets.

25. Cloud Deployment â˜ī¸

Major cloud providers offer managed compute options that abstract away much of the underlying infrastructure.

ProviderCommon Options
AWSECS, EKS, Elastic Beanstalk, EC2
Google CloudCloud Run, GKE, App Engine
AzureApp Service, AKS
Platform-as-a-ServiceRender, Railway, Fly.io, Heroku

Note

Managed PaaS options trade some flexibility for a much simpler deployment experience — a good fit for smaller teams and early-stage projects.

26. Serverless Deployment đŸŒŠī¸

Serverless platforms run your code on-demand without managing servers, scaling automatically and billing per invocation.

src/handler.ts

import type { APIGatewayProxyHandler } from "aws-lambda";

export const handler: APIGatewayProxyHandler = async (event) => {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: "Hello from Lambda!" }),
  };
};

Caution

Serverless functions incur cold starts — minimize dependencies and bundle size to keep invocation latency low.

27. Performance Monitoring 📊

Post-deployment, track key performance indicators continuously to catch regressions before they affect many users.

  • Request latency (p50, p95, p99).
  • Error rate and status code distribution.
  • Event loop lag and memory usage over time.
  • Database query latency and connection pool saturation.

Reference

See the dedicated "Performance & Optimization" tutorial for detailed profiling and monitoring techniques.

28. Best Practices ✅

  • Automate builds and deployments through CI/CD — avoid manual, ad-hoc deploys.
  • Keep application instances stateless to support horizontal scaling.
  • Implement health checks, graceful shutdown, and structured logging from the start.
  • Use infrastructure-as-code (Terraform, Pulumi) to make environments reproducible.
  • Test backup restoration regularly, not just backup creation.

29. Common Mistakes âš ī¸

MistakeConsequence
Deploying manually via SSH/FTPInconsistent, unrepeatable deployments
No graceful shutdown handlingDropped requests and corrupted data on restart
Storing session state in memoryBreaks horizontal scaling entirely
Ignoring health checks in orchestrationTraffic routed to unready or unhealthy instances
Never testing backup restorationBackups may be unusable when actually needed

30. Frequently Asked Questions ❓

Question

Should I use Docker even for a small project?

Answer

Often yes — it guarantees the same environment locally, in CI, and in production, eliminating a huge class of "works on my machine" bugs.

Question

Is Kubernetes overkill for most applications?

Answer

For small to medium apps, often yes — a simpler PaaS or managed container service can meet the same needs with far less operational overhead.

Question

How do I achieve zero-downtime deployments without Kubernetes?

Answer

PM2's cluster mode with reload, or a load balancer combined with rolling instance replacement, can achieve similar results on simpler infrastructure.

Question

What's the difference between liveness and readiness checks?

Answer

Liveness confirms the process hasn't crashed; readiness confirms it's able to serve traffic right now — a healthy-but-not-ready instance shouldn't receive requests yet.

31. Summary 📋

Deploying Node.js applications reliably involves far more than running node index.js on a server. From containerization and CI/CD pipelines to load balancing, health checks, and zero-downtime deployment, each piece contributes to an application that stays available and recoverable under real-world conditions.

  1. Build once, deploy the same artifact across every environment.
  2. Containerize with Docker and orchestrate with Kubernetes or a managed platform as needed.
  3. Automate testing and deployment through CI/CD pipelines.
  4. Implement health checks, graceful shutdown, and centralized logging.
  5. Monitor continuously and keep tested backups ready for recovery.

Summary

A solid deployment and DevOps practice turns "it works on my machine" into "it works reliably, everywhere, all the time." 🚀