Skip to main content

Chapter 9: Deployment & Production

Deploying and running your NestJS app in production requires careful planning. This chapter covers Dockerization, CI/CD, environment management, health checks, logging, monitoring, scaling, and troubleshooting. We’ll walk through practical steps and explain how to make your app production-ready.

9.1 Preparing for Production

Before deploying, ensure your application is production-ready.

Production Checklist

Environment Configuration:
  • Set NODE_ENV=production
  • Use environment variables for all secrets
  • Remove hardcoded credentials
  • Validate all environment variables
Security:
  • Enable CORS with specific origins
  • Set secure HTTP headers (helmet)
  • Use HTTPS
  • Validate all inputs
  • Rate limiting enabled
Performance:
  • Build optimized bundle (npm run build)
  • Remove dev dependencies
  • Enable compression
  • Optimize database queries
  • Use connection pooling
Monitoring:
  • Health checks configured
  • Logging set up
  • Error tracking (Sentry, etc.)
  • Metrics collection
Testing:
  • All tests passing
  • Tested in staging environment
  • Load testing completed
  • Security audit done

9.2 Dockerizing Your App

Containerization makes deployment consistent and portable. Docker packages your app and dependencies into a single image.

Basic Dockerfile

FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM node:18-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./

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

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

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

USER nestjs

EXPOSE 3000

CMD ["node", "dist/main"]

Optimized Dockerfile

FROM node:18-alpine AS deps

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production && \
    npm cache clean --force

FROM node:18-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:18-alpine

WORKDIR /app

# Copy production dependencies
COPY --from=deps /app/node_modules ./node_modules

# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./

# Security: Run as non-root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nestjs -u 1001 && \
    chown -R nestjs:nodejs /app

USER nestjs

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

CMD ["node", "dist/main"]

.dockerignore

node_modules
npm-debug.log
dist
.git
.gitignore
.env
.env.local
*.md
.vscode
.idea
coverage
.nyc_output
test
*.spec.ts

Docker Compose

version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@db:5432/mydb
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:14-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:
Best Practices:
  • Use multi-stage builds for smaller images
  • Keep images minimal (alpine base, no dev dependencies)
  • Use .dockerignore to exclude unnecessary files
  • Run as non-root user
  • Add health checks
  • Use specific version tags

9.3 Environment Variables

Store secrets and configuration in environment variables. Never commit secrets to version control.

Using @nestjs/config

npm install @nestjs/config
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env.local', '.env'],
      validationSchema: Joi.object({
        NODE_ENV: Joi.string()
          .valid('development', 'production', 'test')
          .default('development'),
        PORT: Joi.number().default(3000),
        DATABASE_URL: Joi.string().required(),
        JWT_SECRET: Joi.string().required(),
      }),
    }),
  ],
})
export class AppModule {}

Environment Files

# .env.example (committed)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/mydb
JWT_SECRET=your-secret-key

# .env (not committed)
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:password@db:5432/mydb
JWT_SECRET=super-secret-key-change-in-production

Using Config Service

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
  constructor(private configService: ConfigService) {}

  getDatabaseUrl(): string {
    return this.configService.get<string>('DATABASE_URL');
  }

  getJwtSecret(): string {
    return this.configService.get<string>('JWT_SECRET');
  }
}
Tip: Use schema validation (e.g., with joi) to ensure required environment variables are set and validate their values.

9.4 Health Checks

Health checks help load balancers and orchestrators know if your app is healthy.

Installing Terminus

npm install @nestjs/terminus

Health Check Controller

// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }

  @Get('readiness')
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }

  @Get('liveness')
  @HealthCheck()
  liveness() {
    return { status: 'ok', timestamp: new Date().toISOString() };
  }
}

Custom Health Indicators

import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';

@Injectable()
export class CustomHealthIndicator extends HealthIndicator {
  async isHealthy(key: string): Promise<HealthIndicatorResult> {
    const isHealthy = await this.checkExternalService();
    const result = this.getStatus(key, isHealthy);

    if (isHealthy) {
      return result;
    }
    throw new HealthCheckError('External service check failed', result);
  }

  private async checkExternalService(): Promise<boolean> {
    // Check external service
    return true;
  }
}
Diagram: Health Check Flow
Load Balancer/Orchestrator

GET /health

Health Check Service

Check Database, External Services, etc.

Return Status (healthy/unhealthy)

9.5 Logging & Monitoring

Proper logging and monitoring are essential for production applications.

NestJS Logger

import { Logger } from '@nestjs/common';

@Injectable()
export class UsersService {
  private readonly logger = new Logger(UsersService.name);

  async create(dto: CreateUserDto) {
    this.logger.log(`Creating user: ${dto.email}`);
    
    try {
      const user = await this.userRepository.create(dto);
      this.logger.log(`User created successfully: ${user.id}`);
      return user;
    } catch (error) {
      this.logger.error(`Failed to create user: ${error.message}`, error.stack);
      throw error;
    }
  }
}

Structured Logging

import { Logger } from '@nestjs/common';

@Injectable()
export class LoggerService {
  private readonly logger = new Logger();

  log(message: string, context?: string, metadata?: any) {
    this.logger.log(JSON.stringify({
      message,
      context,
      metadata,
      timestamp: new Date().toISOString(),
    }));
  }

  error(message: string, trace?: string, context?: string) {
    this.logger.error(JSON.stringify({
      message,
      trace,
      context,
      timestamp: new Date().toISOString(),
    }));
  }
}

Winston Integration

npm install nest-winston winston
import { Module } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';

@Module({
  imports: [
    WinstonModule.forRoot({
      transports: [
        new winston.transports.File({
          filename: 'error.log',
          level: 'error',
        }),
        new winston.transports.File({
          filename: 'combined.log',
        }),
      ],
    }),
  ],
})
export class AppModule {}

Error Tracking with Sentry

npm install @sentry/node @sentry/tracing
// main.ts
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [nodeProfilingIntegration()],
  tracesSampleRate: 1.0,
  profilesSampleRate: 1.0,
});
Best Practices:
  • Log errors and warnings
  • Use structured logs (JSON) for cloud platforms
  • Monitor logs and metrics in real time
  • Set up alerts for critical errors
  • Don’t log sensitive information
  • Use log levels appropriately

9.6 CI/CD Pipelines

Automate build, test, and deployment with CI/CD pipelines.

GitHub Actions Workflow

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

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run test
      - run: npm run test:e2e

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker push myapp:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        run: |
          kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}

GitLab CI

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

test:
  stage: test
  script:
    - npm ci
    - npm run lint
    - npm run test
    - npm run test:e2e

build:
  stage: build
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy:
  stage: deploy
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

9.7 Kubernetes Deployment

Deploy NestJS applications to Kubernetes for scalability and reliability.

Deployment Manifest

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nestjs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nestjs-app
  template:
    metadata:
      labels:
        app: nestjs-app
    spec:
      containers:
      - name: app
        image: myapp:latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        livenessProbe:
          httpGet:
            path: /health/liveness
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/readiness
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

Service Manifest

# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nestjs-app-service
spec:
  selector:
    app: nestjs-app
  ports:
  - port: 80
    targetPort: 3000
  type: LoadBalancer

9.8 Scaling & High Availability

Scale your application to handle increased load.

Horizontal Scaling

  • Run multiple instances behind a load balancer
  • Use Kubernetes HPA (Horizontal Pod Autoscaler)
  • Scale based on CPU, memory, or custom metrics

Vertical Scaling

  • Increase instance size (more CPU/memory)
  • Use for applications that can’t scale horizontally
  • Limited by single instance capacity

Database Scaling

  • Use read replicas for read-heavy workloads
  • Shard databases for very large datasets
  • Use connection pooling
  • Cache frequently accessed data

Caching

import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-store';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost',
      port: 6379,
    }),
  ],
})
export class AppModule {}

9.9 Performance Optimization

Optimize your application for production performance.

Enable Compression

// main.ts
import * as compression from 'compression';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(compression());
  await app.listen(3000);
}

Connection Pooling

TypeOrmModule.forRoot({
  // ... other options
  extra: {
    max: 10,
    min: 2,
    idleTimeoutMillis: 30000,
  },
})

Query Optimization

  • Use indexes on frequently queried columns
  • Optimize N+1 queries
  • Use select to limit fields
  • Implement pagination

9.10 Troubleshooting & Maintenance

Monitor and maintain your production application.

Monitoring

  • Monitor CPU, memory, and response times
  • Track error rates
  • Monitor database performance
  • Set up alerts for anomalies

Logging

  • Centralize logs (ELK, CloudWatch, etc.)
  • Search and filter logs
  • Set up log retention policies
  • Monitor log volumes

Backup & Recovery

  • Regular database backups
  • Test restore procedures
  • Document recovery steps
  • Store backups securely

Updates

  • Regularly update dependencies
  • Test updates in staging
  • Use semantic versioning
  • Document breaking changes

9.11 Summary

You’ve learned how to deploy and maintain NestJS applications in production: Key Concepts:
  • Docker: Containerize applications
  • CI/CD: Automate deployment
  • Health Checks: Monitor application health
  • Logging: Track application behavior
  • Monitoring: Observe production systems
  • Scaling: Handle increased load
  • Kubernetes: Orchestrate containers
Best Practices:
  • Use environment variables for configuration
  • Containerize with Docker
  • Implement health checks
  • Set up proper logging
  • Monitor production systems
  • Scale horizontally
  • Regular backups and updates
Next Chapter: Learn about advanced patterns like CQRS, GraphQL, WebSockets, and event sourcing.