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.

January 10, 2024
12 min read
By Naveed Ullah
DockerNestJSContainerizationDeploymentDevOps
How to Containerize NestJS Applications with Docker
Share this article:

How to Containerize NestJS Applications with Docker

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!

    Naveed Ullah

    About Naveed Ullah

    Senior Next.js & NestJS Developer with 5+ years of experience building scalable applications