← All writing
DevOps

How to Containerize NestJS Applications with Docker

Step-by-step guide to containerizing your NestJS applications using Docker. Includes multi-stage builds, optimization techniques, and deployment strategies.

Jan 10, 202412 min readBy Naveed Ullah

Containerization has become essential for modern application deployment. In this comprehensive guide, we'll explore how to containerize NestJS applications using Docker, including best practices for production deployments.

Why Containerize NestJS Applications?

Docker containers provide several benefits for NestJS applications:

  • Consistency: Same environment across development, staging, and production
  • Scalability: Easy horizontal scaling with container orchestration
  • Isolation: Applications run in isolated environments
  • Portability: Deploy anywhere Docker is supported
  • Resource Efficiency: Lightweight compared to virtual machines

Basic Dockerfile for NestJS

Let's start with a basic Dockerfile for a NestJS application:

# Dockerfile
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Expose port
EXPOSE 3000

# Start the application
CMD ["npm", "run", "start:prod"]

Multi-Stage Docker Build

For production applications, use multi-stage builds to optimize image size:

# Multi-stage Dockerfile
# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install all dependencies (including dev dependencies)
RUN npm ci

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Stage 2: Production
FROM node:18-alpine AS production

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force

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

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

# Change ownership of the app directory
RUN chown -R nestjs:nodejs /app
USER nestjs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# Start the application
CMD ["node", "dist/main"]

Docker Compose for Development

Create a docker-compose.yml for local development:

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@db:5432/nestjs_db
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    command: npm run start:dev

  db:
    image: postgres:15-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=nestjs_db
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Development Dockerfile

Create a separate Dockerfile for development:

# Dockerfile.dev
FROM node:18-alpine

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Start development server
CMD ["npm", "run", "start:dev"]

Environment Configuration

Environment Variables

Use environment variables for configuration:

// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST || 'localhost',
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
    username: process.env.DATABASE_USERNAME || 'user',
    password: process.env.DATABASE_PASSWORD || 'password',
    database: process.env.DATABASE_NAME || 'nestjs_db',
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT, 10) || 6379,
  },
});

Docker Environment File

Create a .env.docker file:

# .env.docker
NODE_ENV=production
PORT=3000
DATABASE_HOST=db
DATABASE_PORT=5432
DATABASE_USERNAME=user
DATABASE_PASSWORD=password
DATABASE_NAME=nestjs_db
REDIS_HOST=redis
REDIS_PORT=6379

Optimization Techniques

1. Layer Caching

Optimize Docker layer caching by copying package files first:

# Copy package files first for better caching
COPY package*.json ./
RUN npm ci --only=production

# Then copy source code
COPY . .

2. .dockerignore File

Create a .dockerignore file to exclude unnecessary files:

# .dockerignore
node_modules
npm-debug.log
dist
.git
.gitignore
README.md
.env
.nyc_output
coverage
.coverage
.env.test
.env.local
.env.development.local
.env.staging.local
.env.production.local

3. Alpine Linux

Use Alpine Linux for smaller image sizes:

FROM node:18-alpine

4. Non-Root User

Run containers as non-root user for security:

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001
USER nestjs

Production Deployment

Docker Compose for Production

# docker-compose.prod.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    env_file:
      - .env.docker
    depends_on:
      - db
      - redis
    restart: unless-stopped
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    restart: unless-stopped

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

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Health Checks

Implement health checks in your NestJS application:

// 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'),
    ]);
  }
}

Monitoring and Logging

Structured Logging

Configure structured logging for containers:

// logger.config.ts
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';

export const loggerConfig = WinstonModule.createLogger({
  transports: [
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json(),
      ),
    }),
  ],
});

Container Monitoring

Use Docker stats to monitor container performance:

# Monitor container stats
docker stats

# View container logs
docker logs -f container_name

# Execute commands in running container
docker exec -it container_name sh

CI/CD Pipeline

GitHub Actions Example

# .github/workflows/docker.yml
name: Docker Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to DockerHub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    
    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: your-username/nestjs-app:latest
        cache-from: type=gha
        cache-to: type=gha,mode=max

Security Best Practices

1. Use Official Base Images

Always use official Node.js images:

FROM node:18-alpine

2. Scan for Vulnerabilities

Regularly scan images for vulnerabilities:

docker scan your-image:tag

3. Limit Container Privileges

Run containers with limited privileges:

USER nestjs

4. Use Secrets Management

Don't include secrets in images:

docker run -e DATABASE_PASSWORD_FILE=/run/secrets/db_password your-image

Troubleshooting

Common Issues

  1. Port conflicts: Ensure ports aren't already in use
  2. Permission issues: Check file permissions and user context
  3. Memory limits: Configure appropriate memory limits
  4. Network connectivity: Verify service communication

Debugging Commands

# Check container logs
docker logs container_name

# Execute shell in container
docker exec -it container_name sh

# Inspect container configuration
docker inspect container_name

# Check container processes
docker top container_name

Conclusion

Containerizing NestJS applications with Docker provides numerous benefits for development and deployment. By following these best practices, you can create efficient, secure, and scalable containerized applications.

Key takeaways:

  • Use multi-stage builds for production optimization
  • Implement proper health checks and monitoring
  • Follow security best practices
  • Use Docker Compose for local development
  • Implement CI/CD pipelines for automated deployment

Start containerizing your NestJS applications today and experience the benefits of modern deployment practices!

DockerNestJSContainerizationDeploymentDevOps