Skip to main content

Deployment & Best Practices

You’ve built your Node.js application, and it works perfectly on your machine. But running in production is a completely different challenge. Production environments face real-world constraints: thousands of concurrent users, security threats, server crashes, and the need for zero-downtime updates.

Development vs. Production

Understanding the difference is crucial:
AspectDevelopmentProduction
Error messagesDetailed stack tracesGeneric user-friendly messages
PerformanceNot optimizedMaximized, gzip compression
SecurityRelaxed (localhost)Hardened, HTTPS required
LoggingConsole outputStructured logs, monitoring
RestartsManualAutomatic recovery on crash
ConfigurationHardcoded values OKEnvironment variables only

Why Production Best Practices Matter

Real-World Failures

Ignoring production best practices leads to:
  • Security breaches: Hardcoded API keys leaked in public repositories
  • Downtime: Single process crashes take down entire application
  • Data loss: No proper error handling or logging
  • Poor performance: Uncompressed responses, inefficient code
A single exposed API key or database password can compromise your entire application and user data. Never commit secrets to version control.
Let’s go through essential practices to make your application production-ready.

Environment Variables

Never hardcode sensitive information (API keys, database passwords, ports) in your code. Use environment variables.
  1. Install dotenv:
    npm install dotenv
    
  2. Create a .env file:
    PORT=5000
    DB_URI=mongodb://localhost:27017/myapp
    SECRET_KEY=mysecretkey
    
  3. Use it in your code:
    require('dotenv').config();
    
    const PORT = process.env.PORT || 3000;
    console.log(process.env.DB_URI);
    
  4. Important: Add .env to your .gitignore file so it’s not committed to version control.

Production Best Practices

  1. Use Gzip Compression: Use the compression middleware to decrease the size of the response body.
    const compression = require('compression');
    app.use(compression());
    
  2. Security Headers: Use helmet to set various HTTP headers for security.
    const helmet = require('helmet');
    app.use(helmet());
    
  3. Logging: Use a logger like morgan or winston instead of console.log.
  4. Error Handling: Implement a global error handling middleware.
  5. Clustering: Use Node.js Cluster module or a process manager like PM2 to take advantage of multi-core systems.

Deployment Options

1. Heroku (PaaS)

Heroku is a popular platform for deploying Node.js apps.
  1. Create a Procfile in the root:
    web: node server.js
    
  2. Install Heroku CLI and login.
  3. heroku create
  4. git push heroku main

2. Vercel / Netlify (Serverless)

Great for static sites and serverless functions. Next.js apps deploy seamlessly on Vercel.

3. VPS (DigitalOcean, AWS EC2, Linode)

For full control, you can rent a Virtual Private Server.
  • Set up Linux (Ubuntu).
  • Install Node.js and Nginx (as a reverse proxy).
  • Use PM2 to keep your app running.

Using PM2

PM2 is a production process manager for Node.js.
npm install pm2 -g
pm2 start server.js
Commands:
  • pm2 list: List running processes.
  • pm2 stop <id>: Stop a process.
  • pm2 restart <id>: Restart a process.
  • pm2 logs: View logs.

Summary

  • Use Environment Variables for configuration
  • Follow security best practices (Helmet, Compression)
  • Use a process manager like PM2 for VPS deployments
  • Platforms like Heroku and Vercel offer easy deployment

Docker Deployment

Basic Dockerfile

# Dockerfile
FROM node:20-alpine

# Create app directory
WORKDIR /usr/src/app

# Copy package files
COPY package*.json ./

# Install dependencies (production only)
RUN npm ci --only=production

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Start the app
CMD ["node", "server.js"]

Optimized Multi-Stage Build

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build  # If you have a build step

# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodeapp -u 1001

# Copy only production dependencies
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy built app
COPY --from=builder /app/dist ./dist

# Switch to non-root user
USER nodeapp

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

Docker Compose for Development

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=mongodb://mongo:27017/myapp
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    depends_on:
      - mongo
      - redis

  mongo:
    image: mongo:6
    volumes:
      - mongo-data:/data/db
    ports:
      - "27017:27017"

  redis:
    image: redis:alpine
    ports:
      - "6379:6379"

volumes:
  mongo-data:
# Run with Docker Compose
docker-compose up -d
docker-compose logs -f app
docker-compose down

AWS Deployment Options

ServiceBest ForComplexity
Elastic BeanstalkQuick deployment, managedLow
ECS/FargateDocker containersMedium
EC2Full controlHigh
LambdaServerless, APIsLow
App RunnerContainers, simpleLow

AWS Elastic Beanstalk

# Install EB CLI
pip install awsebcli

# Initialize and deploy
eb init my-node-app --platform node.js
eb create production
eb deploy

# Set environment variables
eb setenv NODE_ENV=production DATABASE_URL=...

Health Checks

Always implement health endpoints:
// Basic health check
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

// Comprehensive health check
app.get('/health/detailed', async (req, res) => {
  const health = {
    uptime: process.uptime(),
    timestamp: Date.now(),
    status: 'healthy',
    checks: {}
  };

  // Database check
  try {
    await mongoose.connection.db.admin().ping();
    health.checks.database = { status: 'healthy' };
  } catch (error) {
    health.status = 'unhealthy';
    health.checks.database = { status: 'unhealthy', error: error.message };
  }

  // Redis check
  try {
    await redis.ping();
    health.checks.redis = { status: 'healthy' };
  } catch (error) {
    health.status = 'unhealthy';
    health.checks.redis = { status: 'unhealthy', error: error.message };
  }

  // Memory check
  const memUsage = process.memoryUsage();
  health.checks.memory = {
    heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`
  };

  res.status(health.status === 'healthy' ? 200 : 503).json(health);
});

Graceful Shutdown

const server = app.listen(PORT);

const gracefulShutdown = async (signal) => {
  console.log(`Received ${signal}. Starting graceful shutdown...`);

  // Stop accepting new connections
  server.close(async () => {
    console.log('HTTP server closed');

    try {
      // Close database connections
      await mongoose.connection.close();
      console.log('MongoDB connection closed');

      // Close Redis
      await redis.quit();
      console.log('Redis connection closed');

      console.log('Graceful shutdown completed');
      process.exit(0);
    } catch (error) {
      console.error('Error during shutdown:', error);
      process.exit(1);
    }
  });

  // Force shutdown after 30 seconds
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

CI/CD with GitHub Actions

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          # Add deployment steps
          echo "Deploying..."